Updater
- The updater looks for updates to certificates from the certificate store. - The updates are spaced by random time intervals so that the reconstruction of the content of the cert store is made more difficult. - The updater can be send into the background - start, stop and status are implemented. - The state of the updater is persisted to disk so that the update procedure survives a restart.
This commit is contained in:
Generated
+42
-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,7 @@ dependencies = [
|
||||
"lettre",
|
||||
"log",
|
||||
"log4rs",
|
||||
"rand 0.10.2",
|
||||
"sequoia-cert-store",
|
||||
"sequoia-directories",
|
||||
"sequoia-keystore",
|
||||
@@ -1614,7 +1636,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 +2312,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 +2361,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 +2979,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,7 @@ indymilter = "0.3.0"
|
||||
lettre = "0.11.22"
|
||||
log = "0.4.30"
|
||||
log4rs = "1.3.0"
|
||||
rand = "0.10.2"
|
||||
sequoia-cert-store = "0.7.3"
|
||||
sequoia-directories = "0.1.0"
|
||||
sequoia-keystore = "0.7.2"
|
||||
|
||||
@@ -8,6 +8,10 @@ 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" ]
|
||||
keyservers = [ "hkp://example.com" ]
|
||||
|
||||
[updater]
|
||||
wait_min = 1
|
||||
wait_max = 2
|
||||
|
||||
[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
|
||||
@@ -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(())
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -10,9 +10,21 @@ use log4rs;
|
||||
|
||||
use crate::types::errors::HuskError;
|
||||
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 UpdaterConfig {
|
||||
pub wait_min: Option<u32>,
|
||||
pub wait_max: Option<u32>,
|
||||
pub pid_file: Option<String>,
|
||||
pub state_file: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct HuskConfig {
|
||||
pub connection: String,
|
||||
@@ -20,6 +32,7 @@ pub struct HuskConfig {
|
||||
pub keyservers: Option<Vec<String>>,
|
||||
pub subject_replacement: Option<String>,
|
||||
pub pid_file: Option<String>,
|
||||
updater: Option<UpdaterConfig>,
|
||||
logging: Option<toml::Value>,
|
||||
}
|
||||
|
||||
@@ -79,6 +92,7 @@ impl HuskConfig {
|
||||
SUBJECT_REPLACEMENT.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_pid_file(&self) -> String {
|
||||
if let Some(pid_file) = &self.pid_file {
|
||||
pid_file.clone()
|
||||
@@ -86,6 +100,53 @@ impl HuskConfig {
|
||||
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 {
|
||||
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 type HuskConfigContainer = Arc<Mutex<HuskConfig>>;
|
||||
@@ -113,6 +174,7 @@ mod tests {
|
||||
subject_replacement: None,
|
||||
logging: None,
|
||||
pid_file: None,
|
||||
updater: None,
|
||||
};
|
||||
|
||||
assert_eq!(config.get_subject_replacement(), defaults::SUBJECT_REPLACEMENT);
|
||||
|
||||
+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; }
|
||||
}
|
||||
|
||||
@@ -6,7 +6,12 @@ pub const CONFIG_FILE_LOCATION: &str = "/etc/husk/config.toml";
|
||||
/// 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";
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Husk certificate updater
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use sequoia_cert_store::LazyCert;
|
||||
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
|
||||
.map_err(|e| { stay = false; e })?;
|
||||
let context = &HuskContext::without_signkey(&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?;
|
||||
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
|
||||
.map_err(|e| { stay = false; e })?;
|
||||
} 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
|
||||
.map_err(|e| { stay = false; e })?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
//! 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;
|
||||
|
||||
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: fpr,
|
||||
processed: false,
|
||||
updated: false,
|
||||
retry_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
write!(f, "{}", cert)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
// XXX: on unix set mode 0o666
|
||||
let mut handle = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.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 {
|
||||
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