Merge branch 'malte/updater' into 'main'
Implement process to update certificates in the store See merge request husk-project/husk-milter!11
This commit is contained in:
Generated
+43
-3
@@ -99,7 +99,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
@@ -333,6 +333,17 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
@@ -447,6 +458,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -1055,6 +1075,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1270,6 +1291,8 @@ dependencies = [
|
||||
"lettre",
|
||||
"log",
|
||||
"log4rs",
|
||||
"rand 0.10.2",
|
||||
"reqwest",
|
||||
"sequoia-cert-store",
|
||||
"sequoia-directories",
|
||||
"sequoia-keystore",
|
||||
@@ -1614,7 +1637,7 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
|
||||
dependencies = [
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2290,6 +2313,17 @@ dependencies = [
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
@@ -2328,6 +2362,12 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
@@ -2940,7 +2980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ indymilter = "0.3.0"
|
||||
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"
|
||||
|
||||
+49
-2
@@ -1,13 +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:
|
||||
# wait_min = 5
|
||||
# wait_max = 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]
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
appenders:
|
||||
stdout_appender:
|
||||
kind: console
|
||||
encoder:
|
||||
pattern: "{h({d(%Y-%m-%d %H:%M:%S)(utc)} - {l}: {m}{n})}"
|
||||
|
||||
file_appender:
|
||||
kind: file
|
||||
path: "/tmp/husk.log"
|
||||
encoder:
|
||||
pattern: "{h({d(%Y-%m-%d %H:%M:%S)(utc)} - {l}: {m}{n})}"
|
||||
|
||||
root:
|
||||
level: debug
|
||||
appenders:
|
||||
- stdout_appender
|
||||
@@ -61,7 +61,8 @@ RUN apt-get update -yqq && \
|
||||
sq \
|
||||
bind9-host \
|
||||
inotify-tools \
|
||||
mailutils
|
||||
mailutils \
|
||||
tor
|
||||
|
||||
RUN adduser --shell /bin/bash --disabled-password --gecos "" hockeypuck
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ authenticate $KARL_FPR
|
||||
create_unix_user laura
|
||||
|
||||
echo -e "To: karl@example.com, Laura <laura@example.com>\nSubject: Testmail\n\nTestmail." | sendmail -t
|
||||
sleep 1
|
||||
sleep 2
|
||||
|
||||
R=1
|
||||
if [ $(expect_mail karl encrypted) == 0 ] ; then
|
||||
|
||||
@@ -34,6 +34,7 @@ pub enum HuskSubcommands {
|
||||
Introducer(IntroducerCommand),
|
||||
Locals(LocalsCommand),
|
||||
Signkey(SignkeyCommand),
|
||||
Updater(UpdaterCommand),
|
||||
}
|
||||
|
||||
// Daemon subcommands
|
||||
@@ -72,6 +73,42 @@ pub struct DaemonStartCommand {
|
||||
background: bool,
|
||||
}
|
||||
|
||||
// Updater subcommands
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(
|
||||
name = "updater",
|
||||
about = "Manage the updater process.",
|
||||
subcommand_required = true,
|
||||
arg_required_else_help = true,
|
||||
disable_colored_help = true,
|
||||
disable_version_flag = true,
|
||||
)]
|
||||
pub struct UpdaterCommand {
|
||||
#[clap(subcommand)]
|
||||
pub subcommand: UpdaterSubcommand,
|
||||
}
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum UpdaterSubcommand {
|
||||
Start(UpdaterStartCommand),
|
||||
Stop,
|
||||
Status
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(
|
||||
name = "daemon",
|
||||
about = "Start the updater.",
|
||||
)]
|
||||
pub struct UpdaterStartCommand {
|
||||
#[clap(
|
||||
long = "background",
|
||||
help = "Start the updater as a background process.",
|
||||
default_value_t = false,
|
||||
)]
|
||||
background: bool,
|
||||
}
|
||||
|
||||
// Introducer subcommands
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::cli::cli_args::{CliArgs, HuskSubcommands};
|
||||
use crate::config::HuskConfigContainer;
|
||||
|
||||
pub mod daemon;
|
||||
pub mod updater;
|
||||
pub mod introducer;
|
||||
pub mod locals;
|
||||
pub mod signkey;
|
||||
@@ -48,6 +49,9 @@ pub async fn dispatch(cli: CliArgs, config: HuskConfigContainer) -> Result<()> {
|
||||
HuskSubcommands::Signkey(subcmd) => {
|
||||
commands::signkey::dispatch(subcmd, config).await?;
|
||||
}
|
||||
HuskSubcommands::Updater(subcmd) => {
|
||||
commands::updater::dispatch(subcmd, config).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::cli::cli_args::{DaemonCommand, DaemonSubcommand};
|
||||
use crate::common::process_descr::ProcessDescr;
|
||||
use crate::config::HuskConfigContainer;
|
||||
use crate::config::HuskConfig;
|
||||
use crate::Daemon;
|
||||
use crate::daemon::Daemon;
|
||||
|
||||
pub async fn dispatch(cmd: DaemonCommand, config: HuskConfigContainer) -> Result<()> {
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//! Updater first level dispatcher
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::cli::cli_args::{UpdaterCommand, UpdaterSubcommand};
|
||||
use crate::common::process_descr::ProcessDescr;
|
||||
use crate::config::HuskConfigContainer;
|
||||
use crate::config::HuskConfig;
|
||||
use crate::updater::Updater;
|
||||
|
||||
pub async fn dispatch(cmd: UpdaterCommand, config: HuskConfigContainer) -> Result<()> {
|
||||
|
||||
let c: HuskConfig = config.clone().into();
|
||||
|
||||
match cmd.subcommand {
|
||||
UpdaterSubcommand::Start(_sub_cmd) => {
|
||||
// Write the pid file.
|
||||
let pid_file_name = c.get_updater_pid_file();
|
||||
ProcessDescr::create(pid_file_name.clone())?;
|
||||
|
||||
let result_code = Updater::run(config).await;
|
||||
|
||||
// and remove it when the daemon terminated.
|
||||
let _ = ProcessDescr::remove(pid_file_name);
|
||||
return result_code;
|
||||
},
|
||||
UpdaterSubcommand::Stop => {
|
||||
let process_descr = ProcessDescr::load(c.get_updater_pid_file())?;
|
||||
if process_descr.send_sig_interrupt().is_ok() {
|
||||
let _ = ProcessDescr::remove(process_descr.file_location);
|
||||
}
|
||||
},
|
||||
UpdaterSubcommand::Status => {
|
||||
if let Ok(process_descr) = ProcessDescr::load(c.get_updater_pid_file()) {
|
||||
if process_descr.is_running() {
|
||||
println!("Running, with pid: {}", process_descr.pid);
|
||||
} else {
|
||||
println!("Not running, but stale pid file detected: {}",
|
||||
c.get_updater_pid_file());
|
||||
}
|
||||
} else {
|
||||
println!("Not running");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+120
-41
@@ -22,6 +22,7 @@ use sequoia_openpgp::packet::prelude::SignatureBuilder;
|
||||
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
|
||||
use sequoia_openpgp::policy::StandardPolicy;
|
||||
use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, Signer, LiteralWriter};
|
||||
use sequoia_openpgp::serialize::stream::padding::Padder;
|
||||
use sequoia_openpgp::{Fingerprint, Cert, Packet};
|
||||
use sequoia_openpgp::types::SignatureType;
|
||||
use sequoia_cert_store::{Store, CertStore, LazyCert, StoreUpdate};
|
||||
@@ -30,6 +31,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;
|
||||
@@ -96,40 +98,80 @@ const USER_AGENT: &'static str = concat!("husk/", env!("CARGO_PKG_VERSION"));
|
||||
const CONNECT_TIMEOUT: Duration = Duration::new(5, 0);
|
||||
const REQUEST_TIMEOUT: Duration = Duration::new(5, 0);
|
||||
|
||||
/// Try to fetch certificates for `email` from online sources (keyservers, wkd,
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum OnlineQuery {
|
||||
Fingerprint(Fingerprint),
|
||||
Email(String),
|
||||
}
|
||||
|
||||
/// Try to fetch certificates for email from online sources (keyservers, wkd,
|
||||
/// dane).
|
||||
pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
|
||||
pub async fn lookup_email<'hc>(context: &HuskContext<'hc>, email: &str)
|
||||
-> anyhow::Result<Vec<Cert>> {
|
||||
query_online_sources(context, OnlineQuery::Email(email.into())).await
|
||||
}
|
||||
|
||||
/// Try to fetch certificates for fingerprint from keyservers. wkd
|
||||
/// and dane can only be queried for email addresses.
|
||||
pub async fn lookup_fingerprint<'hc>(context: &HuskContext<'hc>, fpr: &str)
|
||||
-> anyhow::Result<Vec<Cert>> {
|
||||
let fpr = fpr.parse::<Fingerprint>()?;
|
||||
query_online_sources(context, OnlineQuery::Fingerprint(fpr)).await
|
||||
}
|
||||
|
||||
/// Try to fetch certificates for query from online sources (keyservers, wkd,
|
||||
/// dane).
|
||||
async fn query_online_sources<'hc>(context: &HuskContext<'hc>, query: OnlineQuery)
|
||||
-> anyhow::Result<Vec<Cert>> {
|
||||
|
||||
log::debug!("remote lookup for {}", email);
|
||||
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>>, email: String, policy: &StandardPolicy| {
|
||||
let collect_certs = |certs: Vec<Result<Cert, anyhow::Error>>, query: OnlineQuery, policy: &StandardPolicy| {
|
||||
let mut result = Vec::new();
|
||||
let mut seen: HashSet<Fingerprint> = HashSet::new();
|
||||
|
||||
for cert in certs.into_iter().flatten() {
|
||||
if let Ok(vc) = cert.with_policy(policy, None) {
|
||||
|
||||
// check if the returned certificate contains a userid with the
|
||||
// email address in question.
|
||||
let mut userid_found = false;
|
||||
for userid in vc.userids() {
|
||||
if let Ok(Some(u)) = userid.userid().email() {
|
||||
if u == email {
|
||||
userid_found = true;
|
||||
let mut found = false;
|
||||
match query.clone() {
|
||||
OnlineQuery::Email(email) => {
|
||||
// check if the returned certificate contains a userid with the
|
||||
// email address in question.
|
||||
for userid in vc.userids() {
|
||||
if let Ok(Some(u)) = userid.userid().email() {
|
||||
if u == email {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
OnlineQuery::Fingerprint(f) => {
|
||||
found = cert.fingerprint() == f;
|
||||
}
|
||||
}
|
||||
|
||||
if userid_found {
|
||||
if found {
|
||||
// check if the certificate can be used (for encryption).
|
||||
vc.keys()
|
||||
.supported()
|
||||
@@ -156,43 +198,72 @@ pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
|
||||
|
||||
if let Ok(ks) = KeyServer::with_client(keyserver.as_str(), http_client.clone()) {
|
||||
|
||||
let e = email.to_string();
|
||||
let p = StandardPolicy::new();
|
||||
let query = query.clone();
|
||||
|
||||
jobs.spawn(async move {
|
||||
let mut partial_result = Vec::new();
|
||||
|
||||
if let Ok(certs) = ks.search(e.as_str()).await {
|
||||
partial_result = collect_certs(certs, e, &p);
|
||||
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(err) => {
|
||||
log::debug!("Error: {} {}", e, err.to_string());
|
||||
}
|
||||
}
|
||||
/*
|
||||
if let Ok(certs) = ks.search(e.as_str()).await {
|
||||
partial_result = collect_certs(certs, q, &p);
|
||||
}
|
||||
*/
|
||||
partial_result
|
||||
},
|
||||
OnlineQuery::Fingerprint(f) => {
|
||||
if let Ok(certs) = ks.get(f).await {
|
||||
partial_result = collect_certs(certs, q, &p);
|
||||
}
|
||||
partial_result
|
||||
},
|
||||
}
|
||||
partial_result
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// WKD
|
||||
let e = email.to_string();
|
||||
let p = StandardPolicy::new();
|
||||
jobs.spawn(async move {
|
||||
let mut partial_result = Vec::new();
|
||||
// WKD and DANE need an email address
|
||||
if let OnlineQuery::Email(_) = query {
|
||||
let p = StandardPolicy::new();
|
||||
let q = query.clone();
|
||||
|
||||
if let Ok(certs) = wkd::get(&http_client.clone(), e.as_str()).await {
|
||||
partial_result = collect_certs(certs, e, &p);
|
||||
}
|
||||
partial_result
|
||||
});
|
||||
// WKD
|
||||
jobs.spawn(async move {
|
||||
let mut partial_result = Vec::new();
|
||||
|
||||
// DANE
|
||||
let e = email.to_string();
|
||||
let p = StandardPolicy::new();
|
||||
jobs.spawn(async move {
|
||||
let mut partial_result = Vec::new();
|
||||
if let OnlineQuery::Email(e) = q.clone() {
|
||||
if let Ok(certs) = wkd::get(&http_client.clone(), e.as_str()).await {
|
||||
partial_result = collect_certs(certs, q, &p);
|
||||
}
|
||||
}
|
||||
partial_result
|
||||
});
|
||||
|
||||
if let Ok(certs) = dane::get(e.as_str()).await {
|
||||
partial_result = collect_certs(certs, e, &p);
|
||||
}
|
||||
partial_result
|
||||
});
|
||||
// DANE
|
||||
let p = StandardPolicy::new();
|
||||
let q = query.clone();
|
||||
jobs.spawn(async move {
|
||||
let mut partial_result = Vec::new();
|
||||
|
||||
if let OnlineQuery::Email(e) = q {
|
||||
if let Ok(certs) = dane::get(e.as_str()).await {
|
||||
partial_result = collect_certs(certs, query, &p);
|
||||
}
|
||||
}
|
||||
partial_result
|
||||
});
|
||||
}
|
||||
|
||||
// join the partial results. As the same certificate might be returned by
|
||||
// several sources, dedup and merge the certificates.
|
||||
@@ -240,7 +311,7 @@ pub async fn get_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
|
||||
introducer.cert.self_signed_userids().map(|u| u.userid()).collect::<Vec<_>>());
|
||||
if introducer.can_introduce(format!("<{}>", email).as_str()) {
|
||||
log::debug!(" introducer regex matches!");
|
||||
match lookup_certificates(context, email).await {
|
||||
match lookup_email(context, email).await {
|
||||
Ok(certs) => {
|
||||
for cert in certs {
|
||||
if introducer.is_introducing(&cert) {
|
||||
@@ -412,9 +483,14 @@ pub fn is_encrypted(body: &Bytes) -> bool {
|
||||
pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipient>)
|
||||
-> anyhow::Result<Bytes> {
|
||||
|
||||
log::debug!("encrypt for {}", recipients.iter()
|
||||
.map(|r| r.email.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", "));
|
||||
let mut keys = Vec::new();
|
||||
|
||||
for rcpt in recipients {
|
||||
log::debug!("Check recipient {}", rcpt.email);
|
||||
if rcpt.can_encrypt() {
|
||||
for cert in &rcpt.certs {
|
||||
cert.with_policy(&context.policy, None)?
|
||||
@@ -434,6 +510,7 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipien
|
||||
let message = Message::new(&mut sink);
|
||||
let message = Armorer::new(message).build()?;
|
||||
let mut message = Encryptor::for_recipients(message, keys).build()?;
|
||||
message = Padder::new(message).build()?;
|
||||
if let Some(key) = &context.signing_key {
|
||||
log::debug!("use signing key {}", key.fingerprint());
|
||||
message = Signer::new(message, key.clone())?.build()?;
|
||||
@@ -451,6 +528,7 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipien
|
||||
|
||||
Ok(Bytes::from(sink))
|
||||
} else {
|
||||
log::debug!("not keys at all");
|
||||
Err(HuskError::NoEncryptionKeys.into())
|
||||
}
|
||||
}
|
||||
@@ -887,7 +965,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
|
||||
|
||||
+138
-17
@@ -9,17 +9,65 @@ 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;
|
||||
use crate::types::defaults::UPDATER_WAIT_MIN;
|
||||
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 struct HuskConfig {
|
||||
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.eq_ignore_ascii_case("arti") {
|
||||
TorConnectorType::Arti
|
||||
} else {
|
||||
if let Some(url) = v.strip_prefix("socks5://") {
|
||||
TorConnectorType::SocksProxy(url.to_string())
|
||||
} else {
|
||||
TorConnectorType::Errornous
|
||||
}
|
||||
}
|
||||
},
|
||||
None => TorConnectorType::NotUsed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct MilterConfig {
|
||||
pub connection: String,
|
||||
pub sequoia_home: 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 sequoia_home: String,
|
||||
milter: Option<MilterConfig>,
|
||||
updater: Option<UpdaterConfig>,
|
||||
logging: Option<toml::Value>,
|
||||
}
|
||||
|
||||
@@ -64,26 +112,101 @@ 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_subject_replacement(&self) -> String {
|
||||
if let Some(replacement) = &self.subject_replacement {
|
||||
replacement.clone()
|
||||
pub fn get_milter_tor_usage(&self) -> TorConnectorType {
|
||||
if let Some(milter_config) = &self.milter {
|
||||
milter_config.use_tor_via.clone().into()
|
||||
} else {
|
||||
SUBJECT_REPLACEMENT.to_string()
|
||||
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(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()
|
||||
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 {
|
||||
if let Some(updater_config) = &self.updater {
|
||||
if let Some(pid_file) = &updater_config.pid_file {
|
||||
pid_file.clone()
|
||||
} else {
|
||||
UPDATER_PID_FILE_LOCATION.to_string()
|
||||
}
|
||||
} else {
|
||||
PID_FILE_LOCATION.to_string()
|
||||
UPDATER_PID_FILE_LOCATION.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_updater_state_file(&self) -> String {
|
||||
if let Some(updater_config) = &self.updater {
|
||||
if let Some(state_file) = &updater_config.state_file {
|
||||
state_file.clone()
|
||||
} else {
|
||||
UPDATER_STATE_FILE_LOCATION.to_string()
|
||||
}
|
||||
} else {
|
||||
UPDATER_STATE_FILE_LOCATION.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_updater_min_wait(&self) -> u32 {
|
||||
if let Some(updater_config) = &self.updater {
|
||||
if let Some(wait_min) = &updater_config.wait_min {
|
||||
*wait_min
|
||||
} else {
|
||||
UPDATER_WAIT_MIN
|
||||
}
|
||||
} else {
|
||||
UPDATER_WAIT_MIN
|
||||
}
|
||||
}
|
||||
pub fn get_updater_max_wait(&self) -> u32 {
|
||||
if let Some(updater_config) = &self.updater {
|
||||
if let Some(wait_max) = &updater_config.wait_max {
|
||||
*wait_max
|
||||
} else {
|
||||
UPDATER_WAIT_MAX
|
||||
}
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,12 +230,10 @@ 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,
|
||||
};
|
||||
|
||||
assert_eq!(config.get_subject_replacement(), defaults::SUBJECT_REPLACEMENT);
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -17,7 +17,7 @@ use config::{HuskConfig, HuskConfigContainer};
|
||||
pub mod types;
|
||||
pub mod mail;
|
||||
pub mod daemon;
|
||||
use daemon::Daemon;
|
||||
pub mod updater;
|
||||
pub mod common;
|
||||
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
@@ -26,6 +26,7 @@ pub fn main() -> anyhow::Result<()> {
|
||||
// XXX keep this in sync with cli/cli_args.rs
|
||||
for arg in std::env::args() {
|
||||
if arg == "daemon" && daemonize == 0 { daemonize = 1; }
|
||||
if arg == "updater" && daemonize == 0 { daemonize = 1; }
|
||||
if arg == "start" && daemonize == 1 { daemonize = 2; }
|
||||
if arg == "--background" && daemonize == 2 { daemonize = 3; }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
//! 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
|
||||
/// and replaced by `SUBJECT_REPLACEMENT` (unless configured otherwise).
|
||||
pub const SUBJECT_REPLACEMENT: &str = "...";
|
||||
|
||||
pub const PID_FILE_LOCATION: &str = "/run/husk/pid";
|
||||
pub const PID_FILE_LOCATION: &str = "/var/lib/husk/husk.pid";
|
||||
pub const UPDATER_PID_FILE_LOCATION: &str = "/var/lib/husk/updater.pid";
|
||||
pub const UPDATER_STATE_FILE_LOCATION: &str = "/var/lib/husk/updater.state";
|
||||
|
||||
pub const UPDATER_WAIT_MIN: u32 = 5;
|
||||
pub const UPDATER_WAIT_MAX: u32 = 3600;
|
||||
|
||||
pub const FOOTER: &str = "---\nProcessed by Husk at {date}.\n";
|
||||
pub const FOOTER_RECIPIENTS: &str = "This mail was also send to {rcpts}.\n";
|
||||
|
||||
+34
-11
@@ -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,12 +32,22 @@ 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> {
|
||||
|
||||
/// Create a new HuskContext instance.
|
||||
pub fn new(config: &HuskConfig) -> anyhow::Result<HuskContext<'hc>> {
|
||||
Self::create(config, true)
|
||||
}
|
||||
|
||||
/// Create a new HuskContext without determine the signing key
|
||||
pub fn without_signkey(config: &HuskConfig) -> anyhow::Result<HuskContext<'hc>> {
|
||||
Self::create(config, false)
|
||||
}
|
||||
|
||||
/// Create a new HuskContext instance.
|
||||
pub fn create(config: &HuskConfig, with_signkey: bool) -> 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);
|
||||
@@ -48,16 +58,20 @@ impl<'hc> HuskContext<'hc> {
|
||||
let local_trust_root = crypto::get_local_trust_root(&cert_store)?;
|
||||
let introducers = crypto::get_introducers(&cert_store, &policy, local_trust_root.fingerprint());
|
||||
|
||||
let signing_key = match crypto::get_signing_key(sequoia_home, &cert_store, &policy, &local_trust_root) {
|
||||
Ok(Some(cert)) => Some(cert),
|
||||
Ok(None) => {
|
||||
log::warn!("No signing key found");
|
||||
None
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error while fetching signing key: {}", e);
|
||||
None
|
||||
let signing_key = if with_signkey {
|
||||
match crypto::get_signing_key(sequoia_home, &cert_store, &policy, &local_trust_root) {
|
||||
Ok(Some(cert)) => Some(cert),
|
||||
Ok(None) => {
|
||||
log::warn!("No signing key found");
|
||||
None
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Error while fetching signing key: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(HuskContext {
|
||||
@@ -69,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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -96,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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
//! Husk certificate updater
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use sequoia_cert_store::LazyCert;
|
||||
use sequoia_cert_store::Store;
|
||||
use sequoia_cert_store::StoreUpdate;
|
||||
use sequoia_cert_store::store::MergePublicCollectStats;
|
||||
use tokio::time::sleep;
|
||||
use tokio::time::Duration;
|
||||
|
||||
use crate::config::HuskConfig;
|
||||
use crate::config::HuskConfigContainer;
|
||||
use crate::common::crypto;
|
||||
use crate::types::husk_context::HuskContext;
|
||||
use state::UpdaterState;
|
||||
|
||||
pub mod state;
|
||||
|
||||
pub struct Updater { }
|
||||
|
||||
impl Updater {
|
||||
|
||||
async fn delay(min: u32, max: u32) {
|
||||
|
||||
let val = rand::random_range(min..max);
|
||||
log::debug!("updater sleeping for {} secs", val);
|
||||
sleep(Duration::from_secs(val as u64)).await;
|
||||
}
|
||||
|
||||
pub async fn run(config_container: HuskConfigContainer) -> anyhow::Result<()> {
|
||||
|
||||
let c: HuskConfig = config_container.into();
|
||||
|
||||
let mut stay = true;
|
||||
let state_file = c.get_updater_state_file();
|
||||
let state_path = Path::new(&state_file);
|
||||
while stay {
|
||||
Self::delay(c.get_updater_min_wait(), c.get_updater_max_wait()).await;
|
||||
|
||||
let mut updater_state = UpdaterState::thaw(state_path)
|
||||
.await
|
||||
.inspect_err(|_| { stay = false; })?;
|
||||
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());
|
||||
|
||||
// get local cert from cert store to get the user emails
|
||||
let mut emails = Vec::new();
|
||||
if let Ok(local_certs) = context.cert_store.lookup_by_cert(&fpr.clone().into()) {
|
||||
local_certs.iter()
|
||||
.map(|c| c.with_policy(&context.policy, None))
|
||||
.filter_map(|c| c.ok())
|
||||
.filter(|c|
|
||||
c.keys()
|
||||
.supported()
|
||||
.alive()
|
||||
.revoked(false)
|
||||
.for_transport_encryption()
|
||||
.count() > 0)
|
||||
.for_each(|c|
|
||||
c.userids()
|
||||
.map(|u| u.userid().email())
|
||||
.filter_map(|u| if let Ok(e) = u { e } else { None })
|
||||
.for_each(|e| emails.push(e.to_string()))
|
||||
);
|
||||
|
||||
}
|
||||
log::debug!("with emails: {:?}", emails);
|
||||
|
||||
let stats = MergePublicCollectStats::new();
|
||||
|
||||
let mut certs = crypto::lookup_fingerprint(&context, fpr.to_hex().as_str()).await?;
|
||||
for email in emails {
|
||||
crypto::lookup_email(&context, email.as_str()).await?.iter()
|
||||
.for_each(|c| {
|
||||
if c.fingerprint() == fpr {
|
||||
certs.push(c.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
if !certs.is_empty() {
|
||||
for cert in certs {
|
||||
let cert = Arc::new(LazyCert::from_cert(cert));
|
||||
if context.cert_store.update_by(cert, &stats).is_ok() {
|
||||
updater_state.mark_processed(&fpr, stats.updated_certs() > 0);
|
||||
} else {
|
||||
updater_state.incr_retry_count(&fpr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no updates found
|
||||
updater_state.mark_processed(&fpr, false);
|
||||
}
|
||||
UpdaterState::freeze(state_path, &updater_state)
|
||||
.await
|
||||
.inspect_err(|_| { stay = false; })?;
|
||||
} else {
|
||||
// finished update round
|
||||
let (processed, updated) = updater_state.summarize();
|
||||
log::info!("Updater finished round: {} certificates processed, {} updated",
|
||||
processed,
|
||||
updated);
|
||||
// remove state from disk
|
||||
UpdaterState::remove(state_path)
|
||||
.await
|
||||
.inspect_err(|_| { stay = false; })?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
//! manage the state of certificates while updating
|
||||
|
||||
use std::{fmt, fs};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
use chrono::{Local, NaiveDateTime};
|
||||
use rand::prelude::*;
|
||||
use sequoia_openpgp::Fingerprint;
|
||||
use sequoia_openpgp::types::RevocationStatus;
|
||||
use sequoia_openpgp::parse::buffered_reader::File;
|
||||
use sequoia_cert_store::Store;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::types::husk_context::HuskContext;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum UpdaterError {
|
||||
#[error("IO Error {0}")]
|
||||
IoError(String),
|
||||
#[error("Start time missing")]
|
||||
StartTimeMissing,
|
||||
#[error("Wrong record size {0}")]
|
||||
WrongRecordSize(usize),
|
||||
#[error("Data missing")]
|
||||
DataMissing,
|
||||
#[error("Broken persisted cert state: {0}")]
|
||||
BrokenState(String),
|
||||
#[error("File not found {0}")]
|
||||
FileNotFound(String),
|
||||
}
|
||||
|
||||
const MAX_RETRY: u32 = 3;
|
||||
const DATETIME_PATTERN: &str = "%Y-%m-%d %H:%M:%S";
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct CertState {
|
||||
fpr: Fingerprint,
|
||||
processed: bool,
|
||||
updated: bool,
|
||||
// XXX: do we really want to retry within the same update round?
|
||||
retry_count: u32,
|
||||
}
|
||||
|
||||
impl fmt::Display for CertState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "{}:{}:{}:{}",
|
||||
self.fpr,
|
||||
self.processed,
|
||||
self.updated,
|
||||
self.retry_count)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for CertState {
|
||||
type Error = UpdaterError;
|
||||
|
||||
fn try_from(line: &str) -> Result<Self, UpdaterError> {
|
||||
let line = line.trim();
|
||||
let parts: Vec<&str> = line.split(":").collect();
|
||||
|
||||
if parts.len() != 4 {
|
||||
return Err(UpdaterError::WrongRecordSize(parts.len()));
|
||||
}
|
||||
|
||||
let fpr = parts[0]
|
||||
.parse::<Fingerprint>()
|
||||
.map_err(|_e| UpdaterError::BrokenState(parts[0].to_string()))?;
|
||||
let processed = parts[1]
|
||||
.parse::<bool>()
|
||||
.map_err(|_e| UpdaterError::BrokenState(parts[1].to_string()))?;
|
||||
let updated = parts[2]
|
||||
.parse::<bool>()
|
||||
.map_err(|_e| UpdaterError::BrokenState(parts[2].to_string()))?;
|
||||
let retry_count = parts[3]
|
||||
.parse::<u32>()
|
||||
.map_err(|_e| UpdaterError::BrokenState(parts[3].to_string()))?;
|
||||
Ok(CertState {
|
||||
fpr,
|
||||
processed,
|
||||
updated,
|
||||
retry_count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for CertState {
|
||||
type Error = UpdaterError;
|
||||
|
||||
fn try_from(line: String) -> Result<Self, UpdaterError> {
|
||||
line.as_str().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl CertState {
|
||||
|
||||
pub fn new(fpr: Fingerprint) -> Self {
|
||||
Self {
|
||||
fpr,
|
||||
processed: false,
|
||||
updated: false,
|
||||
retry_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this certificate is already processed.
|
||||
pub fn need_processing(&self) -> bool {
|
||||
!(self.processed || (self.retry_count >= MAX_RETRY))
|
||||
}
|
||||
|
||||
/// Mark this certificate as processed - this doesn't mean that
|
||||
/// there was an actual update.
|
||||
pub fn mark_processed(&mut self) {
|
||||
self.processed = true;
|
||||
}
|
||||
|
||||
/// Mark this certificate as updated - this doesn't mark the
|
||||
/// certificate as processed.
|
||||
pub fn mark_updated(&mut self) {
|
||||
self.updated = true;
|
||||
}
|
||||
|
||||
/// Increment the retry count.
|
||||
pub fn incr_retry_count(&mut self) {
|
||||
self.retry_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct UpdaterState {
|
||||
started: NaiveDateTime,
|
||||
certs: BTreeMap<Fingerprint, CertState>,
|
||||
}
|
||||
|
||||
impl TryFrom<String> for UpdaterState {
|
||||
type Error = UpdaterError;
|
||||
|
||||
fn try_from(data: String) -> Result<Self, UpdaterError> {
|
||||
let mut result = BTreeMap::new();
|
||||
let mut lines = data.lines();
|
||||
let started = lines
|
||||
.next()
|
||||
.and_then(|l|
|
||||
NaiveDateTime::parse_from_str(l, DATETIME_PATTERN).ok());
|
||||
if started.is_none() {
|
||||
return Err(UpdaterError::DataMissing);
|
||||
}
|
||||
|
||||
for line in lines {
|
||||
if line.is_empty() { continue; }
|
||||
let cert_state: CertState = line.try_into()?;
|
||||
result.insert(cert_state.fpr.clone(), cert_state);
|
||||
}
|
||||
Ok(UpdaterState {
|
||||
started: started.unwrap(),
|
||||
certs: result
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for UpdaterState {
|
||||
type Error = UpdaterError;
|
||||
|
||||
fn try_from(data: Vec<u8>) -> Result<Self, UpdaterError> {
|
||||
Self::try_from(String::from_utf8_lossy(&data[..]).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for UpdaterState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "{}", self.started.format(DATETIME_PATTERN))?;
|
||||
for cert in self.certs.values() {
|
||||
write!(f, "{}", cert)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UpdaterState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdaterState {
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
started: Local::now().naive_local(),
|
||||
certs: BTreeMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the next certificate from the updater, which still needs
|
||||
/// processing - this includes retries.
|
||||
pub fn next(&mut self) -> Option<Fingerprint> {
|
||||
let set: Vec<&CertState> = self.certs.values()
|
||||
.filter(|c| c.need_processing())
|
||||
.collect();
|
||||
let mut rng = rand::rng();
|
||||
set.choose(&mut rng)
|
||||
.and_then(|c| Some(c.fpr.clone()))
|
||||
}
|
||||
|
||||
/// Mark the certificate (identified by its fingerprint) as processed. Also
|
||||
/// mark it as updated if `updated` is true.
|
||||
pub fn mark_processed(&mut self, fpr: &Fingerprint, updated: bool) {
|
||||
if let Some(entry) = self.certs.get_mut(fpr) {
|
||||
entry.mark_processed();
|
||||
if updated {
|
||||
entry.mark_updated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the retry count of the certificate (identified by its
|
||||
/// fingerprint).
|
||||
pub fn incr_retry_count(&mut self, fpr: &Fingerprint) {
|
||||
if let Some(entry) = self.certs.get_mut(fpr) {
|
||||
entry.incr_retry_count();
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan the certificate store for new certificates and add them to the
|
||||
/// state (as unprocessed).
|
||||
pub fn refresh(&mut self, context: &HuskContext<'_>) {
|
||||
|
||||
let count = self.certs.keys().count();
|
||||
|
||||
context.cert_store.certs()
|
||||
.filter(|c| {
|
||||
if let Ok(vc) = c.with_policy(&context.policy, None) {
|
||||
// Ignore revoked certificates.
|
||||
if let RevocationStatus::Revoked(_) = vc.revocation_status() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore non exportable certificates, this includes internal
|
||||
// certificates like the trust root or shadow CAs.
|
||||
vc.cert().exportable()
|
||||
} else {
|
||||
// Certificate is invalid under the current policy.
|
||||
false
|
||||
}
|
||||
})
|
||||
.map(|c| c.fingerprint())
|
||||
.for_each(|fpr| {
|
||||
if ! self.certs.contains_key(&fpr) {
|
||||
self.certs.insert(fpr.clone(), CertState::new(fpr));
|
||||
}
|
||||
});
|
||||
|
||||
log::debug!("refresh from {} to {}",
|
||||
count,
|
||||
self.certs.keys().count());
|
||||
}
|
||||
|
||||
/// Recreate the state from file.
|
||||
pub async fn thaw(file: &Path) -> Result<Self, UpdaterError> {
|
||||
|
||||
if !file.exists() {
|
||||
log::debug!("file not existing: {:?}", file);
|
||||
if let Some(parent) = file.parent() {
|
||||
log::debug!("assert parent directory");
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||
}
|
||||
return Ok(Self::new());
|
||||
}
|
||||
|
||||
let meta = file.metadata()
|
||||
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||
let updater_state = if meta.len() == 0 {
|
||||
// no previous state
|
||||
Self::new()
|
||||
} else {
|
||||
let mut handle = File::open(file)
|
||||
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||
let mut content = Vec::with_capacity(meta.len() as usize);
|
||||
handle.read_to_end(&mut content)
|
||||
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||
content.try_into()?
|
||||
};
|
||||
|
||||
Ok(updater_state)
|
||||
}
|
||||
|
||||
/// Persist state to file.
|
||||
pub async fn freeze(file: &Path, updater_state: &UpdaterState) -> Result<(), UpdaterError> {
|
||||
// freeze comes after thaw, so we assume that the parent directory
|
||||
// exists.
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true)
|
||||
.create(true)
|
||||
.truncate(true);
|
||||
#[cfg(unix)]
|
||||
options.mode(0o600);
|
||||
|
||||
let mut handle = options.open(file)
|
||||
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||
|
||||
handle.write_all(updater_state.to_string().as_bytes())
|
||||
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the state file.
|
||||
pub async fn remove(file: &Path) -> Result<(), UpdaterError> {
|
||||
if file.exists() {
|
||||
fs::remove_file(file)
|
||||
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create some statistics.
|
||||
pub fn summarize(&self) -> (usize, usize) {
|
||||
let mut processed = 0;
|
||||
let mut updated = 0;
|
||||
for cert in self.certs.values() {
|
||||
if cert.processed { processed += 1; }
|
||||
if cert.updated { updated += 1; }
|
||||
}
|
||||
(processed, updated)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::updater::state::{DATETIME_PATTERN, UpdaterError};
|
||||
use chrono::NaiveDateTime;
|
||||
use sequoia_openpgp::Fingerprint;
|
||||
|
||||
use super::{CertState, UpdaterState};
|
||||
|
||||
|
||||
#[test]
|
||||
fn serialize_deserialize( ) -> Result<(), UpdaterError> {
|
||||
|
||||
let cert_state = CertState {
|
||||
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB68"
|
||||
.parse::<Fingerprint>()
|
||||
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||
processed: false,
|
||||
updated: false,
|
||||
retry_count: 0,
|
||||
};
|
||||
|
||||
let serialized: String = format!("{}", cert_state);
|
||||
let deserialized: CertState = serialized.try_into()?;
|
||||
|
||||
assert_eq!(cert_state, deserialized);
|
||||
|
||||
let cert_state = CertState {
|
||||
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB68"
|
||||
.parse::<Fingerprint>()
|
||||
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||
processed: false,
|
||||
updated: true,
|
||||
retry_count: 3,
|
||||
};
|
||||
|
||||
let serialized: String = format!("{}", cert_state);
|
||||
eprintln!("{}", serialized);
|
||||
|
||||
let deserialized: CertState = serialized.try_into()?;
|
||||
|
||||
assert_eq!(cert_state, deserialized);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_deserialize_store( ) -> Result<(), UpdaterError> {
|
||||
|
||||
let store = UpdaterState {
|
||||
started: NaiveDateTime::parse_from_str("2026-07-11 15:06:10", DATETIME_PATTERN)
|
||||
.map_err(|_e| UpdaterError::BrokenState("in test".into()))?,
|
||||
certs: [
|
||||
CertState {
|
||||
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB68"
|
||||
.parse::<Fingerprint>()
|
||||
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||
processed: false,
|
||||
updated: true,
|
||||
retry_count: 3,
|
||||
},
|
||||
CertState {
|
||||
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB69"
|
||||
.parse::<Fingerprint>()
|
||||
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||
processed: false,
|
||||
updated: true,
|
||||
retry_count: 3,
|
||||
},
|
||||
CertState {
|
||||
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB6A"
|
||||
.parse::<Fingerprint>()
|
||||
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||
processed: false,
|
||||
updated: true,
|
||||
retry_count: 1,
|
||||
}
|
||||
].into_iter()
|
||||
.map(|c| (c.fpr.clone(), c))
|
||||
.collect::<BTreeMap<Fingerprint, CertState>>()
|
||||
};
|
||||
|
||||
let serialized = store.to_string();
|
||||
eprintln!("{}", serialized);
|
||||
let deserialized: UpdaterState = serialized.try_into()?;
|
||||
|
||||
assert_eq!(store, deserialized);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_turnaround() -> Result<(), UpdaterError> {
|
||||
let mut store = UpdaterState {
|
||||
started: NaiveDateTime::parse_from_str("2026-07-11 15:06:10", DATETIME_PATTERN)
|
||||
.map_err(|_e| UpdaterError::BrokenState("in test".into()))?,
|
||||
certs: [
|
||||
CertState {
|
||||
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB68"
|
||||
.parse::<Fingerprint>()
|
||||
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||
processed: false,
|
||||
updated: false,
|
||||
retry_count: 0,
|
||||
},
|
||||
CertState {
|
||||
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB69"
|
||||
.parse::<Fingerprint>()
|
||||
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||
processed: false,
|
||||
updated: false,
|
||||
retry_count: 0,
|
||||
},
|
||||
CertState {
|
||||
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB6A"
|
||||
.parse::<Fingerprint>()
|
||||
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||
processed: false,
|
||||
updated: false,
|
||||
retry_count: 0,
|
||||
}
|
||||
].into_iter()
|
||||
.map(|c| (c.fpr.clone(), c))
|
||||
.collect::<BTreeMap<Fingerprint, CertState>>()
|
||||
};
|
||||
|
||||
let mut break_counter = 0;
|
||||
// this loop has to end
|
||||
while let Some(fpr) = store.next() {
|
||||
assert!(break_counter < store.certs.len());
|
||||
break_counter += 1;
|
||||
store.mark_processed(&fpr, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user