diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8b5b3b7..09d43fe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 + diff --git a/Cargo.toml b/Cargo.toml index 2ac0421..41d11bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/config/config.toml b/config/config.toml index cea6249..9246d5d 100644 --- a/config/config.toml +++ b/config/config.toml @@ -3,4 +3,6 @@ # connection = "localhost:3000" +sequoia_home = "/tmp/sq_home" + logfile_config = "./config/log4rs.yml" diff --git a/src/config.rs b/src/config.rs index be24d9c..13741d0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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, } impl HuskConfig { - pub fn load(path: &String) -> Option { + pub fn load(path: &String) -> anyhow::Result { 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>; + +impl From for HuskConfig { + fn from(container: HuskConfigContainer) -> Self { + let config = container.lock().unwrap(); + config.clone() + } +} + diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..55e1cac --- /dev/null +++ b/src/crypto.rs @@ -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 { + + 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>> { + + 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 { + + 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) + -> anyhow::Result { + + 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)); + + } + +} diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index b0f48bd..eb82b9e 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -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, + _cx: &mut NegotiateContext>, actions: Actions, opts: ProtoOpts, ) -> Status { @@ -72,7 +77,7 @@ impl Daemon { } async fn handle_connect( - _cx: &mut Context, + _cx: &mut Context>, hostname: CString, socket_info: SocketInfo, ) -> Status { @@ -82,87 +87,163 @@ impl Daemon { Status::Continue } - async fn handle_helo(_cx: &mut Context, hostname: CString) -> Status { + async fn handle_helo(cx: &mut Context>, config_container: HuskConfigContainer, hostname: CString) -> Status { log::debug!("HELO: hostname: {hostname:?}"); - Status::Continue - } - - async fn handle_mail(cx: &mut Context, args: Vec) -> 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, args: Vec) -> Status { + async fn handle_mail(cx: &mut Context>, args: Vec) -> Status { + log::debug!("MAIL: {args:?}"); + + let mut sender: Option = 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>, args: Vec) -> Status { log::debug!("RCPT: {args:?}"); - Status::Continue + if let Some(ref mut context) = cx.data { + + let mut rcpt: Option = 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) -> Status { + async fn handle_data(cx: &mut Context>) -> 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, name: CString, value: CString) -> Status { + async fn handle_header(_cx: &mut Context>, name: CString, value: CString) -> Status { log::debug!("HEADER: {name:?} = {value:?}"); Status::Continue } - async fn handle_eoh(_cx: &mut Context) -> Status { + async fn handle_eoh(_cx: &mut Context>) -> Status { log::debug!("EOH"); Status::Continue } - async fn handle_body(_cx: &mut Context, chunk: Bytes) -> Status { + async fn handle_body(cx: &mut Context>, 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) -> Status { + async fn handle_eom(cx: &mut EomContext>) -> 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) -> Status { + async fn handle_abort(_cx: &mut Context>) -> Status { log::debug!("ABORT"); Status::Continue } - async fn handle_close(_cx: &mut Context) -> Status { + async fn handle_close(_cx: &mut Context>) -> Status { log::debug!("CLOSE"); Status::Continue } - async fn handle_unknown(_cx: &mut Context, arg: CString) -> Status { + async fn handle_unknown(_cx: &mut Context>, arg: CString) -> Status { log::debug!("UNKNOWN: {arg:?}"); Status::Continue diff --git a/src/mail.rs b/src/mail.rs new file mode 100644 index 0000000..96ba6c7 --- /dev/null +++ b/src/mail.rs @@ -0,0 +1,22 @@ +// +// Husk milter +// +// mail functions +// + +use std::ffi::CString; +use sequoia_openpgp::packet::UserID; + +pub fn to_userid(rcpt: &CString) -> Option { + match rcpt.to_str() { + Ok(s) => Some(UserID::from(s)), + _ => None + } +} + +pub fn to_email(rcpt: &CString) -> Option { + if let Some(u) = to_userid(rcpt) { + if let Ok(Some(e)) = u.email() { Some(e.to_string()) } + else { None } + } else { None } +} diff --git a/src/main.rs b/src/main.rs index cf0bfc4..4c0c09e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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..."); }, diff --git a/src/types/errors.rs b/src/types/errors.rs new file mode 100644 index 0000000..fce16a4 --- /dev/null +++ b/src/types/errors.rs @@ -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, +} diff --git a/src/types/husk_context.rs b/src/types/husk_context.rs index 542d1f0..5e6e7e6 100644 --- a/src/types/husk_context.rs +++ b/src/types/husk_context.rs @@ -4,21 +4,62 @@ // Context // -pub struct HuskContext { - pub sender: Option, +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, + pub mail: MailContext<'hc>, } -impl HuskContext { - +impl<'hc> HuskContext<'hc> { + /// Create a new HuskContext instance - pub fn new() -> Option { - Some(HuskContext { - sender: None + pub fn new(config: &HuskConfig) -> anyhow::Result> { + + 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) } } diff --git a/src/types/introducer.rs b/src/types/introducer.rs new file mode 100644 index 0000000..f194199 --- /dev/null +++ b/src/types/introducer.rs @@ -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 + } + } +} + diff --git a/src/types/mail_context.rs b/src/types/mail_context.rs new file mode 100644 index 0000000..d4ecf9a --- /dev/null +++ b/src/types/mail_context.rs @@ -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, + pub recipients: Vec>, + // header + body: Vec, + 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>>) -> 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, + } + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index d827538..16cda95 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1 +1,5 @@ pub mod husk_context; +pub mod mail_context; +pub mod errors; +pub mod recipient; +pub mod introducer; diff --git a/src/types/recipient.rs b/src/types/recipient.rs new file mode 100644 index 0000000..4288b27 --- /dev/null +++ b/src/types/recipient.rs @@ -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>> +} + +impl From for Recipient<'_> { + fn from(email: String) -> Self { + Recipient { + email: email, + certs: Vec::new() + } + } +} + +impl TryFrom for Recipient<'_> { + + type Error=anyhow::Error; + + fn try_from(value: CString) -> Result { + 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 + } +} + +