diff --git a/src/cli/decrypt.rs b/src/cli/decrypt.rs index 6702fc1c..5afb68e6 100644 --- a/src/cli/decrypt.rs +++ b/src/cli/decrypt.rs @@ -8,6 +8,7 @@ use super::types::ClapData; use super::types::FileOrStdin; use super::types::FileOrStdout; use super::types::SessionKey; +use super::types::cert_designator::*; use crate::cli::examples; use examples::Action; @@ -105,12 +106,13 @@ pub struct Command { is given, 0 otherwise]", )] pub signatures: Option, - #[clap( - long = "signer-file", - value_name = "CERT_FILE", - help = "Verify signatures using the certificates in CERT_FILE", - )] - pub sender_cert_file: Vec, + + #[command(flatten)] + pub signers: CertDesignators, + #[clap( long = "recipient-file", value_name = "KEY_FILE", diff --git a/src/cli/types/cert_designator.rs b/src/cli/types/cert_designator.rs index 2a3d4b7d..461786a9 100644 --- a/src/cli/types/cert_designator.rs +++ b/src/cli/types/cert_designator.rs @@ -23,9 +23,9 @@ pub trait ArgumentPrefix { pub struct ConcreteArgumentPrefix(std::marker::PhantomData) where T: typenum::Unsigned; -// "--cert", "--userid", "--file", etc. +/// "--cert", "--userid", "--file", etc. pub type NoPrefix = ConcreteArgumentPrefix; -// "--cert", "--cert-userid", "--cert-file", etc. +/// "--cert", "--cert-userid", "--cert-file", etc. pub type CertPrefix = ConcreteArgumentPrefix; /// "--for", "--for-userid", "--for-file", etc. @@ -61,6 +61,19 @@ impl ArgumentPrefix for RecipientPrefix { } } +/// "--signer", "--signer-userid", "--signer-file", etc. +pub type SignerPrefix = ConcreteArgumentPrefix; + +impl ArgumentPrefix for SignerPrefix { + fn prefix() -> &'static str { + "signer-" + } + + fn name() -> &'static str { + "signer" + } +} + /// Adds a `--file` argument. pub type FileArg = typenum::U1; @@ -79,6 +92,16 @@ pub type DomainArg = typenum::U32; /// Adds a `--grep` argument. pub type GrepArg = typenum::U64; +/// Enables --file, --cert, --userid, --email, and --domain, (i.e., +/// not --grep). +#[allow(dead_code)] +pub type FileCertUserIDEmailDomainArgs + = <<<>::Output + as std::ops::BitOr>::Output + as std::ops::BitOr>::Output + as std::ops::BitOr>::Output; + /// Enables --cert, --userid, --email, --domain, and --grep (i.e., not /// --file). pub type CertUserIDEmailDomainGrepArgs @@ -110,6 +133,51 @@ pub type OneValue = typenum::U1; /// completely optional. pub type OptionalValue = typenum::U2; +// Additional documentation. + +/// The prefix for the designators. +/// +/// See [`NoPrefix`], [`CertPrefix`], etc. +pub trait AdditionalDocs { + /// Text to be added to the help text. + // XXX: This should return a Cow<'static, str>, but there is no + // implementation of From> for StyledStr, + // see https://github.com/clap-rs/clap/issues/5785 + fn help(_arg: &'static str, help: &'static str) -> String { + help.into() + } +} + +/// No additional documentation. +pub struct NoDoc(()); +impl AdditionalDocs for NoDoc {} + + +/// Documentation for signer arguments. +pub struct ToVerifyDoc {} +impl AdditionalDocs for ToVerifyDoc { + fn help(arg: &'static str, help: &'static str) -> String { + match arg { + "cert" | "file" => format!( + "{} to verify the signatures with. \ + Note: signatures verified with a certificate \ + given here are considered authenticated.{}", + help, + if arg == "cert" { + " When this option is not provided, the certificate \ + is still read from the certificate store, if it \ + exists, but it is not implicitly considered \ + authenticated." + } else { + "" + }), + _ => format!( + "{} to verify the signatures with", + help), + } + } +} + /// A certificate designator. #[derive(Debug)] pub enum CertDesignator { @@ -238,16 +306,18 @@ impl CertDesignator { /// change, e.g., `--email` to `--for-email`. /// /// `Options` are the set of options to the argument parser. -pub struct CertDesignators +pub struct CertDesignators { /// The set of certificate designators. pub designators: Vec, - arguments: std::marker::PhantomData<(Arguments, Prefix, Options)>, + arguments: std::marker::PhantomData<(Arguments, Prefix, Options, + Doc)>, } -impl std::fmt::Debug - for CertDesignators +impl std::fmt::Debug + for CertDesignators { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CertDesignators") @@ -256,7 +326,7 @@ impl std::fmt::Debug } } -impl CertDesignators { +impl CertDesignators { /// Like `Vec::push`. pub fn push(&mut self, designator: CertDesignator) { self.designators.push(designator) @@ -273,12 +343,13 @@ impl CertDesignators { } } -impl clap::Args - for CertDesignators +impl clap::Args + for CertDesignators where Arguments: typenum::Unsigned, Prefix: ArgumentPrefix, Options: typenum::Unsigned, + Doc: AdditionalDocs, { fn augment_args(mut cmd: clap::Command) -> clap::Command { @@ -382,8 +453,10 @@ where .value_name("FINGERPRINT|KEYID") .value_parser(parse_as_key_handle) .action(action.clone()) - .help("Use certificates with the specified \ - fingerprint or key ID")); + .help(Doc::help( + "cert", + "Use certificates with the specified \ + fingerprint or key ID"))); arg_group = arg_group.arg(full_name); } @@ -394,7 +467,9 @@ where .long(&full_name) .value_name("USERID") .action(action.clone()) - .help("Use certificates with the specified user ID")); + .help(Doc::help( + "userid", + "Use certificates with the specified user ID"))); arg_group = arg_group.arg(full_name); } @@ -406,8 +481,10 @@ where .value_name("EMAIL") .value_parser(parse_as_email) .action(action.clone()) - .help("Use certificates where a user ID includes \ - the specified email address")); + .help(Doc::help( + "email", + "Use certificates where a user ID includes \ + the specified email address"))); arg_group = arg_group.arg(full_name); } @@ -419,8 +496,10 @@ where .value_name("DOMAIN") .value_parser(parse_as_domain) .action(action.clone()) - .help("Use certificates where a user ID includes \ - an email address for the specified domain")); + .help(Doc::help( + "domain", + "Use certificates where a user ID includes \ + an email address for the specified domain"))); arg_group = arg_group.arg(full_name); } @@ -431,8 +510,10 @@ where .long(&full_name) .value_name("PATTERN") .action(action.clone()) - .help("Use certificates with a user ID that \ - matches the pattern, case insensitively")); + .help(Doc::help( + "grep", + "Use certificates with a user ID that \ + matches the pattern, case insensitively"))); arg_group = arg_group.arg(full_name); } @@ -445,7 +526,9 @@ where .value_name("PATH") .value_parser(clap::value_parser!(PathBuf)) .action(action.clone()) - .help("Read certificates from PATH")); + .help(Doc::help( + "file", + "Read certificates from PATH"))); arg_group = arg_group.arg(full_name); } @@ -460,12 +543,13 @@ where } } -impl clap::FromArgMatches - for CertDesignators +impl clap::FromArgMatches + for CertDesignators where Arguments: typenum::Unsigned, Prefix: ArgumentPrefix, Options: typenum::Unsigned, + Doc: AdditionalDocs, { fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> diff --git a/src/commands/decrypt.rs b/src/commands/decrypt.rs index 05bb4027..02709967 100644 --- a/src/commands/decrypt.rs +++ b/src/commands/decrypt.rs @@ -32,7 +32,6 @@ use crate::{ }, common::password, Sq, - load_certs, load_keys, }; @@ -42,12 +41,13 @@ pub fn dispatch(sq: Sq, command: cli::decrypt::Command) -> Result<()> { let mut input = command.input.open()?; let mut output = command.output.create_safe(&sq)?; - let certs = load_certs( - command.sender_cert_file.iter())?; + let signers = + sq.resolve_certs_or_fail(&command.signers, sequoia_wot::FULLY_TRUSTED)?; + // Fancy default for --signatures. If you change this, // also change the description in the CLI definition. let signatures = command.signatures.unwrap_or_else(|| { - if certs.is_empty() { + if signers.is_empty() { // No certs are given for verification, use 0 as // threshold so we handle only-encrypted messages // gracefully. @@ -62,7 +62,7 @@ pub fn dispatch(sq: Sq, command: cli::decrypt::Command) -> Result<()> { load_keys(command.secret_key_file.iter())?; let session_keys = command.session_key; decrypt(sq, &mut input, &mut output, - signatures, certs, secrets, + signatures, signers, secrets, command.dump_session_key, session_keys)?; diff --git a/src/sq.rs b/src/sq.rs index 1a710ff0..24ab3180 100644 --- a/src/sq.rs +++ b/src/sq.rs @@ -1694,9 +1694,9 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> { /// general, designator-specific errors are returned as `Err`s in /// the `Vec`. General errors, like the certificate store is /// disabled, are returned using the outer `Result`. - pub fn resolve_certs( + pub fn resolve_certs( &self, - designators: &CertDesignators, + designators: &CertDesignators, trust_amount: usize, ) -> Result<(Vec, Vec)> @@ -2099,14 +2099,37 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> { Ok((results, errors)) } + /// Like `Sq::resolve_certs`, but bails if there is an error + /// resolving a certificate. + pub fn resolve_certs_or_fail( + &self, + designators: &CertDesignators, + trust_amount: usize, + ) + -> Result> + where + Prefix: ArgumentPrefix, + { + let (certs, errors) = self.resolve_certs(designators, trust_amount)?; + + for error in errors.iter() { + print_error_chain(error); + } + if ! errors.is_empty() { + return Err(anyhow::anyhow!("Failed to resolve certificates")); + } + + Ok(certs) + } + /// Like `Sq::resolve_certs`, but bails if there is not exactly /// one designator, or the designator resolves to multiple /// certificates. /// /// Returns whether the certificate was read from a file. - pub fn resolve_cert( + pub fn resolve_cert( &self, - designators: &CertDesignators, + designators: &CertDesignators, trust_amount: usize, ) -> Result<(Cert, bool)>