merge malte/first_encryption

This commit is contained in:
Malte Meiboom
2026-01-05 13:07:14 +01:00
14 changed files with 737 additions and 79 deletions
+8 -7
View File
@@ -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
+5
View File
@@ -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"
+2
View File
@@ -3,4 +3,6 @@
#
connection = "localhost:3000"
sequoia_home = "/tmp/sq_home"
logfile_config = "./config/log4rs.yml"
+20 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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...");
},
+21
View File
@@ -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,
}
+49 -8
View File
@@ -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)
}
}
+33
View File
@@ -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
}
}
}
+92
View File
@@ -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,
}
}
}
+4
View File
@@ -1 +1,5 @@
pub mod husk_context;
pub mod mail_context;
pub mod errors;
pub mod recipient;
pub mod introducer;
+52
View File
@@ -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
}
}