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.
This commit is contained in:
Malte Meiboom
2026-08-10 15:12:50 +02:00
parent 1ed3e1e988
commit 04322f9cd9
9 changed files with 177 additions and 33 deletions
Generated
+1
View File
@@ -1292,6 +1292,7 @@ dependencies = [
"log", "log",
"log4rs", "log4rs",
"rand 0.10.2", "rand 0.10.2",
"reqwest",
"sequoia-cert-store", "sequoia-cert-store",
"sequoia-directories", "sequoia-directories",
"sequoia-keystore", "sequoia-keystore",
+2
View File
@@ -22,6 +22,8 @@ lettre = "0.11.22"
log = "0.4.30" log = "0.4.30"
log4rs = "1.3.0" log4rs = "1.3.0"
rand = "0.10.2" 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-cert-store = "0.7.3"
sequoia-directories = "0.1.0" sequoia-directories = "0.1.0"
sequoia-keystore = "0.7.2" sequoia-keystore = "0.7.2"
+45 -2
View File
@@ -1,17 +1,60 @@
# #
# Husk config file # Husk config file
# #
connection = "localhost:3000"
# The directory for anything Sequoia PGP related (keystore, certstore).
#
# default: /var/lib/husk
sequoia_home = "/tmp/sq_home" 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" ] 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] [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_min = 1
wait_max = 2 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] [logging.appenders.console]
kind = "console" kind = "console"
[logging.appenders.console.encoder] [logging.appenders.console.encoder]
+30 -6
View File
@@ -30,6 +30,7 @@ use sequoia_wot::{self as wot, CertSynopsis};
use wot::store::Store as _; use wot::store::Store as _;
use wot::{Depth, Path}; use wot::{Depth, Path};
use crate::config::TorConnectorType;
use crate::types::errors::HuskError; use crate::types::errors::HuskError;
use crate::types::husk_context::HuskContext; use crate::types::husk_context::HuskContext;
use crate::types::introducer::Introducer; 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); log::debug!("remote lookup for {:?}", query);
let mut jobs = JoinSet::new(); 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) .user_agent(USER_AGENT)
.connect_timeout(CONNECT_TIMEOUT) .connect_timeout(CONNECT_TIMEOUT)
.timeout(REQUEST_TIMEOUT) .timeout(REQUEST_TIMEOUT);
.build()?;
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 // local function to check certificates for usability
let collect_certs = |certs: Vec<Result<Cert, anyhow::Error>>, query: OnlineQuery, policy: &StandardPolicy| { let collect_certs = |certs: Vec<Result<Cert, anyhow::Error>>, query: OnlineQuery, policy: &StandardPolicy| {
@@ -152,8 +165,8 @@ async fn query_online_sources<'hc>(context: &HuskContext<'hc>, query: OnlineQuer
} }
} }
}, },
OnlineQuery::Fingerprint(_) => { OnlineQuery::Fingerprint(f) => {
found = true; found = cert.fingerprint() == f;
} }
} }
@@ -193,9 +206,19 @@ async fn query_online_sources<'hc>(context: &HuskContext<'hc>, query: OnlineQuer
let q = query.clone(); let q = query.clone();
match query { match query {
OnlineQuery::Email(e) => { 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 { if let Ok(certs) = ks.search(e.as_str()).await {
partial_result = collect_certs(certs, q, &p); partial_result = collect_certs(certs, q, &p);
} }
*/
partial_result partial_result
}, },
OnlineQuery::Fingerprint(f) => { OnlineQuery::Fingerprint(f) => {
@@ -934,7 +957,8 @@ jk6lh01Ej9QTGQ==
introducers: Vec::new(), introducers: Vec::new(),
keyservers: Vec::new(), keyservers: Vec::new(),
subject_replacement: "...".into(), subject_replacement: "...".into(),
mail: MailContext::new() mail: MailContext::new(),
use_tor_via: crate::config::TorConnectorType::NotUsed,
}; };
// store the local trust root // store the local trust root
+79 -20
View File
@@ -9,6 +9,7 @@ use toml;
use log4rs; use log4rs;
use crate::types::errors::HuskError; use crate::types::errors::HuskError;
use crate::types::defaults::CONNECTION;
use crate::types::defaults::CONSOLE_LOGGING; use crate::types::defaults::CONSOLE_LOGGING;
use crate::types::defaults::UPDATER_PID_FILE_LOCATION; use crate::types::defaults::UPDATER_PID_FILE_LOCATION;
use crate::types::defaults::UPDATER_STATE_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::PID_FILE_LOCATION;
use crate::types::defaults::SUBJECT_REPLACEMENT; use crate::types::defaults::SUBJECT_REPLACEMENT;
#[derive(Deserialize, Debug, Clone)]
pub enum TorConnectorType {
NotUsed,
Errornous,
SocksProxy(String),
Arti,
}
impl From<Option<String>> for TorConnectorType {
fn from(value: Option<String>) -> 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<String>,
pub keyservers: Option<Vec<String>>,
pub subject_replacement: Option<String>,
use_tor_via: Option<String>,
}
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct UpdaterConfig { pub struct UpdaterConfig {
pub wait_min: Option<u32>, pub wait_min: Option<u32>,
pub wait_max: Option<u32>, pub wait_max: Option<u32>,
pub pid_file: Option<String>, pub pid_file: Option<String>,
pub state_file: Option<String>, pub state_file: Option<String>,
use_tor_via: Option<String>,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct HuskConfig { pub struct HuskConfig {
pub connection: String,
pub sequoia_home: String, pub sequoia_home: String,
pub keyservers: Option<Vec<String>>, milter: Option<MilterConfig>,
pub subject_replacement: Option<String>,
pub pid_file: Option<String>,
updater: Option<UpdaterConfig>, updater: Option<UpdaterConfig>,
logging: Option<toml::Value>, logging: Option<toml::Value>,
} }
@@ -77,28 +112,47 @@ impl HuskConfig {
Ok(()) Ok(())
} }
pub fn get_keyservers(&self) -> Vec<String> { pub fn get_connection(&self) -> String {
if let Some(kss) = &self.keyservers { if let Some(milter_config) = &self.milter {
kss.clone() milter_config.connection.clone()
} else { } 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<String> {
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 { pub fn get_subject_replacement(&self) -> String {
if let Some(replacement) = &self.subject_replacement { if let Some(milter_config) = &self.milter {
replacement.clone() if let Some(replacement) = &milter_config.subject_replacement {
} else { return replacement.clone();
SUBJECT_REPLACEMENT.to_string() }
} }
SUBJECT_REPLACEMENT.to_string()
} }
pub fn get_pid_file(&self) -> String { pub fn get_pid_file(&self) -> String {
if let Some(pid_file) = &self.pid_file { if let Some(milter_config) = &self.milter {
pid_file.clone() if let Some(pid_file) = &milter_config.pid_file {
} else { return pid_file.clone();
PID_FILE_LOCATION.to_string() }
} }
PID_FILE_LOCATION.to_string()
} }
pub fn get_updater_pid_file(&self) -> String { pub fn get_updater_pid_file(&self) -> String {
@@ -147,6 +201,14 @@ impl HuskConfig {
UPDATER_WAIT_MAX 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<Mutex<HuskConfig>>; pub type HuskConfigContainer = Arc<Mutex<HuskConfig>>;
@@ -168,12 +230,9 @@ mod tests {
#[test] #[test]
fn subject_line() { fn subject_line() {
let config = HuskConfig { let config = HuskConfig {
connection: "".into(),
sequoia_home: "".into(), sequoia_home: "".into(),
keyservers: None,
subject_replacement: None,
logging: None, logging: None,
pid_file: None, milter: None,
updater: None, updater: None,
}; };
+2 -1
View File
@@ -33,7 +33,7 @@ impl Daemon {
let config: HuskConfig = config_container.clone().into(); let config: HuskConfig = config_container.clone().into();
let listener = TcpListener::bind(&config.connection) let listener = TcpListener::bind(&config.get_connection())
.await .await
.expect(" opening milter socket"); .expect(" opening milter socket");
@@ -116,6 +116,7 @@ impl Daemon {
// setup Context // setup Context
match HuskContext::new(&config) { match HuskContext::new(&config) {
Ok(mut context) => { Ok(mut context) => {
context.for_milter(&config);
context.set_sender(sender.unwrap()); context.set_sender(sender.unwrap());
cx.data = Some(context); cx.data = Some(context);
Status::Continue Status::Continue
+3
View File
@@ -1,5 +1,8 @@
//! Defaults //! Defaults
/// Default host and port of the milter.
pub const CONNECTION: &str = "localhost:3000";
pub const CONFIG_FILE_LOCATION: &str = "/etc/husk/config.toml"; pub const CONFIG_FILE_LOCATION: &str = "/etc/husk/config.toml";
/// If encrypting a mail, the subject is moved into the encrypted body /// If encrypting a mail, the subject is moved into the encrypted body
+11 -1
View File
@@ -17,7 +17,7 @@ use sequoia_cert_store::CertStore;
use sequoia_directories::Home; use sequoia_directories::Home;
use sequoia_keystore::Key; use sequoia_keystore::Key;
use crate::config::HuskConfig; use crate::config::{HuskConfig, TorConnectorType};
use crate::common::crypto; use crate::common::crypto;
use crate::types::mail_context::MailContext; use crate::types::mail_context::MailContext;
@@ -32,6 +32,7 @@ pub struct HuskContext<'hc> {
pub keyservers: Vec<String>, pub keyservers: Vec<String>,
pub subject_replacement: String, pub subject_replacement: String,
pub mail: MailContext<'hc>, pub mail: MailContext<'hc>,
pub use_tor_via: TorConnectorType,
} }
impl<'hc> HuskContext<'hc> { impl<'hc> HuskContext<'hc> {
@@ -82,6 +83,7 @@ impl<'hc> HuskContext<'hc> {
keyservers: config.get_keyservers(), keyservers: config.get_keyservers(),
subject_replacement: config.get_subject_replacement(), subject_replacement: config.get_subject_replacement(),
mail: MailContext::new(), mail: MailContext::new(),
use_tor_via: TorConnectorType::NotUsed,
}) })
} }
@@ -109,4 +111,12 @@ impl<'hc> HuskContext<'hc> {
self.mail.add_recipient(rcpt, certs) 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();
}
} }
+4 -3
View File
@@ -41,15 +41,16 @@ impl Updater {
let mut updater_state = UpdaterState::thaw(state_path) let mut updater_state = UpdaterState::thaw(state_path)
.await .await
.map_err(|e| { stay = false; e })?; .map_err(|e| { stay = false; e })?;
let context = &HuskContext::without_signkey(&c)?; let mut context = HuskContext::without_signkey(&c)?;
updater_state.refresh(context); context.for_updater(&c);
updater_state.refresh(&context);
if let Some(fpr) = updater_state.next() { if let Some(fpr) = updater_state.next() {
log::debug!("try to update {}", fpr.to_hex()); log::debug!("try to update {}", fpr.to_hex());
let stats = MergePublicCollectStats::new(); 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() { if !certs.is_empty() {
for cert in certs { for cert in certs {
let cert = Arc::new(LazyCert::from_cert(cert)); let cert = Arc::new(LazyCert::from_cert(cert));