diff --git a/src/cli/cli_args.rs b/src/cli/cli_args.rs index a20d589..718262d 100644 --- a/src/cli/cli_args.rs +++ b/src/cli/cli_args.rs @@ -33,6 +33,7 @@ pub enum HuskSubcommands { Daemon(DaemonCommand), Introducer(IntroducerCommand), Locals(LocalsCommand), + Signkey(SignkeyCommand), } // Daemon subcommands @@ -76,6 +77,9 @@ pub struct IntroducerCommand { pub enum IntroducerSubcommand { Add(IntroducerAddCommand), Remove(IntroducerRemoveCommand), + #[clap( + about = "List introducers." + )] List } @@ -88,7 +92,8 @@ pub struct IntroducerAddCommand { #[clap( long = "cert", help = "Certificate to declare as introducer.", - conflicts_with = "cert_file" + conflicts_with = "cert_file", + value_name = "FINGERPRINT", )] pub cert: Option, @@ -118,7 +123,8 @@ pub struct IntroducerAddCommand { pub struct IntroducerRemoveCommand { #[clap( long = "cert", - help = "Certificate to be removed as introducer." + help = "Certificate to be removed as introducer.", + value_name = "FINGERPRINT", )] pub cert: String, @@ -149,6 +155,9 @@ pub struct LocalsCommand { pub enum LocalsSubcommand { Add(LocalsAddCommand), Remove(LocalsRemoveCommand), + #[clap( + about = "List locals." + )] List } @@ -174,8 +183,59 @@ pub struct LocalsAddCommand { pub struct LocalsRemoveCommand { #[clap( long = "cert", - help = "Certificate to be removed." + help = "Certificate to be removed.", + value_name = "FINGERPRINT", )] pub cert: String, } +// Signkey subcommands + +#[derive(Parser, Debug)] +#[clap( + name = "signkey", + about = "Manage the signing key.", + subcommand_required = true, + arg_required_else_help = true, + disable_colored_help = true, + disable_version_flag = true, +)] +pub struct SignkeyCommand { + #[clap(subcommand)] + pub subcommand: SignkeySubcommand, +} +#[derive(Debug, Subcommand)] +pub enum SignkeySubcommand { + Set(SignkeySetCommand), + #[clap( + about = "Show the fingerprint of the signing key." + )] + Show, + #[clap( + about = "Print the certificate of the signing key." + )] + Dump, +} + +#[derive(Parser, Debug)] +#[clap( + about = "Set a new signing key.", +)] +pub struct SignkeySetCommand { + #[clap( + long, + help = "Load a new signing key from .", + value_name = "FILE", + conflicts_with = "cert" + )] + pub cert_file: Option, + + #[clap( + long = "cert", + help = "Certificate to declare as signing key.", + conflicts_with = "cert_file", + value_name = "FINGERPRINT", + )] + pub cert: Option, +} + diff --git a/src/commands.rs b/src/commands.rs index 333cafb..4c1ca7f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -9,6 +9,7 @@ use crate::config::HuskConfigContainer; pub mod daemon; pub mod introducer; pub mod locals; +pub mod signkey; #[derive(thiserror::Error, Debug)] pub enum CommandError { @@ -34,6 +35,9 @@ pub async fn dispatch(cli: CliArgs, config: HuskConfigContainer) -> Result<()> { HuskSubcommands::Locals(subcmd) => { commands::locals::dispatch(subcmd, config).await?; }, + HuskSubcommands::Signkey(subcmd) => { + commands::signkey::dispatch(subcmd, config).await?; + } } Ok(()) diff --git a/src/commands/locals/add.rs b/src/commands/locals/add.rs index 652c087..37193f9 100644 --- a/src/commands/locals/add.rs +++ b/src/commands/locals/add.rs @@ -37,7 +37,6 @@ pub async fn dispatch(cmd: LocalsAddCommand, config: HuskConfigContainer) -> Res &vc, crypto::Role::Local, true, - None, None ) } diff --git a/src/commands/locals/remove.rs b/src/commands/locals/remove.rs index a7addfb..85ac4f8 100644 --- a/src/commands/locals/remove.rs +++ b/src/commands/locals/remove.rs @@ -41,7 +41,6 @@ pub async fn dispatch(cmd: LocalsRemoveCommand, config: HuskConfigContainer) -> cert, crypto::Role::Local, false, - None, None) } else { Err(CommandError::CertNotUsable(fpr).into()) diff --git a/src/commands/signkey.rs b/src/commands/signkey.rs new file mode 100644 index 0000000..c9f971f --- /dev/null +++ b/src/commands/signkey.rs @@ -0,0 +1,37 @@ +//! signkey subcommand + +use anyhow::Result; +use sequoia_openpgp::serialize::SerializeInto; + +use crate::cli::cli_args::{SignkeyCommand, SignkeySubcommand}; +use crate::config::HuskConfigContainer; +use crate::types::husk_context::HuskContext; + +pub mod set; + +pub async fn dispatch(cmd: SignkeyCommand, config: HuskConfigContainer) -> Result<()> { + match cmd.subcommand { + SignkeySubcommand::Set(set_cmd) => { + set::dispatch(set_cmd, config).await?; + }, + SignkeySubcommand::Show => { + let context = HuskContext::new(&config.into())?; + if let Some(key) = context.signing_key { + println!("Fingerprint: {}", key.fingerprint()); + } else { + println!("No active signing key available."); + } + }, + SignkeySubcommand::Dump => { + let context = HuskContext::new(&config.into())?; + if let Some(key) = context.signing_key { + let armored = key.armored().to_vec()?; + println!("{}", String::from_utf8(armored)?); + } else { + println!("No active signing key available."); + } + } + } + + Ok(()) +} diff --git a/src/commands/signkey/set.rs b/src/commands/signkey/set.rs new file mode 100644 index 0000000..f79f36b --- /dev/null +++ b/src/commands/signkey/set.rs @@ -0,0 +1,67 @@ +//! signkey set command + +use anyhow::Result; +use sequoia_cert_store::Store; +use sequoia_openpgp::cert::ValidCert; +use sequoia_openpgp::cert; +use sequoia_openpgp::parse::Parse; +use sequoia_openpgp::{Fingerprint, KeyHandle}; +use sequoia_openpgp::types::RevocationStatus; + +use crate::cli::cli_args::SignkeySetCommand; +use crate::commands::CommandError; +use crate::common::crypto; +use crate::config::HuskConfigContainer; +use crate::types::husk_context::HuskContext; + +pub async fn dispatch(cmd: SignkeySetCommand, config: HuskConfigContainer) -> Result<()> { + let context = HuskContext::new(&config.into())?; + let policy = &context.policy; + let cert_store = &context.cert_store; + + if let Some(cert_file) = cmd.cert_file { + let c = cert::Cert::from_file(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_sign(&vc) { + return Err(CommandError::CertNotUsable(vc.fingerprint()).into()); + } + + // set the signing key + crypto::set_signing_key( + &context, + &vc + )?; + + } else if let Some(cert) = cmd.cert { + let fpr = Fingerprint::from_hex(cert.as_str())?; + let certs = cert_store.lookup_by_cert(&KeyHandle::from(&fpr)) + .map_err(|_| CommandError::CertNotFound(fpr.clone()))?; + + let certs: Vec = certs.iter() + .filter_map(|c| + c.with_policy(policy, None).ok() + ) + .filter(|vc| crypto::can_sign(vc)) + .collect(); + + if let Some(cert) = certs.first() { + crypto::set_signing_key( + &context, + cert + )?; + } else { + return Err(CommandError::CertNotUsable(fpr).into()); + } + } + + Ok(()) +} + diff --git a/src/common/crypto.rs b/src/common/crypto.rs index 40f746e..c5b8dad 100644 --- a/src/common/crypto.rs +++ b/src/common/crypto.rs @@ -37,6 +37,10 @@ use crate::types::recipient::Recipient; use crate::mail; use crate::common::escape; +/// Signing keys used by Husk must have a certification with a +/// notation of the name `HUSK_SIG_NOTATION`. +const HUSK_SIG_NOTATION: &str = "signing_key@husk-project.org"; + /// Returns the local trust root from the cert_store. If the trust root /// cannot be found, it returns an error. pub fn get_local_trust_root(cert_store: &CertStore) -> anyhow::Result { @@ -420,7 +424,7 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec, body: &[u8], recipients: &Vec>, notation: Option<(String, String)>) +pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activate: bool, domains: Option>) -> anyhow::Result<()> { + internal_authenticate(context, cert, role, activate, domains, None) +} + +/// Make `cert` the new signing key. +pub fn set_signing_key(context: &HuskContext, cert: &ValidCert) -> anyhow::Result<()> { + internal_authenticate(context, cert, Role::SigningKey, true, None, Some((HUSK_SIG_NOTATION.to_string(), "placeholder".to_string()))) +} + +/// Internal authentication method. +fn internal_authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activate: bool, domains: Option>, notation: Option<(String, String)>) -> anyhow::Result<()> { let policy = &context.policy; @@ -475,7 +490,7 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activat if activate { // set the trust level and depth for an active local/introducer. - if role == Role::Local { + if role == Role::Local || role == Role::SigningKey { builder = builder.set_trust_signature(0, 120)?; } else { // Introducer @@ -572,9 +587,16 @@ pub fn can_encrypt(cert: &ValidCert) -> bool { .count() > 0 } -/// Signing keys used by Husk must have a certification with a -/// notation of the name `HUSK_SIG_NOTATION`. -pub const HUSK_SIG_NOTATION: &str = "signing_key@husk-project.org"; +/// Returns true if `cert` can be used for signing. +pub fn can_sign(cert: &ValidCert) -> bool { + cert.keys() + .for_signing() + .alive() + .supported() + .revoked(false) + .count() > 0 +} + /// Get Husks signing key pub fn get_signing_key(sequoia_home: Home, cert_store: &CertStore<'_>, policy: &StandardPolicy<'_>, local_trust_root: &Cert) -> anyhow::Result> { @@ -609,10 +631,10 @@ pub fn get_signing_key(sequoia_home: Home, cert_store: &CertStore<'_>, policy: & } } -/// If `vc` is a certificate marked as a Husk signing key -/// key, return the creation time of the signature - else return `None`. +/// If `vc` is a certificate marked as a Husk signing key, return the +/// creation time of the signature - else return `None`. pub fn contains_signing_key(vc: &ValidCert, local_root_fpr: &Fingerprint) -> Option { - for uid in vc.userids().into_iter() { + for uid in vc.userids() { for sig in uid.certifications() { if sig.issuer_fingerprints().any(|i| i == local_root_fpr) && sig.notation(HUSK_SIG_NOTATION).count() > 0 { @@ -628,6 +650,7 @@ pub fn contains_signing_key(vc: &ValidCert, local_root_fpr: &Fingerprint) -> Opt } /// Get all keys from all backends +// XXX: Maybe the softkeys backend is sufficient pub fn get_all_keys(sequoia_home: Home) -> Option> { let mut result = Vec::new(); @@ -669,10 +692,10 @@ pub mod tests { use sequoia_openpgp::{cert::CertBuilder, policy::StandardPolicy}; use std::sync::Arc; - use crate::common::crypto::{HUSK_SIG_NOTATION, get_all_keys}; + use crate::common::crypto::get_all_keys; use crate::types::{husk_context::HuskContext, mail_context::MailContext}; - use super::{authenticate, get_signing_key}; + use super::get_signing_key; #[test] pub fn test_is_encrypted() { @@ -815,12 +838,7 @@ jk6lh01Ej9QTGQ== let keys= get_all_keys(home.clone()); eprintln!("{:?}", keys); - authenticate(&context, - &valid_signing_cert, - super::Role::Local, - true, - None, - Some((HUSK_SIG_NOTATION.to_string(), "dummy".to_string())))?; + super::set_signing_key(&context, &valid_signing_cert)?; // get the Husk signing key let key = get_signing_key(home, diff --git a/src/types/introducer.rs b/src/types/introducer.rs index 142c7f3..d44e82c 100644 --- a/src/types/introducer.rs +++ b/src/types/introducer.rs @@ -83,8 +83,7 @@ impl Introducer { cert, crypto::Role::Introducer, true, - Some(domains), - None) + Some(domains)) } /// Remove an introducer. @@ -102,7 +101,6 @@ impl Introducer { cert, crypto::Role::Introducer, false, - None, None) } }