From 64a2c7aa13912d610fe336e8dab4c72412ec3fae Mon Sep 17 00:00:00 2001 From: Sebastian Tobie Date: Wed, 14 May 2025 23:04:56 +0200 Subject: [PATCH] Seperated code into different files and functons Part 1 --- src/main.rs | 54 +++++++--------- src/process.rs | 53 +++++++++++---- src/{ => types}/config.rs | 129 ++++++------------------------------- src/types/cryptography.rs | 31 +++++++++ src/types/foreign_impl.rs | 83 ++++++++++++++++++++++++ src/types/mod.rs | 14 ++++ src/{ => types}/structs.rs | 77 +++------------------- src/types/traits.rs | 117 +++++++++++++++++++++++++++++++++ src/utils.rs | 34 ++-------- 9 files changed, 339 insertions(+), 253 deletions(-) rename src/{ => types}/config.rs (53%) create mode 100644 src/types/cryptography.rs create mode 100644 src/types/foreign_impl.rs create mode 100644 src/types/mod.rs rename src/{ => types}/structs.rs (64%) create mode 100644 src/types/traits.rs diff --git a/src/main.rs b/src/main.rs index b91f8f1..8ba9051 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,20 +3,25 @@ #![allow(clippy::clone_on_copy)] #![allow(clippy::identity_op)] -pub(crate) mod config; pub(crate) mod consts; pub(crate) mod macros; pub(crate) mod process; -pub(crate) mod structs; +pub(crate) mod types; pub(crate) mod utils; use crate::{ - config::SiteConfig, consts::*, macros::match_error, - structs::{ - Arguments, - ProcessorArgs, + types::{ + config, + config::{ + CA, + SiteConfig, + }, + structs::{ + Arguments, + ProcessorArgs, + }, }, utils::prefix_emails, }; @@ -28,7 +33,6 @@ use acme2_eab::{ }; use async_scoped::TokioScope; use clap::Parser; -use config::CA; use env_logger::init as log_init; use libsystemd::daemon; use log::*; @@ -39,7 +43,10 @@ use openssl::{ Private, }, }; -use process::process_site; +use process::{ + process_site, + services, +}; use reqwest::{ Client, tls::Version, @@ -72,9 +79,10 @@ use tokio_stream::{ StreamExt, wrappers::ReadDirStream, }; -use zbus_systemd::systemd1; - -type SafeSet = Mutex>; +use types::{ + config::GeneralConfig, + traits::FromFile as _, +}; fn default_client() -> reqwest::Client { @@ -201,13 +209,12 @@ async fn process_accounts( async fn racme(flags: Arguments) { let client = default_client(); let systemd_access = daemon::booted(); - let mainconfig = - config::read_config::(match_error!(FILE_MODE.open(flags.config).await=>Err(error)-> "error reading the config: {error}")).await; + let mainconfig = GeneralConfig::from_file(match_error!(FILE_MODE.open(flags.config).await=>Err(error)-> "error reading the config: {error}")).await; trace!("Parsed Config: {mainconfig:?}"); let files = ReadDirStream::new(match_error!(read_dir(mainconfig.sites_path.clone()).await=>Err(error)-> "could not read files from sites dir: {error}")); let mut siteconfigs = Vec::new(); for file in files.filter(Result::is_ok).map(|file| file.unwrap().path()).collect::>().await { - let mut site = config::read_config::(FILE_MODE.open(file.clone()).await.unwrap()).await; + let mut site = SiteConfig::from_file(FILE_MODE.open(file.clone()).await.unwrap()).await; site.name = file.file_stem().unwrap().to_str().unwrap().to_string(); siteconfigs.push(site); } @@ -262,24 +269,7 @@ async fn racme(flags: Arguments) { .await; if systemd_access { - let conn = match_error!(zbus_systemd::zbus::Connection::system().await=>Err(error)-> "Failed to connect with the systemd manager: {error}"); - - let systemd_manager = systemd1::ManagerProxy::new(&conn).await.unwrap(); - let restart_services = restart_services.into_inner(); - - for service in reload_services.into_inner().difference(&restart_services.clone()) { - match systemd_manager.reload_unit(service.to_owned(), "replace".to_string()).await { - Ok(_) => info!("Reloaded {service}"), - Err(error) => error!("Failed to reload service {service}: {error}"), - }; - } - - for service in restart_services.iter() { - match systemd_manager.restart_unit(service.to_owned(), "replace".to_string()).await { - Ok(_) => info!("Restarted {service}"), - Err(error) => error!("Failed to restart service {service}: {error}"), - }; - } + services(restart_services.into_inner(), reload_services.into_inner()).await; } } diff --git a/src/process.rs b/src/process.rs index 16318fa..7cfb2ff 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,14 +1,11 @@ use std::{ + collections::HashSet, fs::Permissions, os::unix::fs::PermissionsExt, path::PathBuf, }; use crate::{ - config::{ - Algorithm, - match_algo, - }, consts::{ ATTEMPTS, FILE_MODE, @@ -16,15 +13,19 @@ use crate::{ WAIT_TIME, }, load_privkey, - match_error, - structs::{ - ProcessorArgs, - San, - }, - utils::{ - gen_key, - is_matching, + macros::match_error, + types::{ + cryptography::Algorithm, + structs::{ + ProcessorArgs, + San, + }, + traits::{ + MatchAlgorithm as _, + MatchX509, + }, }, + utils::gen_key, }; use acme2_eab::{ Authorization, @@ -62,6 +63,7 @@ use tokio::{ AsyncWriteExt, }, }; +use zbus_systemd::systemd1; fn gen_stack(args: &ProcessorArgs, context: X509v3Context) -> Stack { let mut stack = Stack::new().unwrap(); @@ -104,7 +106,7 @@ pub async fn process_site(args: ProcessorArgs<'_>) { private_key = match_error!(gen_key(args.algorithm(), args.strength())=>Err(error)-> "Aborting processing the site due to problem with the certificate generation: {error}"); } else if let Ok(key) = load_privkey(private_key_file.clone()).await { private_key = key; - if !match_algo(&private_key, args.algorithm(), args.strength()) { + if !private_key.matches(args.algorithm(), args.strength()) { info!("Algorithm for the private key has changed, updating the key"); cert_renew = true; write_pkey = true; @@ -133,7 +135,10 @@ pub async fn process_site(args: ProcessorArgs<'_>) { Ok(key) => key, Err(_) => todo!(), }; - if !is_matching(pubkey, args.refresh_time(), args.san()) { + if !pubkey.days_left(args.refresh_time()) { + info!("Certificate is running out of time"); + cert_renew = true + } else if !pubkey.match_san(args.san()) { info!("Subject Alternative Names differ from Certifcate"); cert_renew = true; }; @@ -249,3 +254,23 @@ pub async fn process_auth(auth: Authorization, challenge_dir: Option, d } error!("Cannot prove the challenges: {}", auth.challenges.iter().map(|c| c.r#type.clone()).collect::>().join(", ")) } + +pub async fn services(restart_services: HashSet, reload_services: HashSet) { + let conn = match_error!(zbus_systemd::zbus::Connection::system().await=>Err(error)-> "Failed to connect with the systemd manager: {error}"); + + let systemd_manager = systemd1::ManagerProxy::new(&conn).await.unwrap(); + + for service in reload_services.difference(&restart_services.clone()) { + match systemd_manager.reload_unit(service.to_owned(), "replace".to_string()).await { + Ok(_) => info!("Reloaded {service}"), + Err(error) => error!("Failed to reload service {service}: {error}"), + }; + } + + for service in restart_services.iter() { + match systemd_manager.restart_unit(service.to_owned(), "replace".to_string()).await { + Ok(_) => info!("Restarted {service}"), + Err(error) => error!("Failed to restart service {service}: {error}"), + }; + } +} diff --git a/src/config.rs b/src/types/config.rs similarity index 53% rename from src/config.rs rename to src/types/config.rs index a64f410..7f66f7c 100644 --- a/src/config.rs +++ b/src/types/config.rs @@ -1,37 +1,28 @@ use crate::{ - consts::{ - BRAINPOOL_MIDDLE, - BRAINPOOL_STRONG, - BRAINPOOL_WEAK, - SECP_MIDDLE, - SECP_STRONG, - SECP_WEAK, + macros::{ + DefDer, + match_error, + }, + types::{ + VString, + cryptography::{ + Algorithm, + Strength, + }, + structs::Error, }, - macros::DefDer, - match_error, - structs::Error, }; -use log::*; + use macro_rules_attribute::macro_rules_derive; use openssl::pkey::{ - Id, PKey, Private, }; -use serde::{ - Deserialize, - de::DeserializeOwned, -}; +use serde::Deserialize; use std::{ collections::HashMap, net::IpAddr, }; -use tokio::{ - fs::File, - io::AsyncReadExt, -}; - -type VString = Vec; #[macro_rules_derive(DefDer)] #[derive(Deserialize)] @@ -50,48 +41,34 @@ pub struct GeneralConfig { pub ca: HashMap, } -impl Default for GeneralConfig { - #[inline] - fn default() -> Self { - Self { - accounts_path: Self::default_accounts(), - sites_path: Self::default_sites(), - http_challenge_path: Self::default_challenge(), - dns: Self::default_dns(), - certificates_path: Self::default_certificates(), - ca: Self::default_cas(), - } - } -} - impl GeneralConfig { #[inline] - fn default_accounts() -> String { + pub(super) fn default_accounts() -> String { "accounts".into() } #[inline] - fn default_sites() -> String { + pub(super) fn default_sites() -> String { "sites".into() } #[inline] - fn default_challenge() -> Option { + pub(super) fn default_challenge() -> Option { None } #[inline] - fn default_dns() -> Option { + pub(super) fn default_dns() -> Option { None } #[inline] - fn default_cas() -> HashMap { + pub(super) fn default_cas() -> HashMap { HashMap::new() } #[inline] - fn default_certificates() -> String { + pub(super) fn default_certificates() -> String { "certificates".into() } } @@ -143,52 +120,6 @@ impl CA { } } -#[macro_rules_derive(DefDer)] -#[derive(Copy, Deserialize, Default)] -pub enum Algorithm { - Rsa, - Brainpool, - Secp, - #[default] - ED25519, -} - -#[macro_rules_derive(DefDer)] -#[derive(Copy, Deserialize, Default)] -pub enum Strength { - Weak, - Middle, - #[default] - Strong, -} - -impl Strength { - pub fn rsabits(self) -> u32 { - self as u32 - } -} - -pub fn match_algo(key: &PKey, algorithm: Algorithm, strength: Strength) -> bool { - match (key.id(), algorithm) { - (Id::ED25519, Algorithm::ED25519) => true, - (Id::RSA, Algorithm::Rsa) if key.bits() == strength.rsabits() => true, - (Id::EC, Algorithm::Secp) | (Id::EC, Algorithm::Brainpool) => { - let pkey = key.ec_key().unwrap(); - let curve = pkey.group().curve_name().unwrap(); - match (algorithm, strength) { - (Algorithm::Secp, Strength::Weak) if SECP_WEAK == curve => true, - (Algorithm::Secp, Strength::Middle) if SECP_MIDDLE == curve => true, - (Algorithm::Secp, Strength::Strong) if SECP_STRONG == curve => true, - (Algorithm::Brainpool, Strength::Weak) if BRAINPOOL_WEAK == curve => true, - (Algorithm::Brainpool, Strength::Middle) if BRAINPOOL_MIDDLE == curve => true, - (Algorithm::Brainpool, Strength::Strong) if BRAINPOOL_STRONG == curve => true, - _ => false, - } - }, - _ => false, - } -} - #[macro_rules_derive(DefDer)] #[derive(Deserialize, Default)] pub struct SiteConfig { @@ -229,25 +160,3 @@ pub struct SiteConfig { #[serde(skip)] pub name: String, } - -pub async fn read_config(mut file: File) -> T { - let mut data = String::new(); - - match file.read_to_string(&mut data).await { - Ok(_) => {}, - Err(error) => { - warn!("Failed to load config: {error}"); - - return Default::default(); - }, - } - - match toml::from_str(&data) { - Ok(output) => output, - Err(error) => { - warn!("Failed to parse toml file: {error}"); - - Default::default() - }, - } -} diff --git a/src/types/cryptography.rs b/src/types/cryptography.rs new file mode 100644 index 0000000..7fc4540 --- /dev/null +++ b/src/types/cryptography.rs @@ -0,0 +1,31 @@ +use macro_rules_attribute::macro_rules_derive; +use serde::Deserialize; + +use crate::macros::DefDer; + + +#[macro_rules_derive(DefDer)] +#[derive(Copy, Deserialize, Default)] +pub enum Algorithm { + Rsa, + Brainpool, + Secp, + #[default] + ED25519, +} + + +#[macro_rules_derive(DefDer)] +#[derive(Copy, Deserialize, Default)] +pub enum Strength { + Weak, + Middle, + #[default] + Strong, +} + +impl Strength { + pub fn rsabits(self) -> u32 { + self as u32 + } +} diff --git a/src/types/foreign_impl.rs b/src/types/foreign_impl.rs new file mode 100644 index 0000000..bb3cca0 --- /dev/null +++ b/src/types/foreign_impl.rs @@ -0,0 +1,83 @@ +use std::{ + fmt::Display, + net::IpAddr, +}; + +use acme2_eab::Identifier; +use openssl::x509::GeneralName; + +use super::{ + config::GeneralConfig, + structs::{ + Error, + San, + }, +}; + + +impl Default for GeneralConfig { + #[inline] + fn default() -> Self { + Self { + accounts_path: Self::default_accounts(), + sites_path: Self::default_sites(), + http_challenge_path: Self::default_challenge(), + dns: Self::default_dns(), + certificates_path: Self::default_certificates(), + ca: Self::default_cas(), + } + } +} + +impl From for San { + fn from(value: GeneralName) -> Self { + if let Some(dns) = value.dnsname() { + return Self::Dns(dns.to_owned()); + } + if let Some(ipaddr) = value.ipaddress() { + if ipaddr.len() == 4 { + let mut addr = [0u8; 4]; + addr.copy_from_slice(ipaddr); + return Self::IPAddress(IpAddr::from(addr)); + } else { + let mut addr = [0u8; 16]; + addr.copy_from_slice(ipaddr); + return Self::IPAddress(IpAddr::from(addr)); + } + } + if let Some(email) = value.email() { + return Self::Email(email.to_owned()); + } + unreachable!(); + } +} +impl From for Identifier { + fn from(value: San) -> Self { + match value { + San::Dns(domain) => { + Identifier { + r#type: "dns".into(), + value: domain, + } + }, + San::Email(email) => { + Identifier { + r#type: "email".into(), + value: email, + } + }, + San::IPAddress(ip) => { + Identifier { + r#type: "ip".into(), + value: ip.to_string(), + } + }, + } + } +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..6a26497 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,14 @@ +use std::collections::HashSet; +use tokio::sync::Mutex; + +pub mod config; +pub mod cryptography; +mod foreign_impl; +pub mod structs; +pub mod traits; + +/// Alias for Vec\ +pub type VString = Vec; + +/// Alias for an Safe Hashset +pub type SafeSet = Mutex>; diff --git a/src/structs.rs b/src/types/structs.rs similarity index 64% rename from src/structs.rs rename to src/types/structs.rs index 2ca4801..64be2d2 100644 --- a/src/structs.rs +++ b/src/types/structs.rs @@ -1,32 +1,29 @@ use std::{ collections::HashSet, - fmt::Display, net::IpAddr, path::PathBuf, sync::Arc, }; -use acme2_eab::{ - Account, - Identifier, -}; +use acme2_eab::Account; use clap::Parser; use macro_rules_attribute::macro_rules_derive; -use openssl::x509::GeneralName; use tokio::sync::MutexGuard; use crate::{ - SafeSet, - config::{ - Algorithm, - SiteConfig, - Strength, - }, + config::SiteConfig, macros::{ DefDer, Hashable, attr_function, }, + types::{ + SafeSet, + cryptography::{ + Algorithm, + Strength, + }, + }, }; @@ -129,56 +126,8 @@ pub enum San { IPAddress(IpAddr), } -impl From for San { - fn from(value: GeneralName) -> Self { - if let Some(dns) = value.dnsname() { - return Self::Dns(dns.to_owned()); - } - if let Some(ipaddr) = value.ipaddress() { - if ipaddr.len() == 4 { - let mut addr = [0u8; 4]; - addr.copy_from_slice(ipaddr); - return Self::IPAddress(IpAddr::from(addr)); - } else { - let mut addr = [0u8; 16]; - addr.copy_from_slice(ipaddr); - return Self::IPAddress(IpAddr::from(addr)); - } - } - if let Some(email) = value.email() { - return Self::Email(email.to_owned()); - } - unreachable!(); - } -} - -impl From for Identifier { - fn from(value: San) -> Self { - match value { - San::Dns(domain) => { - Identifier { - r#type: "dns".into(), - value: domain, - } - }, - San::Email(email) => { - Identifier { - r#type: "email".into(), - value: email, - } - }, - San::IPAddress(ip) => { - Identifier { - r#type: "ip".into(), - value: ip.to_string(), - } - }, - } - } -} - #[macro_rules_derive(DefDer)] -pub struct Error(String); +pub struct Error(pub(super) String); impl Error { #[inline] @@ -191,9 +140,3 @@ impl Error { Self(message) } } - -impl Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.0) - } -} diff --git a/src/types/traits.rs b/src/types/traits.rs new file mode 100644 index 0000000..410a290 --- /dev/null +++ b/src/types/traits.rs @@ -0,0 +1,117 @@ +use std::collections::HashSet; + +use crate::consts::{ + BRAINPOOL_MIDDLE, + BRAINPOOL_STRONG, + BRAINPOOL_WEAK, + SECP_MIDDLE, + SECP_STRONG, + SECP_WEAK, +}; +use log::*; +use openssl::{ + asn1::Asn1Time, + pkey::{ + Id, + PKey, + Private, + }, + x509::X509, +}; +use serde::de::DeserializeOwned; +use tokio::{ + fs::File, + io::AsyncReadExt, +}; + +use super::{ + cryptography::{ + Algorithm, + Strength, + }, + structs::San, +}; + +pub trait FromFile: Default + DeserializeOwned { + async fn from_file(file: File) -> Self; +} + +impl FromFile for T { + async fn from_file(mut file: File) -> Self { + let mut data = String::new(); + + match file.read_to_string(&mut data).await { + Ok(_) => {}, + Err(error) => { + warn!("Failed to load config: {error}"); + + return Default::default(); + }, + } + + match toml::from_str(&data) { + Ok(output) => output, + Err(error) => { + warn!("Failed to parse toml file: {error}"); + + Default::default() + }, + } + } +} + + +pub trait MatchAlgorithm { + fn matches(&self, algorithm: Algorithm, strength: Strength) -> bool; +} + +impl MatchAlgorithm for PKey { + fn matches(&self, algorithm: Algorithm, strength: Strength) -> bool { + match (self.id(), algorithm) { + (Id::ED25519, Algorithm::ED25519) => true, + (Id::RSA, Algorithm::Rsa) if self.bits() == strength.rsabits() => true, + (Id::EC, Algorithm::Secp) | (Id::EC, Algorithm::Brainpool) => { + let pkey = self.ec_key().unwrap(); + let curve = pkey.group().curve_name().unwrap(); + match (algorithm, strength) { + (Algorithm::Secp, Strength::Weak) if SECP_WEAK == curve => true, + (Algorithm::Secp, Strength::Middle) if SECP_MIDDLE == curve => true, + (Algorithm::Secp, Strength::Strong) if SECP_STRONG == curve => true, + (Algorithm::Brainpool, Strength::Weak) if BRAINPOOL_WEAK == curve => true, + (Algorithm::Brainpool, Strength::Middle) if BRAINPOOL_MIDDLE == curve => true, + (Algorithm::Brainpool, Strength::Strong) if BRAINPOOL_STRONG == curve => true, + _ => false, + } + }, + _ => false, + } + } +} + +pub trait MatchX509 { + fn days_left(&self, days: u32) -> bool; + fn match_san>(&self, names: T) -> bool; +} + +impl MatchX509 for X509 { + fn days_left(&self, days: u32) -> bool { + let future_date = Asn1Time::days_from_now(days).unwrap(); + self.not_after().compare(&future_date).is_ok_and(|order| order.is_le()) + } + + fn match_san>(&self, names: T) -> bool { + let alt_names = match self.subject_alt_names() { + None => return false, + Some(x) => x, + }; + let mut cert_san = HashSet::::new(); + for san in alt_names { + cert_san.insert(san.into()); + } + let mut config_san = HashSet::new(); + for san in names { + config_san.insert(san); + } + config_san.difference(&cert_san).count() == 0 + } +} diff --git a/src/utils.rs b/src/utils.rs index fc4d7ff..f7f7ed4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,4 @@ -use std::collections::HashSet; - use crate::{ - config::{ - Algorithm, - Strength, - }, consts::{ BRAINPOOL_MIDDLE, BRAINPOOL_STRONG, @@ -13,11 +7,13 @@ use crate::{ SECP_STRONG, SECP_WEAK, }, - structs::San, + types::cryptography::{ + Algorithm, + Strength, + }, }; use log::*; use openssl::{ - asn1::Asn1Time, ec::EcKey, error::ErrorStack, nid::Nid, @@ -27,7 +23,6 @@ use openssl::{ }, rsa::Rsa, x509::{ - X509, X509Name, X509NameBuilder, }, @@ -94,27 +89,6 @@ pub fn gen_key(algorithm: Algorithm, strength: Strength) -> Result } } - -pub fn is_matching(cert: X509, daydiff: u32, sans: Vec) -> bool { - let now = Asn1Time::days_from_now(daydiff).unwrap(); - if cert.not_after().compare(&now).is_ok_and(|order| order.is_le()) { - return false; - } - let alt_names = match cert.subject_alt_names() { - None => return false, - Some(x) => x, - }; - let mut cert_san = HashSet::::new(); - for san in alt_names { - cert_san.insert(san.into()); - } - let mut config_san = HashSet::with_capacity(sans.len()); - for san in sans { - config_san.insert(san); - } - config_san.difference(&cert_san).count() == 0 -} - pub fn string_to_cn(name: String) -> X509Name { let mut builder = X509NameBuilder::new().unwrap(); builder.append_entry_by_nid(Nid::COMMONNAME, &name).unwrap();