From 56647806b7387deeaa5d0ece164734ac617c8e33 Mon Sep 17 00:00:00 2001 From: Malte Meiboom Date: Tue, 9 Dec 2025 12:11:07 +0100 Subject: [PATCH] Steps towards encryption - Added certstore and retrieval of the local trust root. - Major work on keeping the context between calls to the milter. - Added a context for an email in processing - Added Husk specific errors --- Cargo.toml | 4 +++ config/config.toml | 2 ++ src/config.rs | 14 ++++++++- src/crypto.rs | 47 ++++++++++++++++++++++++++++ src/daemon/mod.rs | 66 +++++++++++++++++++++++++-------------- src/main.rs | 14 ++++----- src/types/errors.rs | 15 +++++++++ src/types/husk_context.rs | 36 ++++++++++++++++----- src/types/mail_context.rs | 37 ++++++++++++++++++++++ src/types/mod.rs | 2 ++ 10 files changed, 197 insertions(+), 40 deletions(-) create mode 100644 src/crypto.rs create mode 100644 src/types/errors.rs create mode 100644 src/types/mail_context.rs diff --git a/Cargo.toml b/Cargo.toml index 2ac0421..7bf02a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,11 @@ 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" 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..987db39 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,11 +3,13 @@ // use std::fs; +use std::sync::{Arc, Mutex}; use serde_derive::Deserialize; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct HuskConfig { pub connection: String, + pub sequoia_home: String, logfile_config: Option, } @@ -41,3 +43,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..ef55fb6 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,47 @@ +// +// Husk milter +// +// cryptographic functions +// + +use anyhow; +use sequoia_openpgp::parse::Parse; +use sequoia_openpgp::{Fingerprint, Cert}; +use sequoia_openpgp::cert::raw::RawCertParser; +use sequoia_cert_store::CertStore; +use crate::types::errors::HuskError; + +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()) + } + +} diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index b0f48bd..63d398d 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -25,13 +25,15 @@ use indymilter::{ }; use crate::types::husk_context::HuskContext; -use crate::config::HuskConfig; +use crate::config::{HuskConfig, HuskConfigContainer}; 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 +42,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 +64,7 @@ impl Daemon { } async fn handle_negotiate( - _cx: &mut NegotiateContext, + _cx: &mut NegotiateContext>, actions: Actions, opts: ProtoOpts, ) -> Status { @@ -72,7 +74,7 @@ impl Daemon { } async fn handle_connect( - _cx: &mut Context, + _cx: &mut Context>, hostname: CString, socket_info: SocketInfo, ) -> Status { @@ -82,15 +84,28 @@ 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 + let config: HuskConfig = config_container.into(); + + // setup Context + match HuskContext::new(&config) { + Ok(context) => { + cx.data = Some(context).take(); + Status::Continue + }, + Err(e) => { + log::error!("{}", e); + Status::Tempfail + } + } } - async fn handle_mail(cx: &mut Context, args: Vec) -> Status { + async fn handle_mail(cx: &mut Context>, args: Vec) -> Status { log::debug!("MAIL: {args:?}"); + // XXX let sender = match args.first() { Some(cs) => { match cs.to_str() { @@ -101,68 +116,71 @@ impl Daemon { None => { "missing".to_string() } }; - // setup Context - if let Some(mut context) = HuskContext::new() { + if let Some(ref mut context) = cx.data { context.set_sender(sender); - cx.data = Some(context).take(); + Status::Continue + } else { + Status::Tempfail } - - Status::Continue } - async fn handle_rcpt(_cx: &mut Context, args: Vec) -> Status { + async fn handle_rcpt(cx: &mut Context>, args: Vec) -> Status { log::debug!("RCPT: {args:?}"); + if let Some(ref mut context) = cx.data { + + } + Status::Continue } - async fn handle_data(_cx: &mut Context) -> Status { + async fn handle_data(_cx: &mut Context>) -> Status { log::debug!("DATA"); Status::Continue } - 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 } - 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); } 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/main.rs b/src/main.rs index cf0bfc4..5d156a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,15 +2,13 @@ // 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 daemon; use daemon::Daemon; @@ -30,7 +28,9 @@ async fn main() { 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..6951951 --- /dev/null +++ b/src/types/errors.rs @@ -0,0 +1,15 @@ +// +// Husk milter +// +// Error typ +// + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum HuskError { + #[error("Cannot find local trust root")] + NoLocalTrustRoot, + #[error("Cannot access cert store")] + NoCertStore +} diff --git a/src/types/husk_context.rs b/src/types/husk_context.rs index 542d1f0..5cea065 100644 --- a/src/types/husk_context.rs +++ b/src/types/husk_context.rs @@ -4,21 +4,41 @@ // Context // -pub struct HuskContext { - pub sender: Option, +use anyhow; +use std::path::PathBuf; +use sequoia_openpgp::Cert; +use sequoia_cert_store::CertStore; +use sequoia_directories::Home; + +use crate::{config::HuskConfig, crypto}; +use crate::types::mail_context::MailContext; + +pub struct HuskContext<'hc> { + pub cert_store: CertStore<'hc>, + pub local_trust_root: Cert, + 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 local_trust_root = crypto::get_local_trust_root(&cert_store)?; + + Ok(HuskContext { + cert_store, + local_trust_root, + mail: MailContext::new(), }) } pub fn set_sender(&mut self, sender: String) { - self.sender = Some(sender); + self.mail.set_sender(sender); } } diff --git a/src/types/mail_context.rs b/src/types/mail_context.rs new file mode 100644 index 0000000..498cf6a --- /dev/null +++ b/src/types/mail_context.rs @@ -0,0 +1,37 @@ +// +// Husk milter +// +// Mail Context +// + +use std::ffi::CString; + +use sequoia_openpgp::cert::ValidCert; + +pub struct Recipient<'r> { + pub email: CString, + pub cert: Option>, +} + +pub struct MailContext<'c> { + pub sender: Option, + pub recipients: Vec>, + // header + // body + // body size +} + +impl MailContext<'_> { + + pub fn new() -> Self { + MailContext { + sender: None, + recipients: Vec::new(), + } + } + + pub fn set_sender(&mut self, sender: String) { + self.sender = Some(sender); + } + +} diff --git a/src/types/mod.rs b/src/types/mod.rs index d827538..01c14ea 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1 +1,3 @@ pub mod husk_context; +pub mod mail_context; +pub mod errors;