merge malte/first_encryption
This commit is contained in:
+8
-7
@@ -1,12 +1,6 @@
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
|
||||
build-job:
|
||||
stage: build
|
||||
image: localhost:5000/huskbuilder:latest
|
||||
script:
|
||||
- cargo build
|
||||
- e2e
|
||||
|
||||
test-job:
|
||||
stage: test
|
||||
@@ -14,3 +8,10 @@ test-job:
|
||||
script:
|
||||
- cargo test
|
||||
|
||||
e2e-job:
|
||||
stage: e2e
|
||||
image: localhost:5000/mta:latest
|
||||
script:
|
||||
- bash /usr/bin/prepare.sh $CI_COMMIT_BRANCH
|
||||
- bash /usr/bin/test_1.sh
|
||||
|
||||
|
||||
@@ -16,7 +16,12 @@ bytes = "1.10.1"
|
||||
indymilter = "0.3.0"
|
||||
log = "0.4.27"
|
||||
log4rs = "1.3.0"
|
||||
sequoia-cert-store = "0.7.1"
|
||||
sequoia-directories = "0.1.0"
|
||||
sequoia-openpgp = "2.1.0"
|
||||
sequoia-wot = "0.15.0"
|
||||
serde = "1.0.228"
|
||||
serde_derive = "1.0.228"
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.47.1", features = [ "tokio-macros", "rt-multi-thread", "signal" ] }
|
||||
toml = "0.9.8"
|
||||
|
||||
@@ -3,4 +3,6 @@
|
||||
#
|
||||
connection = "localhost:3000"
|
||||
|
||||
sequoia_home = "/tmp/sq_home"
|
||||
|
||||
logfile_config = "./config/log4rs.yml"
|
||||
|
||||
+20
-7
@@ -2,31 +2,34 @@
|
||||
// Parsing the config file
|
||||
//
|
||||
|
||||
use anyhow;
|
||||
use std::fs;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
use crate::types::errors::HuskError;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct HuskConfig {
|
||||
pub connection: String,
|
||||
pub sequoia_home: String,
|
||||
logfile_config: Option<String>,
|
||||
}
|
||||
|
||||
impl HuskConfig {
|
||||
|
||||
pub fn load(path: &String) -> Option<Self> {
|
||||
pub fn load(path: &String) -> anyhow::Result<Self> {
|
||||
match fs::read_to_string(path) {
|
||||
Ok(data) => {
|
||||
match toml::from_str(data.as_str()) {
|
||||
Ok(configuration) => Some(configuration),
|
||||
Ok(configuration) => Ok(configuration),
|
||||
Err(e) => {
|
||||
eprintln!("Error while parsing config file: {:?}", e);
|
||||
None
|
||||
Err(HuskError::ConfigFileParseError(e.to_string()).into())
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Error while reading config file: {:?}", e);
|
||||
None
|
||||
Err(HuskError::ConfigFileOpenError(e.to_string()).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,3 +44,13 @@ impl HuskConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type HuskConfigContainer = Arc<Mutex<HuskConfig>>;
|
||||
|
||||
impl From<HuskConfigContainer> for HuskConfig {
|
||||
fn from(container: HuskConfigContainer) -> Self {
|
||||
let config = container.lock().unwrap();
|
||||
config.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
//
|
||||
// Husk milter
|
||||
//
|
||||
// cryptographic functions
|
||||
//
|
||||
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow;
|
||||
use bytes::Bytes;
|
||||
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
|
||||
use sequoia_openpgp::policy::StandardPolicy;
|
||||
use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, LiteralWriter};
|
||||
use sequoia_openpgp::{Fingerprint, Cert, Packet};
|
||||
use sequoia_openpgp::cert::raw::RawCertParser;
|
||||
use sequoia_cert_store::{Store, CertStore, LazyCert};
|
||||
use sequoia_wot::{self as wot};
|
||||
use wot::store::Store as _;
|
||||
use wot::{Depth, Path};
|
||||
|
||||
use crate::types::errors::HuskError;
|
||||
use crate::types::husk_context::HuskContext;
|
||||
use crate::types::introducer::Introducer;
|
||||
use crate::types::recipient::Recipient;
|
||||
|
||||
/// Returns the local trust root from the cert_store. If the trust root
|
||||
/// cannot be found, it returns an error.
|
||||
pub fn get_local_trust_root(cert_store: &CertStore) -> anyhow::Result<Cert> {
|
||||
|
||||
let root = cert_store.certd()
|
||||
.and_then(|certd| {
|
||||
match certd.certd().get(sequoia_cert_store::store::openpgp_cert_d::TRUST_ROOT) {
|
||||
Ok(Some((_tag, bytes))) => Some(bytes),
|
||||
Ok(None) => None,
|
||||
Err(_) => None
|
||||
}
|
||||
})
|
||||
.and_then(|bytes| {
|
||||
match RawCertParser::from_bytes(&bytes[..]) {
|
||||
Ok(mut parser) => {
|
||||
match parser.next() {
|
||||
Some(Ok(cert)) => {
|
||||
match Cert::from_bytes(cert.as_bytes()) {
|
||||
Ok(c) => Some(c),
|
||||
Err(_) => None,
|
||||
}
|
||||
},
|
||||
Some(Err(_))
|
||||
| None => None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
}
|
||||
});
|
||||
|
||||
match root {
|
||||
Some(r) => Ok(r),
|
||||
None => Err(HuskError::NoLocalTrustRoot.into())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Returns all certificates with an authenticated binding to user id 'email'.
|
||||
/// The certificates are not checked for usefulness (for instance, they might
|
||||
/// miss an encryption subkey).
|
||||
pub fn get_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
|
||||
-> Vec<Arc<LazyCert<'hc>>> {
|
||||
|
||||
let cert_store = &context.cert_store;
|
||||
let trust_roots = vec![(context.local_trust_root.fingerprint(), wot::FULLY_TRUSTED)];
|
||||
let wot_store = wot::store::CertStore::from_store(cert_store, &context.policy, None);
|
||||
let nb = wot::NetworkBuilder::rooted(&wot_store, &*trust_roots);
|
||||
let n = nb.build();
|
||||
|
||||
let bindings: Vec<_> = n.lookup_synopses_by_email(email)
|
||||
.into_iter()
|
||||
.map(|(fp, userid)| (fp, Some(userid)))
|
||||
.collect();
|
||||
|
||||
let mut result = vec![];
|
||||
for (fpr, some_userid) in bindings {
|
||||
if let Some(userid) = some_userid {
|
||||
let paths = n.authenticate(userid, &fpr, wot::FULLY_TRUSTED);
|
||||
if paths.len() > 0 {
|
||||
log::debug!("{} authenticated!!!", email);
|
||||
for (path, _) in paths.iter() {
|
||||
if let Ok(cert) = cert_store.lookup_by_cert_fpr(&path.target().fingerprint()) {
|
||||
result.push(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns all introducers available in cert_store.
|
||||
pub fn get_introducers<'c>(cert_store: &CertStore<'c>, policy: &StandardPolicy, local_trust_root: Fingerprint) -> Vec<Introducer> {
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
let trust_roots = vec![(local_trust_root, wot::FULLY_TRUSTED)];
|
||||
let wot_store = wot::store::CertStore::from_store(cert_store, policy, None);
|
||||
let n = wot::NetworkBuilder::rooted(&wot_store, &*trust_roots)
|
||||
.build();
|
||||
|
||||
n.certified_userids().iter().for_each(|(fpr, user_id)| {
|
||||
let paths = n.authenticate(user_id, fpr, wot::FULLY_TRUSTED);
|
||||
|
||||
// find the longest path (highest trust depth)
|
||||
let mut depth = Depth::Limit(0);
|
||||
let mut target: Option<&Path> = None;
|
||||
paths.iter().for_each(|(path, amount)| {
|
||||
if *amount == wot::FULLY_TRUSTED {
|
||||
let d = path.residual_depth();
|
||||
if d > depth {
|
||||
depth = d;
|
||||
target = Some(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if depth > Depth::Limit(0) {
|
||||
if let Some(p) = target {
|
||||
if let Some(c) = p.certifications().last() {
|
||||
result.push( Introducer {
|
||||
cert: p.target().clone(),
|
||||
certification: c.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Checks if a message contains the OpenPGP artifacts of an encrypted message.
|
||||
pub fn is_encrypted(body: &Bytes) -> bool {
|
||||
|
||||
let mut ppr = match PacketParser::from_bytes(body) {
|
||||
Ok(pp) => pp,
|
||||
Err(_) => return false, // no OpenPGP artifacts
|
||||
};
|
||||
|
||||
let mut encrypted: bool = false;
|
||||
|
||||
while let PacketParserResult::Some(pp) = ppr {
|
||||
if let Packet::SEIP(_) = pp.packet {
|
||||
encrypted = true;
|
||||
}
|
||||
|
||||
// parse the whole message, don't stop at first SEIP
|
||||
ppr = match pp.recurse() {
|
||||
Ok((_, next_ppr)) => next_ppr,
|
||||
Err(_) => return false, // broken artifact -> not encrypted
|
||||
}
|
||||
}
|
||||
if let PacketParserResult::EOF(eof) = ppr {
|
||||
if eof.is_message().is_ok() && encrypted {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Encrypt a message for `recipients`.
|
||||
pub fn encrypt(context: &HuskContext<'_>, body: &Bytes, recipients: &Vec<Recipient>)
|
||||
-> anyhow::Result<Bytes> {
|
||||
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// XXX: handle the case where a certificate misses an encryption subkey
|
||||
for rcpt in recipients {
|
||||
if rcpt.can_encrypt() {
|
||||
for cert in &rcpt.certs {
|
||||
cert.with_policy(&context.policy, None)?
|
||||
.keys()
|
||||
.supported()
|
||||
.alive()
|
||||
.revoked(false)
|
||||
.for_transport_encryption()
|
||||
.for_each(|k| {
|
||||
keys.push(k);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if keys.len() > 0 {
|
||||
let mut sink = Vec::new();
|
||||
let message = Message::new(&mut sink);
|
||||
let message = Armorer::new(message).build()?;
|
||||
let message = Encryptor::for_recipients(message, keys).build()?;
|
||||
let mut message = LiteralWriter::new(message).build()?;
|
||||
message.write_all(&body[..])?;
|
||||
message.finalize()?;
|
||||
|
||||
Ok(Bytes::from(sink))
|
||||
} else {
|
||||
Err(HuskError::NoEncryptionKeys.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use bytes::Bytes;
|
||||
|
||||
#[test]
|
||||
pub fn test_is_encrypted() {
|
||||
|
||||
use crate::crypto::is_encrypted;
|
||||
|
||||
// cleartext
|
||||
let body = b"Hello world";
|
||||
let body = Bytes::from(&body[..]);
|
||||
assert!(! is_encrypted(&body));
|
||||
|
||||
// a valid encrypted message
|
||||
let body = b"-----BEGIN PGP MESSAGE-----
|
||||
|
||||
wWwGFQRhclhA/Qg3CiL81NAswyOWkyk8jxIBB0AmlBkVEVbQst60C/1+mcmIxLbG
|
||||
m0w+qv2aP5NQhAetCzDgA/KEhtim0Ebzf06jJG7bITPkYmfN2mizdChgv1f3y2eQ
|
||||
80hYpgbD930RtKXTD1TSWgIJAgai2MelZrDAQXIaFKlRRY9u7R1YIXQmMa6uDced
|
||||
W/g9mJDd0ral30VL0FfuKRR2Je2TRfizqMb4tL4EIRFqrfSBesb9WcniEfBekG3B
|
||||
jk6lh01Ej9QTGQ==
|
||||
-----END PGP MESSAGE-----
|
||||
";
|
||||
|
||||
let body = Bytes::from(&body[..]);
|
||||
assert!(is_encrypted(&body));
|
||||
|
||||
// a PUBLIC KEY BLOCK is an artifact, but it's not encrypted
|
||||
let body = b"-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xjMEaUE/aRYJKwYBBAHaRw8BAQdAXScnsTjHmy1yZ/SmddPq7tmY4SZwsyWmRNei
|
||||
EX60UJDCwBEEHxYKAIMFgmlBP2kFiQWkj70DCwkHCRC9/0MioyzN/0cUAAAAAAAe
|
||||
ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfUHGvbmmp7vFwpMzGj36fZ
|
||||
Px+taozYHxyGljxsJnlV3AMVCggCmwECHgkWIQSQO2vdjfwfK4J6FaK9/0MioyzN
|
||||
/wAATokBAP9abr1BeV2qrvwF9TsQDlvgnaNPtzhPy00oDi17/8x5AP99DLK8hAJ3
|
||||
FtejeAJ0/2+BfUZzRlF8tlSGe5/Iln9DCM0TPGFsaWNlQGV4YW1wbGUuY29tPsLA
|
||||
FAQTFgoAhgWCaUE/aQWJBaSPvQMLCQcJEL3/QyKjLM3/RxQAAAAAAB4AIHNhbHRA
|
||||
bm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZyWqUJWT+0+sGq1ci9IkqHvnsUN+c5LR
|
||||
B921ZbGlCwLXAxUKCAKZAQKbAQIeCRYhBJA7a92N/B8rgnoVor3/QyKjLM3/AAB5
|
||||
+AEAvMCVVynlwsaFQvACga9NTfyZGQ7IjhFhjbCXY+CZbHoA/0FaajzhUF6mKQH7
|
||||
RMwMK6Dp0YD2A34/DGb5KbDkLjIOzjMEaUE/aRYJKwYBBAHaRw8BAQdAd4Y3kt2s
|
||||
NszsrkMELS0FC2viTw+NEU0ZnAT/aD5eydLCwMUEGBYKATcFgmlBP2kFiQWkj70J
|
||||
EL3/QyKjLM3/RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y
|
||||
Z1R29gVOHk9hllft18uHrwEV0MgvzykG3yWKF1B+vT9hApsCvqAEGRYKAG8FgmlB
|
||||
P2kJEHx7uc2lNPhjRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdw
|
||||
Lm9yZ9Q8ycbljQVzg2YapbQX+IJ9H+1KSgSLXQtVtCsaJh8RFiEEerJu6BkdLdse
|
||||
21ngfHu5zaU0+GMAAD3BAQCzmPr7q3Wf1BFKO95WBhe66Cg+lAnoJZKbgHBpUsIB
|
||||
6wEAmGzOcL1zYXRBWpquzFKS2bNxuFdpbewkxkf0axvMnA8WIQSQO2vdjfwfK4J6
|
||||
FaK9/0MioyzN/wAAoD8A/2a7+DrKEjL7fAt7UZnPYZOXPEOpYXxUE3ZGQbnsKrdw
|
||||
AP0ae/B4LYXd89UwHpQUBEJHbY113W8NRUfpM/EJUQRdBM4zBGlBP2kWCSsGAQQB
|
||||
2kcPAQEHQNhqbQtL5hS4w0n9H0hHMiD84WY39ukclu1yCtBii/+xwsDFBBgWCgE3
|
||||
BYJpQT9pBYkFpI+9CRC9/0MioyzN/0cUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z
|
||||
ZXF1b2lhLXBncC5vcmd6G9Mcoj01NqY/TIgPlhe4L5OMUS/R4cIc2wWl2EmQTAKb
|
||||
IL6gBBkWCgBvBYJpQT9pCRAR5pEu+j17CUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u
|
||||
cy5zZXF1b2lhLXBncC5vcmeJ47SGs3Kf1A20PTM/BFkWz2zd2dfaiVx8zKNbXoEZ
|
||||
9BYhBK5UVQP/gdjl2RIb3RHmkS76PXsJAACjYgD/SfQ8WoCnBDnywpVknkW8FtHd
|
||||
akewRjkT17Q5gK1NEWUA/jOMj8chhN4R6jnnWEb0ClujkkVMBWP7h2N5HWGN4DoD
|
||||
FiEEkDtr3Y38HyuCehWivf9DIqMszf8AAC7NAQCNZWDduo/vseZGLDNOK9y+krFZ
|
||||
fDSGVBmvBHGiGjX6agD/SHcsROm0C/HXe+KxS/SAhHob2KdZ8FQ+dCtr3oY/egXO
|
||||
OARpQT9pEgorBgEEAZdVAQUBAQdA1dxpaBPWnqr8lhzucvP2F746IIdfYk+UVImg
|
||||
pe3GD1UDAQgHwsAGBBgWCgB4BYJpQT9pBYkFpI+9CRC9/0MioyzN/0cUAAAAAAAe
|
||||
ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeDJU2fBZAzwn32lcgdEyjB
|
||||
/LtqMEQ7Qv5HwfJFRlseCAKbDBYhBJA7a92N/B8rgnoVor3/QyKjLM3/AADjHQEA
|
||||
yvN5w7wxf50yriNlrHCwRzQ4MljfJawXfAfOthJlh8gBANycfude13YmUeFvwuyQ
|
||||
Vmac4mRS69ivzHMLDNXUsMwB
|
||||
=WPEh
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
";
|
||||
let body = Bytes::from(&body[..]);
|
||||
assert!(! is_encrypted(&body));
|
||||
|
||||
// broken message
|
||||
let body = b"-----BEGIN PGP MESSAGE-----
|
||||
|
||||
wWwGFQRhclhA/Qg3CiL81NAswyOWkyk8jxIBB0AmlBkVEVbQst60C/1+mcmIxLbG
|
||||
W/g9mJDd0ral30VL0FfuKRR2Je2TRfizqMb4tL4EIRFqrfSBesb9WcniEfBekG3B
|
||||
jk6lh01Ej9QTGQ==
|
||||
-----END PGP MESSAGE-----
|
||||
";
|
||||
let body = Bytes::from(&body[..]);
|
||||
assert!(! is_encrypted(&body));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+124
-43
@@ -17,21 +17,26 @@ use indymilter::{
|
||||
EomContext,
|
||||
Macros,
|
||||
NegotiateContext,
|
||||
ProtoOpts,
|
||||
ProtoOpts,
|
||||
SocketInfo,
|
||||
Status,
|
||||
Config,
|
||||
Status,
|
||||
Config,
|
||||
ContextActions,
|
||||
};
|
||||
|
||||
use crate::types::husk_context::HuskContext;
|
||||
use crate::config::HuskConfig;
|
||||
use crate::config::{HuskConfig, HuskConfigContainer};
|
||||
use crate::mail;
|
||||
use crate::crypto;
|
||||
use crate::types::mail_context::ProtectionPossibility;
|
||||
|
||||
pub struct Daemon { }
|
||||
|
||||
impl Daemon {
|
||||
|
||||
pub async fn run(config: &HuskConfig) -> anyhow::Result<()> {
|
||||
pub async fn run(config_container: HuskConfigContainer) -> anyhow::Result<()> {
|
||||
|
||||
let config: HuskConfig = config_container.clone().into();
|
||||
|
||||
let listener = TcpListener::bind(&config.connection)
|
||||
.await
|
||||
@@ -40,7 +45,7 @@ impl Daemon {
|
||||
let callbacks = Callbacks::new()
|
||||
.on_negotiate(|cx, actions, opts| Box::pin(Self::handle_negotiate(cx, actions, opts)))
|
||||
.on_connect(|cx, hostname, socket_info| Box::pin(Self::handle_connect(cx, hostname, socket_info)))
|
||||
.on_helo(|cx, hostname| Box::pin(Self::handle_helo(cx, hostname)))
|
||||
.on_helo(move |cx, hostname| Box::pin(Self::handle_helo(cx, config_container.clone(), hostname)))
|
||||
.on_mail(|cx, args| Box::pin(Self::handle_mail(cx, args)))
|
||||
.on_rcpt(|cx, args| Box::pin(Self::handle_rcpt(cx, args)))
|
||||
.on_data(|cx| Box::pin(Self::handle_data(cx)))
|
||||
@@ -62,7 +67,7 @@ impl Daemon {
|
||||
}
|
||||
|
||||
async fn handle_negotiate(
|
||||
_cx: &mut NegotiateContext<HuskContext>,
|
||||
_cx: &mut NegotiateContext<HuskContext<'_>>,
|
||||
actions: Actions,
|
||||
opts: ProtoOpts,
|
||||
) -> Status {
|
||||
@@ -72,7 +77,7 @@ impl Daemon {
|
||||
}
|
||||
|
||||
async fn handle_connect(
|
||||
_cx: &mut Context<HuskContext>,
|
||||
_cx: &mut Context<HuskContext<'_>>,
|
||||
hostname: CString,
|
||||
socket_info: SocketInfo,
|
||||
) -> Status {
|
||||
@@ -82,87 +87,163 @@ impl Daemon {
|
||||
Status::Continue
|
||||
}
|
||||
|
||||
async fn handle_helo(_cx: &mut Context<HuskContext>, hostname: CString) -> Status {
|
||||
async fn handle_helo(cx: &mut Context<HuskContext<'_>>, config_container: HuskConfigContainer, hostname: CString) -> Status {
|
||||
log::debug!("HELO: hostname: {hostname:?}");
|
||||
|
||||
Status::Continue
|
||||
}
|
||||
|
||||
async fn handle_mail(cx: &mut Context<HuskContext>, args: Vec<CString>) -> Status {
|
||||
log::debug!("MAIL: {args:?}");
|
||||
|
||||
let sender = match args.first() {
|
||||
Some(cs) => {
|
||||
match cs.to_str() {
|
||||
Ok(s) => s.to_string(),
|
||||
_ => "corrupt".to_string()
|
||||
}
|
||||
},
|
||||
None => { "missing".to_string() }
|
||||
};
|
||||
let config: HuskConfig = config_container.into();
|
||||
|
||||
// setup Context
|
||||
if let Some(mut context) = HuskContext::new() {
|
||||
context.set_sender(sender);
|
||||
cx.data = Some(context).take();
|
||||
match HuskContext::new(&config) {
|
||||
Ok(context) => {
|
||||
log::debug!("found introducers: {:?}", context.introducers);
|
||||
cx.data = Some(context).take();
|
||||
Status::Continue
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
Status::Tempfail
|
||||
}
|
||||
}
|
||||
|
||||
Status::Continue
|
||||
}
|
||||
|
||||
async fn handle_rcpt(_cx: &mut Context<HuskContext>, args: Vec<CString>) -> Status {
|
||||
async fn handle_mail(cx: &mut Context<HuskContext<'_>>, args: Vec<CString>) -> Status {
|
||||
log::debug!("MAIL: {args:?}");
|
||||
|
||||
let mut sender: Option<String> = None;
|
||||
|
||||
for arg in args {
|
||||
if let Some(s) = mail::to_email(&arg) {
|
||||
sender = Some(s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if sender.is_none() { return Status::Tempfail; }
|
||||
|
||||
if let Some(ref mut context) = cx.data {
|
||||
context.reset_mail();
|
||||
context.set_sender(sender.unwrap());
|
||||
Status::Continue
|
||||
} else {
|
||||
Status::Tempfail
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_rcpt(cx: &mut Context<HuskContext<'_>>, args: Vec<CString>) -> Status {
|
||||
log::debug!("RCPT: {args:?}");
|
||||
|
||||
Status::Continue
|
||||
if let Some(ref mut context) = cx.data {
|
||||
|
||||
let mut rcpt: Option<String> = None;
|
||||
for arg in args {
|
||||
if let Some(s) = mail::to_email(&arg) {
|
||||
rcpt = Some(s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if rcpt.is_none() {
|
||||
Status::Tempfail
|
||||
} else {
|
||||
context.add_recipient(rcpt.unwrap()).unwrap();
|
||||
Status::Continue
|
||||
}
|
||||
|
||||
} else {
|
||||
Status::Tempfail
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_data(_cx: &mut Context<HuskContext>) -> Status {
|
||||
async fn handle_data(cx: &mut Context<HuskContext<'_>>) -> Status {
|
||||
log::debug!("DATA");
|
||||
|
||||
Status::Continue
|
||||
if let Some(ref mut _context) = cx.data {
|
||||
// XXX: check for early accepting
|
||||
// if there are no recipients with authenticated certificates
|
||||
// tell the MTA that this milter is done and the mail can be further
|
||||
// processed.
|
||||
Status::Continue
|
||||
} else {
|
||||
Status::Tempfail
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_header(_cx: &mut Context<HuskContext>, name: CString, value: CString) -> Status {
|
||||
async fn handle_header(_cx: &mut Context<HuskContext<'_>>, name: CString, value: CString) -> Status {
|
||||
log::debug!("HEADER: {name:?} = {value:?}");
|
||||
|
||||
Status::Continue
|
||||
}
|
||||
|
||||
async fn handle_eoh(_cx: &mut Context<HuskContext>) -> Status {
|
||||
async fn handle_eoh(_cx: &mut Context<HuskContext<'_>>) -> Status {
|
||||
log::debug!("EOH");
|
||||
|
||||
Status::Continue
|
||||
}
|
||||
|
||||
async fn handle_body(_cx: &mut Context<HuskContext>, chunk: Bytes) -> Status {
|
||||
async fn handle_body(cx: &mut Context<HuskContext<'_>>, chunk: Bytes) -> Status {
|
||||
log::debug!("BODY: chunk with {:?} bytes received", &chunk.len());
|
||||
|
||||
Status::Continue
|
||||
if let Some(ref mut context) = cx.data {
|
||||
context.mail.add_chunk(chunk);
|
||||
Status::Continue
|
||||
} else {
|
||||
Status::Tempfail
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_eom(cx: &mut EomContext<HuskContext>) -> Status {
|
||||
async fn handle_eom(cx: &mut EomContext<HuskContext<'_>>) -> Status {
|
||||
log::debug!("EOM");
|
||||
|
||||
if let Some(ref mut context_data) = cx.data {
|
||||
log::debug!("Mail from {:?} complete", context_data.sender);
|
||||
if let Some(ref mut context) = cx.data {
|
||||
log::debug!("Mail from {:?} complete", context.mail.sender);
|
||||
log::debug!("recipients: {:?}", context.mail.recipients);
|
||||
log::debug!("body size: {}", context.mail.body_size);
|
||||
|
||||
let body = context.mail.collect_body();
|
||||
|
||||
// check if mail is already encrypted. If it is, keep it as such.
|
||||
if crypto::is_encrypted(&body) {
|
||||
log::info!("encrypted mail detected");
|
||||
return Status::Accept;
|
||||
}
|
||||
|
||||
|
||||
// possible protection
|
||||
// none -> accept
|
||||
// partial -> accept (later: split)
|
||||
// full -> encrypt
|
||||
|
||||
let protection = ProtectionPossibility::from(&context.mail);
|
||||
|
||||
if protection == ProtectionPossibility::Full {
|
||||
// if an error occures the body is not exchanged
|
||||
match crypto::encrypt(&context, &body, &context.mail.recipients) {
|
||||
Ok(encrypted) => {
|
||||
if cx.actions.replace_body(&encrypted).await.is_err() {
|
||||
log::error!("Cannot exchange body");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Status::Continue
|
||||
}
|
||||
|
||||
async fn handle_abort(_cx: &mut Context<HuskContext>) -> Status {
|
||||
async fn handle_abort(_cx: &mut Context<HuskContext<'_>>) -> Status {
|
||||
log::debug!("ABORT");
|
||||
|
||||
Status::Continue
|
||||
}
|
||||
|
||||
async fn handle_close(_cx: &mut Context<HuskContext>) -> Status {
|
||||
async fn handle_close(_cx: &mut Context<HuskContext<'_>>) -> Status {
|
||||
log::debug!("CLOSE");
|
||||
|
||||
Status::Continue
|
||||
}
|
||||
|
||||
async fn handle_unknown(_cx: &mut Context<HuskContext>, arg: CString) -> Status {
|
||||
async fn handle_unknown(_cx: &mut Context<HuskContext<'_>>, arg: CString) -> Status {
|
||||
log::debug!("UNKNOWN: {arg:?}");
|
||||
|
||||
Status::Continue
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Husk milter
|
||||
//
|
||||
// mail functions
|
||||
//
|
||||
|
||||
use std::ffi::CString;
|
||||
use sequoia_openpgp::packet::UserID;
|
||||
|
||||
pub fn to_userid(rcpt: &CString) -> Option<UserID> {
|
||||
match rcpt.to_str() {
|
||||
Ok(s) => Some(UserID::from(s)),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_email(rcpt: &CString) -> Option<String> {
|
||||
if let Some(u) = to_userid(rcpt) {
|
||||
if let Ok(Some(e)) = u.email() { Some(e.to_string()) }
|
||||
else { None }
|
||||
} else { None }
|
||||
}
|
||||
+15
-14
@@ -2,15 +2,14 @@
|
||||
// Husk milter
|
||||
//
|
||||
|
||||
use std::{
|
||||
env,
|
||||
process,
|
||||
};
|
||||
use std::{env, process};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub mod config;
|
||||
use config::HuskConfig;
|
||||
use config::{HuskConfig, HuskConfigContainer};
|
||||
pub mod types;
|
||||
|
||||
pub mod crypto;
|
||||
pub mod mail;
|
||||
pub mod daemon;
|
||||
use daemon::Daemon;
|
||||
|
||||
@@ -18,19 +17,21 @@ use daemon::Daemon;
|
||||
async fn main() {
|
||||
|
||||
let config_file = env::args().nth(1).expect("config file missing");
|
||||
let husk_config = HuskConfig::load(&config_file);
|
||||
|
||||
if husk_config.is_none() {
|
||||
eprintln!("Cannot load configuration");
|
||||
process::exit(1);
|
||||
}
|
||||
let husk_config = husk_config.unwrap();
|
||||
let husk_config = match HuskConfig::load(&config_file) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("{:?}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
log4rs::init_file(husk_config.logfile_config(), Default::default()).unwrap();
|
||||
|
||||
log::info!("starting...");
|
||||
|
||||
match Daemon::run(&husk_config).await {
|
||||
let config_container: HuskConfigContainer = Arc::new(Mutex::new(husk_config));
|
||||
|
||||
match Daemon::run(config_container).await {
|
||||
Ok(_) => {
|
||||
println!("exiting...");
|
||||
},
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Husk milter
|
||||
//
|
||||
// Error typ
|
||||
//
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum HuskError {
|
||||
#[error("Cannot open config file: {0}")]
|
||||
ConfigFileOpenError(String),
|
||||
#[error("Error while parsing config file: {0}")]
|
||||
ConfigFileParseError(String),
|
||||
#[error("Cannot find local trust root")]
|
||||
NoLocalTrustRoot,
|
||||
#[error("Cannot access cert store")]
|
||||
NoCertStore,
|
||||
#[error("No encryption keys found")]
|
||||
NoEncryptionKeys,
|
||||
}
|
||||
@@ -4,21 +4,62 @@
|
||||
// Context
|
||||
//
|
||||
|
||||
pub struct HuskContext {
|
||||
pub sender: Option<String>,
|
||||
use anyhow;
|
||||
use sequoia_openpgp::policy::StandardPolicy;
|
||||
use std::path::PathBuf;
|
||||
use sequoia_openpgp::Cert;
|
||||
use sequoia_cert_store::{CertStore, StoreUpdate};
|
||||
use sequoia_directories::Home;
|
||||
|
||||
use crate::{config::HuskConfig, crypto};
|
||||
use crate::types::mail_context::MailContext;
|
||||
|
||||
use crate::types::introducer::Introducer;
|
||||
|
||||
pub struct HuskContext<'hc> {
|
||||
pub policy: StandardPolicy<'hc>,
|
||||
pub cert_store: CertStore<'hc>,
|
||||
pub local_trust_root: Cert,
|
||||
pub introducers: Vec<Introducer>,
|
||||
pub mail: MailContext<'hc>,
|
||||
}
|
||||
|
||||
impl HuskContext {
|
||||
|
||||
impl<'hc> HuskContext<'hc> {
|
||||
|
||||
/// Create a new HuskContext instance
|
||||
pub fn new() -> Option<HuskContext> {
|
||||
Some(HuskContext {
|
||||
sender: None
|
||||
pub fn new(config: &HuskConfig) -> anyhow::Result<HuskContext<'hc>> {
|
||||
|
||||
let sequoia_home = Home::new(PathBuf::from(&config.sequoia_home))?;
|
||||
let cert_store_base = sequoia_home.data_dir(sequoia_directories::Component::CertD);
|
||||
let cert_store = CertStore::open(cert_store_base)?;
|
||||
|
||||
let policy = StandardPolicy::new();
|
||||
|
||||
let local_trust_root = crypto::get_local_trust_root(&cert_store)?;
|
||||
let introducers = crypto::get_introducers(&cert_store, &policy, local_trust_root.fingerprint());
|
||||
|
||||
Ok(HuskContext {
|
||||
policy,
|
||||
cert_store,
|
||||
local_trust_root,
|
||||
introducers,
|
||||
mail: MailContext::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_sender(&mut self, sender: String) {
|
||||
self.sender = Some(sender);
|
||||
self.mail.set_sender(sender);
|
||||
}
|
||||
|
||||
pub fn reset_mail(&mut self) {
|
||||
self.mail = MailContext::new();
|
||||
}
|
||||
|
||||
pub fn add_recipient(&mut self, rcpt: String) -> anyhow::Result<()> {
|
||||
|
||||
let certs = crypto::get_certificates(&self, rcpt.as_str());
|
||||
|
||||
self.mail.add_recipient(rcpt, certs)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// husk-milter
|
||||
//
|
||||
// introducers
|
||||
//
|
||||
|
||||
use sequoia_wot as wot;
|
||||
use wot::{CertSynopsis, Certification};
|
||||
use sequoia_openpgp::{policy::StandardPolicy, Cert, KeyID};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Introducer {
|
||||
pub cert: CertSynopsis,
|
||||
pub certification: Certification,
|
||||
}
|
||||
|
||||
impl Introducer {
|
||||
|
||||
pub fn can_introduce(&self, email: &str) -> bool {
|
||||
if let Some(regexset) = self.certification.regular_expressions() {
|
||||
if regexset.matches_everything() {
|
||||
// return false, because unlimited certifications are
|
||||
// ignored.
|
||||
false
|
||||
} else {
|
||||
regexset.is_match(email)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// Husk milter
|
||||
//
|
||||
// Mail Context
|
||||
//
|
||||
|
||||
use std::sync::Arc;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use sequoia_cert_store::LazyCert;
|
||||
|
||||
use crate::types::recipient::Recipient;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum ProtectionPossibility {
|
||||
// no recipient has a cert
|
||||
NoProtection,
|
||||
// some recipients have a cert (but not all)
|
||||
Partial,
|
||||
// all recipients have a cert
|
||||
Full
|
||||
}
|
||||
|
||||
|
||||
pub struct MailContext<'mc> {
|
||||
pub sender: Option<String>,
|
||||
pub recipients: Vec<Recipient<'mc>>,
|
||||
// header
|
||||
body: Vec<Bytes>,
|
||||
pub body_size: usize,
|
||||
}
|
||||
|
||||
impl<'mc> MailContext<'mc> {
|
||||
|
||||
pub fn new() -> Self {
|
||||
MailContext {
|
||||
sender: None,
|
||||
recipients: Vec::new(),
|
||||
body: Vec::new(),
|
||||
body_size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_sender(&mut self, sender: String) {
|
||||
self.sender = Some(sender);
|
||||
}
|
||||
|
||||
pub fn add_recipient(&mut self, rcpt: String, certs: Vec<Arc<LazyCert<'mc>>>) -> anyhow::Result<()> {
|
||||
let mut recipient: Recipient = rcpt.into();
|
||||
recipient.certs = certs;
|
||||
|
||||
self.recipients.push(recipient);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_chunk(&mut self, chunk: Bytes) {
|
||||
self.body_size += chunk.len();
|
||||
self.body.push(chunk);
|
||||
}
|
||||
|
||||
pub fn collect_body(&self) -> Bytes {
|
||||
let mut buffer = BytesMut::with_capacity(self.body_size);
|
||||
for chunk in &self.body {
|
||||
buffer.extend_from_slice(&chunk[..]);
|
||||
}
|
||||
buffer.into()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl From<&MailContext<'_>> for ProtectionPossibility {
|
||||
fn from(mail_context: &MailContext<'_>) -> ProtectionPossibility {
|
||||
let mut can_encrypt = false;
|
||||
let mut cannot_encrypt = false;
|
||||
|
||||
for rcpt in mail_context.recipients.iter() {
|
||||
if rcpt.can_encrypt() {
|
||||
can_encrypt = true;
|
||||
} else {
|
||||
cannot_encrypt = true;
|
||||
}
|
||||
}
|
||||
|
||||
match (can_encrypt, cannot_encrypt) {
|
||||
(true, false) => ProtectionPossibility::Full,
|
||||
(true, true) => ProtectionPossibility::Partial,
|
||||
(false, true) => ProtectionPossibility::NoProtection,
|
||||
// (false, false) occurs if there are no recipients
|
||||
(false, false) => ProtectionPossibility::NoProtection,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
pub mod husk_context;
|
||||
pub mod mail_context;
|
||||
pub mod errors;
|
||||
pub mod recipient;
|
||||
pub mod introducer;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Husk milter
|
||||
//
|
||||
// Recipient
|
||||
//
|
||||
|
||||
use anyhow;
|
||||
use std::sync::Arc;
|
||||
use std::ffi::CString;
|
||||
use sequoia_cert_store::LazyCert;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Recipient<'r> {
|
||||
pub email: String,
|
||||
pub certs: Vec<Arc<LazyCert<'r>>>
|
||||
}
|
||||
|
||||
impl From<String> for Recipient<'_> {
|
||||
fn from(email: String) -> Self {
|
||||
Recipient {
|
||||
email: email,
|
||||
certs: Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CString> for Recipient<'_> {
|
||||
|
||||
type Error=anyhow::Error;
|
||||
|
||||
fn try_from(value: CString) -> Result<Self, Self::Error> {
|
||||
match value.into_string() {
|
||||
Ok(s) => Ok(s.into()),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Recipient<'_> {
|
||||
/// check if this Recipient can encrypt.
|
||||
// XXX: currently this check is naive
|
||||
pub fn can_encrypt(&self) -> bool {
|
||||
if self.certs.len() == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// XXX: add check to see if a cert has encryption capable subkeys
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user