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
This commit is contained in:
Malte Meiboom
2025-12-09 12:11:07 +01:00
parent 0f3a05e811
commit 56647806b7
10 changed files with 197 additions and 40 deletions
+4
View File
@@ -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"
+2
View File
@@ -3,4 +3,6 @@
#
connection = "localhost:3000"
sequoia_home = "/tmp/sq_home"
logfile_config = "./config/log4rs.yml"
+13 -1
View File
@@ -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<String>,
}
@@ -41,3 +43,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()
}
}
+47
View File
@@ -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<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())
}
}
+42 -24
View File
@@ -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<HuskContext>,
_cx: &mut NegotiateContext<HuskContext<'_>>,
actions: Actions,
opts: ProtoOpts,
) -> Status {
@@ -72,7 +74,7 @@ impl Daemon {
}
async fn handle_connect(
_cx: &mut Context<HuskContext>,
_cx: &mut Context<HuskContext<'_>>,
hostname: CString,
socket_info: SocketInfo,
) -> Status {
@@ -82,15 +84,28 @@ 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
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<HuskContext>, args: Vec<CString>) -> Status {
async fn handle_mail(cx: &mut Context<HuskContext<'_>>, args: Vec<CString>) -> 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<HuskContext>, args: Vec<CString>) -> Status {
async fn handle_rcpt(cx: &mut Context<HuskContext<'_>>, args: Vec<CString>) -> Status {
log::debug!("RCPT: {args:?}");
if let Some(ref mut context) = cx.data {
}
Status::Continue
}
async fn handle_data(_cx: &mut Context<HuskContext>) -> Status {
async fn handle_data(_cx: &mut Context<HuskContext<'_>>) -> Status {
log::debug!("DATA");
Status::Continue
}
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
}
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);
}
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
+7 -7
View File
@@ -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...");
},
+15
View File
@@ -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
}
+28 -8
View File
@@ -4,21 +4,41 @@
// Context
//
pub struct HuskContext {
pub sender: Option<String>,
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<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 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);
}
}
+37
View File
@@ -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<ValidCert<'r>>,
}
pub struct MailContext<'c> {
pub sender: Option<String>,
pub recipients: Vec<Recipient<'c>>,
// 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);
}
}
+2
View File
@@ -1 +1,3 @@
pub mod husk_context;
pub mod mail_context;
pub mod errors;