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",
"log4rs",
"rand 0.10.2",
"reqwest",
"sequoia-cert-store",
"sequoia-directories",
"sequoia-keystore",
+2
View File
@@ -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"
+45 -2
View File
@@ -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]
+30 -6
View File
@@ -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<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(_) => {
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
+79 -20
View File
@@ -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<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)]
pub struct UpdaterConfig {
pub wait_min: Option<u32>,
pub wait_max: Option<u32>,
pub pid_file: Option<String>,
pub state_file: Option<String>,
use_tor_via: Option<String>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct HuskConfig {
pub connection: String,
pub sequoia_home: String,
pub keyservers: Option<Vec<String>>,
pub subject_replacement: Option<String>,
pub pid_file: Option<String>,
milter: Option<MilterConfig>,
updater: Option<UpdaterConfig>,
logging: Option<toml::Value>,
}
@@ -77,28 +112,47 @@ impl HuskConfig {
Ok(())
}
pub fn get_keyservers(&self) -> Vec<String> {
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<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 {
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<Mutex<HuskConfig>>;
@@ -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,
};
+2 -1
View File
@@ -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
+3
View File
@@ -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
+11 -1
View File
@@ -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<String>,
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();
}
}
+4 -3
View File
@@ -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));