diff --git a/src/cli/cli_args.rs b/src/cli/cli_args.rs index f437203..a20d589 100644 --- a/src/cli/cli_args.rs +++ b/src/cli/cli_args.rs @@ -94,8 +94,8 @@ pub struct IntroducerAddCommand { #[clap( long = "cert-file", - help = "Import certificate from CERT_FILE and declare it as introducer.", - value_name = "CERT_FILE", + help = "Import certificate from and declare it as introducer.", + value_name = "FILE", conflicts_with = "cert" )] pub cert_file: Option, @@ -159,8 +159,9 @@ pub enum LocalsSubcommand { )] pub struct LocalsAddCommand { #[clap( - long = "cert", - help = "Certificate to add." + long, + help = "Import a certificate from .", + value_name = "FILE", )] pub cert_file: String, } @@ -168,12 +169,12 @@ pub struct LocalsAddCommand { #[derive(Parser, Debug)] #[clap( name = "locals_remove", - about = "Certificate to remove.", + about = "Disable a Certificate.", )] pub struct LocalsRemoveCommand { #[clap( long = "cert", - help = "Certificate to removed." + help = "Certificate to be removed." )] pub cert: String, } diff --git a/src/commands/introducer.rs b/src/commands/introducer.rs index 269a17a..927d96c 100644 --- a/src/commands/introducer.rs +++ b/src/commands/introducer.rs @@ -1,3 +1,4 @@ +//! introducer subcommand use anyhow::Result; use crate::cli::cli_args::{IntroducerCommand, IntroducerSubcommand}; diff --git a/src/commands/introducer/add.rs b/src/commands/introducer/add.rs index 9f7d63a..113b91b 100644 --- a/src/commands/introducer/add.rs +++ b/src/commands/introducer/add.rs @@ -31,14 +31,14 @@ pub async fn dispatch(cmd: IntroducerAddCommand, config: HuskConfigContainer) -> if matches!(vc.revocation_status(), RevocationStatus::Revoked(_)) { return Err(CommandError::CertRevoked(vc.fingerprint()).into()); } - if !crypto::has_certification_capability(&vc) { + if !crypto::can_certify(&vc) { return Err(CommandError::CertNotUsable(vc.fingerprint()).into()); } Introducer::create(&context, &vc, cmd.domains)?; } else if let Some(cert) = cmd.cert { let fpr = Fingerprint::from_hex(cert.as_str())?; - let certs = cert_store.lookup_by_cert(&KeyHandle::try_from(&fpr)?) + let certs = cert_store.lookup_by_cert(&KeyHandle::from(&fpr)) .map_err(|_| CommandError::CertNotFound(fpr.clone()))?; let certs: Vec = certs.iter() @@ -48,7 +48,7 @@ pub async fn dispatch(cmd: IntroducerAddCommand, config: HuskConfigContainer) -> .collect(); if let Some(cert) = certs.first() { - Introducer::create(&context, &cert, cmd.domains)?; + Introducer::create(&context, cert, cmd.domains)?; } else { return Err(CommandError::CertNotUsable(fpr).into()); } diff --git a/src/commands/introducer/remove.rs b/src/commands/introducer/remove.rs index 3383680..859089a 100644 --- a/src/commands/introducer/remove.rs +++ b/src/commands/introducer/remove.rs @@ -19,7 +19,7 @@ pub async fn dispatch(cmd: IntroducerRemoveCommand, config: HuskConfigContainer) // Get the certificate let fpr = Fingerprint::from_hex(&cmd.cert)?; - let certs = cert_store.lookup_by_cert(&KeyHandle::try_from(&fpr)?) + let certs = cert_store.lookup_by_cert(&KeyHandle::from(&fpr)) .map_err(|_| CommandError::CertNotFound(fpr.clone()))?; // Filter out valid certificates. If the certificate is not @@ -35,8 +35,6 @@ pub async fn dispatch(cmd: IntroducerRemoveCommand, config: HuskConfigContainer) } else { return Err(CommandError::CertNotUsable(fpr).into()); } - - Ok(()) } diff --git a/src/commands/locals.rs b/src/commands/locals.rs index fafd74c..dc51ed2 100644 --- a/src/commands/locals.rs +++ b/src/commands/locals.rs @@ -1,3 +1,4 @@ +//! locals subcommand use anyhow::Result; use crate::cli::cli_args::{LocalsCommand, LocalsSubcommand}; @@ -11,10 +12,10 @@ pub mod remove; pub async fn dispatch(cmd: LocalsCommand, config: HuskConfigContainer) -> Result<()> { match cmd.subcommand { LocalsSubcommand::Add(add_cmd) => { - add::dispatch(add_cmd, &config).await?; + add::dispatch(add_cmd, config).await?; }, LocalsSubcommand::Remove(rm_cmd) => { - remove::dispatch(rm_cmd, &config).await?; + remove::dispatch(rm_cmd, config).await?; }, LocalsSubcommand::List => { let context = HuskContext::new(&config.into())?; diff --git a/src/commands/locals/add.rs b/src/commands/locals/add.rs index bc88bae..780c050 100644 --- a/src/commands/locals/add.rs +++ b/src/commands/locals/add.rs @@ -1,12 +1,45 @@ use anyhow::Result; +use sequoia_openpgp::cert; +use sequoia_openpgp::parse::Parse; +use sequoia_openpgp::types::RevocationStatus; + use crate::cli::cli_args::LocalsAddCommand; use crate::config::HuskConfigContainer; +use crate::types::husk_context::HuskContext; +use crate::commands::CommandError; +use crate::common::crypto; -pub async fn dispatch(cmd: LocalsAddCommand, _config: &HuskConfigContainer) -> Result<()> { +pub async fn dispatch(cmd: LocalsAddCommand, config: HuskConfigContainer) -> Result<()> { println!("add"); println!(" {:?}", cmd.cert_file); - Ok(()) + + let context = HuskContext::new(&config.into())?; + let policy = &context.policy; + + let c = cert::Cert::from_file(cmd.cert_file)?; + let vc = c.with_policy(policy, None)?; + + // guards + if vc.alive().is_err() { + return Err(CommandError::CertNotAlive(vc.fingerprint()).into()); + } + if matches!(vc.revocation_status(), RevocationStatus::Revoked(_)) { + return Err(CommandError::CertRevoked(vc.fingerprint()).into()); + } + if !crypto::can_encrypt(&vc) { + return Err(CommandError::CertNotUsable(vc.fingerprint()).into()); + } + if vc.fingerprint() == context.local_trust_root.fingerprint() { + // XXX: find a better error + return Err(CommandError::CertNotUsable(vc.fingerprint()).into()); + } + + crypto::authenticate( + &context, + &vc, + crypto::Role::Local, + true, + None + ) } - - diff --git a/src/commands/locals/remove.rs b/src/commands/locals/remove.rs index 8966055..989401a 100644 --- a/src/commands/locals/remove.rs +++ b/src/commands/locals/remove.rs @@ -1,12 +1,52 @@ +//! locals remove subcommand + use anyhow::Result; -use crate::cli::cli_args::LocalsRemoveCommand; -use crate::config::HuskConfigContainer; +use sequoia_openpgp::Fingerprint; +use sequoia_openpgp::KeyHandle; +use sequoia_openpgp::cert::ValidCert; +use sequoia_cert_store::Store; -pub async fn dispatch(cmd: LocalsRemoveCommand, _config: &HuskConfigContainer) -> Result<()> { +use crate::cli::cli_args::LocalsRemoveCommand; +use crate::common::crypto; +use crate::config::HuskConfigContainer; +use crate::commands::CommandError; +use crate::types::husk_context::HuskContext; + +pub async fn dispatch(cmd: LocalsRemoveCommand, config: HuskConfigContainer) -> Result<()> { println!("remove"); println!(" {:?}", cmd.cert); - Ok(()) + let context = HuskContext::new(&config.into())?; + let policy = &context.policy; + let cert_store = &context.cert_store; + + // Get the certificate + let fpr = Fingerprint::from_hex(&cmd.cert)?; + let certs = cert_store.lookup_by_cert(&KeyHandle::from(&fpr)) + .map_err(|_| CommandError::CertNotFound(fpr.clone()))?; + + // Filter out valid certificates. If the certificate is not + // valid according to the policy, it is already no introducer. + let certs: Vec = certs.iter() + .filter_map(|c| + c.with_policy(policy, None).ok() + ) + .collect(); + + if let Some(cert) = certs.first() { + // filter out the trust root + if cert.fingerprint() == context.local_trust_root.fingerprint() { + return Err(CommandError::CertNotUsable(fpr).into()); + } + crypto::authenticate( + &context, + cert, + crypto::Role::Local, + false, + None) + } else { + Err(CommandError::CertNotUsable(fpr).into()) + } } diff --git a/src/common/crypto.rs b/src/common/crypto.rs index 81452bb..f72326c 100644 --- a/src/common/crypto.rs +++ b/src/common/crypto.rs @@ -346,17 +346,13 @@ pub fn get_locals<'c>(cert_store: &CertStore<'c>, policy: &StandardPolicy, local for p in targets { let cert_synopsis = p.target().clone(); - match set.entry(cert_synopsis.fingerprint()) { - Vacant(_) => { - // Only consider non introduced certificates. - if !p.certifications().any(|c| - introducers.contains(&c.issuer().fingerprint())) { + if let Vacant(_) = set.entry(cert_synopsis.fingerprint()) { + // Only consider non introduced certificates. + if !p.certifications().any(|c| + introducers.contains(&c.issuer().fingerprint())) { - set.insert(cert_synopsis.fingerprint(), cert_synopsis); - } - }, - // The target is already known, do nothing. - _ => {} + set.insert(cert_synopsis.fingerprint(), cert_synopsis); + } } } set.into_values().collect() @@ -428,13 +424,20 @@ pub fn encrypt(context: &HuskContext<'_>, body: &Bytes, recipients: &Vec>) +pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activate: bool, domains: Option>) -> anyhow::Result<()> { let policy = &context.policy; @@ -465,27 +468,37 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, activate: bool, dom .set_revocable(true)?; // set no expiration date - // Add domains if activate { - // set the trust level and depth for an active introducer. - builder = builder.set_trust_signature(1, 120)?; + // set the trust level and depth for an active local/introducer. + if role == Role::Local { + builder = builder.set_trust_signature(0, 120)?; + } else { + // Introducer + builder = builder.set_trust_signature(1, 120)?; - if let Some(domains) = domains { - for domain in domains { - if !mail::is_domain(domain.as_str()) { - return Err(HuskError::InvalidDomainName(domain).into()); + // Add domains + if let Some(domains) = domains { + if domains.is_empty() { + // An introducer without domains would be unlimited. + return Err(HuskError::UnlimitedIntroducer.into()); } - builder = builder.add_regular_expression(format!("<[^>]+[@.]{}>$", - escape::regex_ctrl_escape(domain.as_str())? - ))?; + for domain in domains { + if !mail::is_domain(domain.as_str()) { + return Err(HuskError::InvalidDomainName(domain).into()); + } + + builder = builder.add_regular_expression(format!("<[^>]+[@.]{}>$", + escape::regex_ctrl_escape(domain.as_str())? + ))?; + } + } else { + // An introducer without domains would be unlimited. + return Err(HuskError::UnlimitedIntroducer.into()); } - } else { - // An introducer without domains would be unlimited. - return Err(HuskError::UnlimitedIntroducer.into()); } } else { - // deactive the introducer + // deactive the certificate builder = builder.set_trust_signature(0, 1)?; } @@ -526,7 +539,8 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, activate: bool, dom Ok(()) } -pub fn has_certification_capability(cert: &ValidCert) -> bool { +/// Returns true if `cert` can be used for certifications. +pub fn can_certify(cert: &ValidCert) -> bool { cert.keys() .for_certification() .alive() @@ -535,6 +549,17 @@ pub fn has_certification_capability(cert: &ValidCert) -> bool { .count() > 0 } +/// Returns true if `cert` can be used for encryption. +pub fn can_encrypt(cert: &ValidCert) -> bool { + cert.keys() + .for_transport_encryption() + .alive() + .supported() + .revoked(false) + .count() > 0 +} + + #[cfg(test)] pub mod tests { diff --git a/src/main.rs b/src/main.rs index 5c6c265..046a9f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ //! Husk milter -use anyhow; use std::process; use std::sync::{Arc, Mutex}; diff --git a/src/types/introducer.rs b/src/types/introducer.rs index b3f5251..6113138 100644 --- a/src/types/introducer.rs +++ b/src/types/introducer.rs @@ -42,6 +42,7 @@ impl Introducer { } } + /// Has this introducer certified cert? pub fn is_introducing(&self, cert: &Cert) -> bool { let policy = StandardPolicy::new(); @@ -66,6 +67,7 @@ impl Introducer { result } + /// Create an introducer. pub fn create(context: &HuskContext, cert: &ValidCert, domains: Vec) -> Result<()> { @@ -75,9 +77,15 @@ impl Introducer { return Err(IntroducerError::UnsuitableCertificate.into()); } - crypto::authenticate(context, cert, true, Some(domains)) + crypto::authenticate( + context, + cert, + crypto::Role::Introducer, + true, + Some(domains)) } + /// Remove an introducer. pub fn remove(context: &HuskContext, cert: &ValidCert, _demote: bool) -> Result<()> { @@ -87,7 +95,12 @@ impl Introducer { return Err(IntroducerError::UnsuitableCertificate.into()); } - crypto::authenticate(context, cert, false, None) + crypto::authenticate( + context, + cert, + crypto::Role::Introducer, + false, + None) } }