From b0009938174e198023959c5376ec9fcdcaced86c Mon Sep 17 00:00:00 2001 From: Justus Winter Date: Tue, 16 Jan 2024 17:57:37 +0100 Subject: [PATCH] Move all global PKI options to subcommands where they are needed. --- src/cli/pki.rs | 157 ++++++++++++++++++++++--------- src/cli/types.rs | 2 +- src/commands/pki.rs | 219 +++++++++++++++++++++++--------------------- tests/sq-pki.rs | 4 +- 4 files changed, 235 insertions(+), 147 deletions(-) diff --git a/src/cli/pki.rs b/src/cli/pki.rs index 76c81ac1..de17813b 100644 --- a/src/cli/pki.rs +++ b/src/cli/pki.rs @@ -36,47 +36,6 @@ necessarily institutions – act as trusted introducers. arg_required_else_help = true, )] pub struct Command { - /// Treats all certificates as unreliable trust roots. - /// - /// This option is useful for figuring out what others think about - /// a certificate (i.e., gossip or hearsay). In other words, this - /// finds arbitrary paths to a particular certificate. - /// - /// Gossip is useful in helping to identify alternative ways to - /// authenticate a certificate. For instance, imagine Ed wants to - /// authenticate Laura's certificate, but asking her directly is - /// inconvenient. Ed discovers that Micah has certified Laura's - /// certificate, but Ed hasn't yet authenticated Micah's - /// certificate. If Ed is willing to rely on Micah as a trusted - /// introducer, and authenticating Micah's certificate is easier - /// than authenticating Laura's certificate, then Ed has learned - /// about an easier way to authenticate Laura's certificate. - #[arg(global=true, display_order=850, long)] - pub gossip: bool, - - /// Treats the network as a certification network. - /// - /// Normally, `sq pki` treats the Web of Trust network as an - /// authentication network where a certification only means that - /// the binding is correct, not that the target should be treated - /// as a trusted introducer. In a certification network, the - /// targets of certifications are treated as trusted introducers - /// with infinite depth, and any regular expressions are ignored. - /// Note: The trust amount remains unchanged. This is how most - /// so-called pgp path-finding algorithms work. - #[arg(global=true, display_order=860, long)] - pub certification_network: bool, - - /// The required amount of trust. - /// - /// 120 indicates full authentication; values less than 120 - /// indicate partial authentication. When - /// `--certification-network` is passed, this defaults to 1200, - /// i.e., `sq pki` tries to find 10 paths. - #[arg(global=true, display_order=800, short='a', long="amount", - value_name = "AMOUNT")] - pub trust_amount: Option>, - #[command(subcommand)] pub subcommand: Subcommands, } @@ -126,6 +85,15 @@ pub struct AuthenticateCommand { #[command(flatten)] pub email: EmailArg, + #[command(flatten)] + pub gossip: GossipArg, + + #[command(flatten)] + pub certification_network: CertificationNetworkArg, + + #[command(flatten)] + pub trust_amount: RequiredTrustAmountArg, + #[command(flatten)] pub cert: CertArg, @@ -161,6 +129,15 @@ pub struct LookupCommand { #[command(flatten)] pub email: EmailArg, + #[command(flatten)] + pub gossip: GossipArg, + + #[command(flatten)] + pub certification_network: CertificationNetworkArg, + + #[command(flatten)] + pub trust_amount: RequiredTrustAmountArg, + #[command(flatten)] pub userid: UserIDArg, } @@ -192,6 +169,15 @@ $ sq pki identify --gossip \\ 3217C509292FC67076ECD75C7614269BDDF73B36 ")] pub struct IdentifyCommand { + #[command(flatten)] + pub gossip: GossipArg, + + #[command(flatten)] + pub certification_network: CertificationNetworkArg, + + #[command(flatten)] + pub trust_amount: RequiredTrustAmountArg, + #[command(flatten)] pub cert: CertArg, } @@ -224,6 +210,15 @@ pub struct ListCommand { #[command(flatten)] pub email: EmailArg, + #[command(flatten)] + pub gossip: GossipArg, + + #[command(flatten)] + pub certification_network: CertificationNetworkArg, + + #[command(flatten)] + pub trust_amount: RequiredTrustAmountArg, + /// A pattern to select the bindings to authenticate. /// /// The pattern is treated as a UTF-8 encoded string and a @@ -259,7 +254,13 @@ $ sq pki path \\ ")] pub struct PathCommand { #[command(flatten)] - pub email: EmailArg, + pub gossip: GossipArg, + + #[command(flatten)] + pub certification_network: CertificationNetworkArg, + + #[command(flatten)] + pub trust_amount: RequiredTrustAmountArg, // This should actually be a repeatable positional argument // (Vec) followed by a manadatory positional argument (a @@ -400,3 +401,75 @@ impl Deref for EmailArg { } } +#[derive(clap::Args, Debug)] +pub struct GossipArg { + /// Treats all certificates as unreliable trust roots. + /// + /// This option is useful for figuring out what others think about + /// a certificate (i.e., gossip or hearsay). In other words, this + /// finds arbitrary paths to a particular certificate. + /// + /// Gossip is useful in helping to identify alternative ways to + /// authenticate a certificate. For instance, imagine Ed wants to + /// authenticate Laura's certificate, but asking her directly is + /// inconvenient. Ed discovers that Micah has certified Laura's + /// certificate, but Ed hasn't yet authenticated Micah's + /// certificate. If Ed is willing to rely on Micah as a trusted + /// introducer, and authenticating Micah's certificate is easier + /// than authenticating Laura's certificate, then Ed has learned + /// about an easier way to authenticate Laura's certificate. + #[arg(long)] + pub gossip: bool, +} + +impl Deref for GossipArg { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.gossip + } +} + +#[derive(clap::Args, Debug)] +pub struct CertificationNetworkArg { + /// Treats the network as a certification network. + /// + /// Normally, `sq pki` treats the Web of Trust network as an + /// authentication network where a certification only means that + /// the binding is correct, not that the target should be treated + /// as a trusted introducer. In a certification network, the + /// targets of certifications are treated as trusted introducers + /// with infinite depth, and any regular expressions are ignored. + /// Note: The trust amount remains unchanged. This is how most + /// so-called pgp path-finding algorithms work. + #[arg(long)] + pub certification_network: bool, +} + +impl Deref for CertificationNetworkArg { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.certification_network + } +} + +#[derive(clap::Args, Debug)] +pub struct RequiredTrustAmountArg { + /// The required amount of trust. + /// + /// 120 indicates full authentication; values less than 120 + /// indicate partial authentication. When + /// `--certification-network` is passed, this defaults to 1200, + /// i.e., `sq pki` tries to find 10 paths. + #[arg(short='a', long="amount", value_name = "AMOUNT")] + pub trust_amount: Option>, +} + +impl Deref for RequiredTrustAmountArg { + type Target = Option>; + + fn deref(&self) -> &Self::Target { + &self.trust_amount + } +} diff --git a/src/cli/types.rs b/src/cli/types.rs index 8e64bf27..71c435f3 100644 --- a/src/cli/types.rs +++ b/src/cli/types.rs @@ -760,7 +760,7 @@ impl From for KeyFlags { } /// Describes the purpose of the encryption. -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub enum TrustAmount { /// Partial trust. Partial, diff --git a/src/commands/pki.rs b/src/commands/pki.rs index c18c00d7..903ed580 100644 --- a/src/commands/pki.rs +++ b/src/commands/pki.rs @@ -6,7 +6,6 @@ use openpgp::Fingerprint; use openpgp::KeyHandle; use openpgp::Result; use openpgp::packet::UserID; -use openpgp::policy::Policy; use sequoia_cert_store as cert_store; use cert_store::store::StatusListener; @@ -16,11 +15,13 @@ use cert_store::store::StoreError; use sequoia_wot as wot; use wot::store::CertStore; use wot::store::Backend; +use wot::store::Store; pub mod output; use crate::cli; use cli::output::OutputFormat; +use cli::types::TrustAmount; use crate::commands::pki as pki_cmd; use pki_cmd::output::print_path; @@ -31,13 +32,14 @@ use pki_cmd::output::OutputType as _; use crate::Config; -fn trust_amount(cli: &cli::pki::Command) +fn required_trust_amount(trust_amount: Option>, + certification_network: bool) -> Result { - let amount = if let Some(v) = &cli.trust_amount { + let amount = if let Some(v) = &trust_amount { v.amount() } else { - if cli.certification_network { + if certification_network { // Look for multiple paths. Specifically, try to find 10 // paths. 10 * wot::FULLY_TRUSTED @@ -73,18 +75,48 @@ fn have_self_signed_userid(cert: &wot::CertSynopsis, } /// Authenticate bindings defined by a Query on a Network -fn authenticate( +fn authenticate( config: &Config, - cli: &cli::pki::Command, - q: &wot::Query<'_, S>, - gossip: bool, + precompute: bool, + list_pattern: Option, email: bool, + gossip: bool, + certification_network: bool, + trust_amount: Option>, userid: Option<&UserID>, certificate: Option<&KeyHandle>, ) -> Result<()> - where S: wot::store::Store { - let required_amount = trust_amount(cli)?; + // Build the network. + let cert_store = match config.cert_store() { + Ok(Some(cert_store)) => cert_store, + Ok(None) => { + return Err(anyhow::anyhow!("Certificate store has been disabled")); + } + Err(err) => { + return Err(err).context("Opening certificate store"); + } + }; + + let mut cert_store = CertStore::from_store( + cert_store, &config.policy, config.time); + if precompute { + cert_store.precompute(); + } + + let n = wot::Network::new(cert_store)?; + + let mut q = wot::QueryBuilder::new(&n); + if ! gossip { + q.roots(wot::Roots::new(config.trust_roots())); + } + if certification_network { + q.certification_network(); + } + let q = q.build(); + + let required_amount = + required_trust_amount(trust_amount, certification_network)?; let fingerprint: Option = if let Some(kh) = certificate { Some(match kh { @@ -186,8 +218,7 @@ fn authenticate( bindings = q.network().certified_userids(); - use cli::pki::*; - if let Subcommands::List(ListCommand { pattern: Some(pattern), .. }) = &cli.subcommand { + if let Some(pattern) = list_pattern { // Or rather, just User IDs that match the pattern. let pattern = pattern.to_lowercase(); @@ -239,7 +270,7 @@ fn authenticate( required_amount, q.roots(), gossip, - cli.certification_network, + certification_network, )) as Box } @@ -413,27 +444,47 @@ fn authenticate( } // For `sq-wot path`. -fn check_path<'a: 'b, 'b, S>(config: &Config, - cli: &cli::pki::Command, - q: &wot::Query<'b, S>, - policy: &dyn Policy) +fn check_path(config: &Config, + gossip: bool, + certification_network: bool, + trust_amount: Option>, + path: cli::pki::PathArg) -> Result<()> -where S: wot::store::Store + wot::store::Backend<'a> { tracer!(TRACE, "check_path"); - let required_amount = trust_amount(cli)?; - - use cli::pki::*; - let (khs, userid) = if let Subcommands::Path(PathCommand { path, .. }) = &cli.subcommand { - (path.certs()?, path.userid()?) - } else { - unreachable!("checked"); + // Build the network. + let cert_store = match config.cert_store() { + Ok(Some(cert_store)) => cert_store, + Ok(None) => { + return Err(anyhow::anyhow!("Certificate store has been disabled")); + } + Err(err) => { + return Err(err).context("Opening certificate store"); + } }; + let cert_store = CertStore::from_store( + cert_store, &config.policy, config.time); + + let n = wot::Network::new(cert_store)?; + + let mut q = wot::QueryBuilder::new(&n); + if ! gossip { + q.roots(wot::Roots::new(config.trust_roots())); + } + if certification_network { + q.certification_network(); + } + let q = q.build(); + + let required_amount = + required_trust_amount(trust_amount, certification_network)?; + + let (khs, userid) = (path.certs()?, path.userid()?); assert!(khs.len() > 0, "guaranteed by clap"); - let r = q.lint_path(&khs, &userid, required_amount, policy); + let r = q.lint_path(&khs, &userid, required_amount, &config.policy); let target_kh = khs.last().expect("have one"); @@ -497,89 +548,53 @@ impl StatusListener for KeyServerUpdate { pub fn dispatch(config: Config, cli: cli::pki::Command) -> Result<()> { tracer!(TRACE, "pki::dispatch"); - // Build the network. - let cert_store = match config.cert_store() { - Ok(Some(cert_store)) => cert_store, - Ok(None) => { - return Err(anyhow::anyhow!("Certificate store has been disabled")); - } - Err(err) => { - return Err(err).context("Opening certificate store"); - } - }; - - let mut cert_store = CertStore::from_store( - cert_store, &config.policy, config.time); use cli::pki::*; - if let Subcommands::List(ListCommand { pattern: None, .. }) = cli.subcommand { - cert_store.precompute(); - } - - let n = wot::Network::new(cert_store)?; - - let mut q = wot::QueryBuilder::new(&n); - if ! cli.gossip { - q.roots(wot::Roots::new(config.trust_roots())); - } - if cli.certification_network { - q.certification_network(); - } - let q = q.build(); - - match &cli.subcommand { + match cli.subcommand { + // Authenticate a given binding. Subcommands::Authenticate(AuthenticateCommand { - email, - cert, - userid, - .. - }) => { - // Authenticate a given binding. - authenticate( - &config, &cli, &q, - cli.gossip, - **email, - Some(userid), Some(cert))?; - } + email, gossip, certification_network, trust_amount, + cert, userid, + }) => authenticate( + &config, false, None, + *email, *gossip, *certification_network, *trust_amount, + Some(&userid), Some(&cert))?, + + // Find all authenticated bindings for a given User ID, list + // the certificates. Subcommands::Lookup(LookupCommand { - email, + email, gossip, certification_network, trust_amount, userid, - .. - }) => { - // Find all authenticated bindings for a given - // User ID, list the certificates. - authenticate( - &config, &cli, &q, - cli.gossip, - **email, - Some(userid), None)?; - } + }) => authenticate( + &config, false, None, + *email, *gossip, *certification_network, *trust_amount, + Some(&userid), None)?, + + // Find and list all authenticated bindings for a given + // certificate. Subcommands::Identify(IdentifyCommand { + gossip, certification_network, trust_amount, cert, - .. - }) => { - // Find and list all authenticated bindings for a given - // certificate. - authenticate( - &config, &cli, &q, - cli.gossip, - false, - None, Some(cert))?; - } + }) => authenticate( + &config, false, None, + false, *gossip, *certification_network, *trust_amount, + None, Some(&cert))?, + + // List all authenticated bindings. Subcommands::List(ListCommand { - email, - .. - }) => { - // List all authenticated bindings. - authenticate( - &config, &cli, &q, - cli.gossip, - **email, - None, None)?; - } - Subcommands::Path(PathCommand { .. }) => { - check_path( - &config, &cli, &q, &config.policy)?; - } + email, gossip, certification_network, trust_amount, + pattern, + }) => authenticate( + &config, pattern.is_none(), pattern, + *email, *gossip, *certification_network, *trust_amount, + None, None)?, + + // Authenticates a given path. + Subcommands::Path(PathCommand { + gossip, certification_network, trust_amount, + path, + }) => check_path( + &config, *gossip, *certification_network, *trust_amount, + path)?, } Ok(()) diff --git a/tests/sq-pki.rs b/tests/sq-pki.rs index 644d8f87..e597c788 100644 --- a/tests/sq-pki.rs +++ b/tests/sq-pki.rs @@ -108,8 +108,8 @@ mod integration { } cmd .arg("pki") - .args(sqwot_args) - .arg(command); + .arg(command) + .args(sqwot_args); if let Some(target) = target { cmd.arg(&target.to_string()); }