From 04322f9cd94f44e7cef783d8df31d7c54aa3157e Mon Sep 17 00:00:00 2001 From: Malte Meiboom Date: Mon, 10 Aug 2026 15:12:50 +0200 Subject: [PATCH] Tor support - Add support for using Tor via a socks5 proxy. - Tor usage of the milter and the updater process are configured separately. - Restructure the config file and add some documentation. --- Cargo.lock | 1 + Cargo.toml | 2 + config/config.toml | 47 ++++++++++++++++++- src/common/crypto.rs | 36 +++++++++++--- src/config.rs | 99 +++++++++++++++++++++++++++++++-------- src/daemon/mod.rs | 3 +- src/types/defaults.rs | 3 ++ src/types/husk_context.rs | 12 ++++- src/updater/mod.rs | 7 +-- 9 files changed, 177 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea2903a..67f107a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1292,6 +1292,7 @@ dependencies = [ "log", "log4rs", "rand 0.10.2", + "reqwest", "sequoia-cert-store", "sequoia-directories", "sequoia-keystore", diff --git a/Cargo.toml b/Cargo.toml index 8082def..c181d71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ lettre = "0.11.22" log = "0.4.30" log4rs = "1.3.0" rand = "0.10.2" +# the reqwest version has to be the same sequoia-net reexports! +reqwest = { version = "0.12", features = ["socks"] } sequoia-cert-store = "0.7.3" sequoia-directories = "0.1.0" sequoia-keystore = "0.7.2" diff --git a/config/config.toml b/config/config.toml index 5cab41a..07c39c1 100644 --- a/config/config.toml +++ b/config/config.toml @@ -1,17 +1,60 @@ # # Husk config file # -connection = "localhost:3000" +# The directory for anything Sequoia PGP related (keystore, certstore). +# +# default: /var/lib/husk sequoia_home = "/tmp/sq_home" -#keyservers = [ "hkps://keys.openpgp.org/", "hkps://mail-api.proton.me", "hkps://keys.mailvelope.com", "hkps://keyserver.ubuntu.com", "hkps://sks.pod01.fleetstreetops.com" ] +[milter] +# IP and port the milter will listen on. +# +# no default, must be specified. +connection = "localhost:3000" + +# Keyservers to use for key lookup. +# +# Example: +# keyservers = [ "hkps://keys.openpgp.org/", "hkps://mail-api.proton.me", "hkps://keys.mailvelope.com", "hkps://keyserver.ubuntu.com", "hkps://sks.pod01.fleetstreetops.com" ] +# +# The default is the empty list, which prevents keyserver lookups. keyservers = [ "hkp://example.com" ] +# Location of the pid file of the milter. +# +# default: /var/lib/husk/husk.pid + +# Subject header lines are protected (exchanged). +# +# Specify your replacement string: +# subject_replacement = "Encrypted message" +# +# default: "..." + +# Tor usage: If configured, the milter will use Tor via the specified +# sock5 proxy. +# +# use_tor_via = "socks5://localhost:9050/" +# +# default: unconfigured, meaning: no usage of Tor. + [updater] + +# The interval between two update requests for a certificate are +# random. +# +# wait_min and wait_max specify the range for that randomness. +# defaults: +# min_wait = 5 +# max_wait = 3600 wait_min = 1 wait_max = 2 +# Tor usage of the updater process. Semantics (and default) are the same +# as with the milter. +use_tor_via = "socks5://localhost:9050/" + [logging.appenders.console] kind = "console" [logging.appenders.console.encoder] diff --git a/src/common/crypto.rs b/src/common/crypto.rs index ef40920..2cd5315 100644 --- a/src/common/crypto.rs +++ b/src/common/crypto.rs @@ -30,6 +30,7 @@ use sequoia_wot::{self as wot, CertSynopsis}; use wot::store::Store as _; use wot::{Depth, Path}; +use crate::config::TorConnectorType; use crate::types::errors::HuskError; use crate::types::husk_context::HuskContext; use crate::types::introducer::Introducer; @@ -125,11 +126,23 @@ async fn query_online_sources<'hc>(context: &HuskContext<'hc>, query: OnlineQuer log::debug!("remote lookup for {:?}", query); let mut jobs = JoinSet::new(); - let http_client = sequoia_net::reqwest::Client::builder() + let mut http_client_builder = sequoia_net::reqwest::Client::builder() .user_agent(USER_AGENT) .connect_timeout(CONNECT_TIMEOUT) - .timeout(REQUEST_TIMEOUT) - .build()?; + .timeout(REQUEST_TIMEOUT); + + if let TorConnectorType::SocksProxy(socks_url) = &context.use_tor_via { + // Configure the use of a tor proxy. + let mut nonce: [u8; 4] = [0; 4]; + sequoia_openpgp::crypto::random(&mut nonce[..])?; + let nonce: String = sequoia_openpgp::fmt::hex::encode(&nonce); + let url: String = format!("socks5h://anonymous:{}@{}", nonce, socks_url); + + http_client_builder = http_client_builder + .proxy(sequoia_net::reqwest::Proxy::all(url)?); + } + + let http_client = http_client_builder.build()?; // local function to check certificates for usability let collect_certs = |certs: Vec>, query: OnlineQuery, policy: &StandardPolicy| { @@ -152,8 +165,8 @@ async fn query_online_sources<'hc>(context: &HuskContext<'hc>, query: OnlineQuer } } }, - OnlineQuery::Fingerprint(_) => { - found = true; + OnlineQuery::Fingerprint(f) => { + found = cert.fingerprint() == f; } } @@ -193,9 +206,19 @@ async fn query_online_sources<'hc>(context: &HuskContext<'hc>, query: OnlineQuer let q = query.clone(); match query { OnlineQuery::Email(e) => { + match ks.search(e.as_str()).await { + Ok(certs) => { + partial_result = collect_certs(certs, q, &p); + } + Err(e) => { + log::debug!("Error: {}", e.to_string()); + } + } + /* if let Ok(certs) = ks.search(e.as_str()).await { partial_result = collect_certs(certs, q, &p); } + */ partial_result }, OnlineQuery::Fingerprint(f) => { @@ -934,7 +957,8 @@ jk6lh01Ej9QTGQ== introducers: Vec::new(), keyservers: Vec::new(), subject_replacement: "...".into(), - mail: MailContext::new() + mail: MailContext::new(), + use_tor_via: crate::config::TorConnectorType::NotUsed, }; // store the local trust root diff --git a/src/config.rs b/src/config.rs index 08b3615..47ea2d9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,6 +9,7 @@ use toml; use log4rs; use crate::types::errors::HuskError; +use crate::types::defaults::CONNECTION; use crate::types::defaults::CONSOLE_LOGGING; use crate::types::defaults::UPDATER_PID_FILE_LOCATION; use crate::types::defaults::UPDATER_STATE_FILE_LOCATION; @@ -17,21 +18,55 @@ use crate::types::defaults::UPDATER_WAIT_MAX; use crate::types::defaults::PID_FILE_LOCATION; use crate::types::defaults::SUBJECT_REPLACEMENT; +#[derive(Deserialize, Debug, Clone)] +pub enum TorConnectorType { + NotUsed, + Errornous, + SocksProxy(String), + Arti, +} + +impl From> for TorConnectorType { + fn from(value: Option) -> Self { + match value { + Some(v) => { + if v.to_ascii_lowercase() == "arti" { + TorConnectorType::Arti + } else { + if v.starts_with("socks5://") { + TorConnectorType::SocksProxy(v[9..].to_string()) + } else { + TorConnectorType::Errornous + } + } + }, + None => TorConnectorType::NotUsed, + } + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct MilterConfig { + pub connection: String, + pub pid_file: Option, + pub keyservers: Option>, + pub subject_replacement: Option, + use_tor_via: Option, +} + #[derive(Deserialize, Debug, Clone)] pub struct UpdaterConfig { pub wait_min: Option, pub wait_max: Option, pub pid_file: Option, pub state_file: Option, + use_tor_via: Option, } #[derive(Deserialize, Debug, Clone)] pub struct HuskConfig { - pub connection: String, pub sequoia_home: String, - pub keyservers: Option>, - pub subject_replacement: Option, - pub pid_file: Option, + milter: Option, updater: Option, logging: Option, } @@ -77,28 +112,47 @@ impl HuskConfig { Ok(()) } - pub fn get_keyservers(&self) -> Vec { - if let Some(kss) = &self.keyservers { - kss.clone() + pub fn get_connection(&self) -> String { + if let Some(milter_config) = &self.milter { + milter_config.connection.clone() } else { - Vec::new() + CONNECTION.to_string() } } + pub fn get_milter_tor_usage(&self) -> TorConnectorType { + if let Some(milter_config) = &self.milter { + milter_config.use_tor_via.clone().into() + } else { + TorConnectorType::NotUsed + } + } + + pub fn get_keyservers(&self) -> Vec { + if let Some(milter_config) = &self.milter { + if let Some(kss) = &milter_config.keyservers { + return kss.clone(); + } + } + Vec::new() + } + pub fn get_subject_replacement(&self) -> String { - if let Some(replacement) = &self.subject_replacement { - replacement.clone() - } else { - SUBJECT_REPLACEMENT.to_string() + if let Some(milter_config) = &self.milter { + if let Some(replacement) = &milter_config.subject_replacement { + return replacement.clone(); + } } + SUBJECT_REPLACEMENT.to_string() } pub fn get_pid_file(&self) -> String { - if let Some(pid_file) = &self.pid_file { - pid_file.clone() - } else { - PID_FILE_LOCATION.to_string() + if let Some(milter_config) = &self.milter { + if let Some(pid_file) = &milter_config.pid_file { + return pid_file.clone(); + } } + PID_FILE_LOCATION.to_string() } pub fn get_updater_pid_file(&self) -> String { @@ -147,6 +201,14 @@ impl HuskConfig { UPDATER_WAIT_MAX } } + + pub fn get_updater_tor_usage(&self) -> TorConnectorType { + if let Some(updater_config) = &self.updater { + updater_config.use_tor_via.clone().into() + } else { + TorConnectorType::NotUsed + } + } } pub type HuskConfigContainer = Arc>; @@ -168,12 +230,9 @@ mod tests { #[test] fn subject_line() { let config = HuskConfig { - connection: "".into(), sequoia_home: "".into(), - keyservers: None, - subject_replacement: None, logging: None, - pid_file: None, + milter: None, updater: None, }; diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index fd8bec6..16cad98 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -33,7 +33,7 @@ impl Daemon { let config: HuskConfig = config_container.clone().into(); - let listener = TcpListener::bind(&config.connection) + let listener = TcpListener::bind(&config.get_connection()) .await .expect(" opening milter socket"); @@ -116,6 +116,7 @@ impl Daemon { // setup Context match HuskContext::new(&config) { Ok(mut context) => { + context.for_milter(&config); context.set_sender(sender.unwrap()); cx.data = Some(context); Status::Continue diff --git a/src/types/defaults.rs b/src/types/defaults.rs index 12e0446..1272c36 100644 --- a/src/types/defaults.rs +++ b/src/types/defaults.rs @@ -1,5 +1,8 @@ //! Defaults +/// Default host and port of the milter. +pub const CONNECTION: &str = "localhost:3000"; + pub const CONFIG_FILE_LOCATION: &str = "/etc/husk/config.toml"; /// If encrypting a mail, the subject is moved into the encrypted body diff --git a/src/types/husk_context.rs b/src/types/husk_context.rs index 8e13464..bee28d4 100644 --- a/src/types/husk_context.rs +++ b/src/types/husk_context.rs @@ -17,7 +17,7 @@ use sequoia_cert_store::CertStore; use sequoia_directories::Home; use sequoia_keystore::Key; -use crate::config::HuskConfig; +use crate::config::{HuskConfig, TorConnectorType}; use crate::common::crypto; use crate::types::mail_context::MailContext; @@ -32,6 +32,7 @@ pub struct HuskContext<'hc> { pub keyservers: Vec, pub subject_replacement: String, pub mail: MailContext<'hc>, + pub use_tor_via: TorConnectorType, } impl<'hc> HuskContext<'hc> { @@ -82,6 +83,7 @@ impl<'hc> HuskContext<'hc> { keyservers: config.get_keyservers(), subject_replacement: config.get_subject_replacement(), mail: MailContext::new(), + use_tor_via: TorConnectorType::NotUsed, }) } @@ -109,4 +111,12 @@ impl<'hc> HuskContext<'hc> { self.mail.add_recipient(rcpt, certs) } + pub fn for_milter(&mut self, config: &HuskConfig) { + self.use_tor_via = config.get_milter_tor_usage(); + } + + pub fn for_updater(&mut self, config: &HuskConfig) { + self.use_tor_via = config.get_updater_tor_usage(); + } + } diff --git a/src/updater/mod.rs b/src/updater/mod.rs index db49460..a8164a1 100644 --- a/src/updater/mod.rs +++ b/src/updater/mod.rs @@ -41,15 +41,16 @@ impl Updater { let mut updater_state = UpdaterState::thaw(state_path) .await .map_err(|e| { stay = false; e })?; - let context = &HuskContext::without_signkey(&c)?; - updater_state.refresh(context); + let mut context = HuskContext::without_signkey(&c)?; + context.for_updater(&c); + updater_state.refresh(&context); if let Some(fpr) = updater_state.next() { log::debug!("try to update {}", fpr.to_hex()); let stats = MergePublicCollectStats::new(); - let certs = crypto::lookup_fingerprint(context, fpr.to_hex().as_str()).await?; + let certs = crypto::lookup_fingerprint(&context, fpr.to_hex().as_str()).await?; if !certs.is_empty() { for cert in certs { let cert = Arc::new(LazyCert::from_cert(cert));