diff --git a/NEWS b/NEWS index ca1e36ba..662f65dd 100644 --- a/NEWS +++ b/NEWS @@ -81,6 +81,10 @@ - Removed `sq pki link add`'s positional argument for specifying a user ID directly or by email address. Use the named arguments, `--userid` or `--email` instead. + - Add `--add-userid` to `sq pki link add`. This aligns it with `sq + pki certify`. + - Removed `sq pki link add`'s `--petname` argument. Use `--userid` + in conjunction with `--add-userid` instead. * Changes in 0.38.0 ** Notable changes diff --git a/src/cli/pki/link.rs b/src/cli/pki/link.rs index 23937e04..b5f82fb0 100644 --- a/src/cli/pki/link.rs +++ b/src/cli/pki/link.rs @@ -5,6 +5,8 @@ use clap::{ArgGroup, Parser, Subcommand}; use crate::cli::examples::*; use crate::cli::types::CertDesignators; use crate::cli::types::cert_designator; +use crate::cli::types::UserIDDesignators; +use crate::cli::types::userid_designator; use crate::cli::types::Expiration; use crate::cli::types::TrustAmount; @@ -281,52 +283,9 @@ to force the signature to be re-created anyway.", cert_designator::CertPrefix, cert_designator::OneValue>, - #[clap( - long = "all", - conflicts_with_all = &[ "userid", "email", "petname" ], - required = false, - help = "Link all valid self-signed User ID to the certificate.", - long_help = "Link all valid self-signed User ID to the certificate.", - )] - pub all: bool, - - #[clap( - long = "userid", - value_name = "USERID", - required = false, - help = "A User ID to link to the certificate.", - long_help = "A User ID to link to the certificate. This must match \ - a self-signed User ID. To link a User ID to the \ - certificate that does not have a self-signature, use \ - `--petname`.", - )] - pub userid: Vec, - #[clap( - long = "email", - value_name = "EMAIL", - required = false, - help = "An email address to link to the certificate.", - long_help = "An email address to link to the certificate. The email \ - address must match the email address of a \ - self-signed User ID. To link an email address to the \ - certificate that does not appear in a self-signed \ - User ID, use `--petname`. If the specified email \ - appears in multiple self-signed User IDs, then all of \ - them are linked.", - )] - pub email: Vec, - #[clap( - long = "petname", - value_name = "PETNAME", - required = false, - help = "A User ID to link to the certificate.", - long_help = "A User ID to link to the certificate. Unlike `--userid`, \ - this does not need to match a self-signed User ID. Bare \ - email address are automatically wrapped in angle brackets. \ - That is if `alice@example.org` is provided, it is \ - silently converted to ``.", - )] - pub petname: Vec, + #[command(flatten)] + pub userids: UserIDDesignators< + userid_designator::MaybeSelfSignedUserIDEmailAllArgs>, } const ADD_EXAMPLES: Actions = Actions { diff --git a/src/cli/types/userid_designator.rs b/src/cli/types/userid_designator.rs index c0b99eb1..cf387477 100644 --- a/src/cli/types/userid_designator.rs +++ b/src/cli/types/userid_designator.rs @@ -12,14 +12,23 @@ pub type UserIDArg = typenum::U1; /// Adds a `--email` argument. pub type EmailArg = typenum::U2; +/// Adds a `--all` argument. +pub type AllUserIDsArg = typenum::U4; + /// Adds a `--add-userid` argument. -pub type AddUserIDArg = typenum::U4; +pub type AddUserIDArg = typenum::U8; /// Enables --userid, --email, and --add-userid. pub type MaybeSelfSignedUserIDEmailArgs = <>::Output as std::ops::BitOr>::Output; +/// Enables --userid, --email, --all, and --add-userid. +pub type MaybeSelfSignedUserIDEmailAllArgs + = <<>::Output + as std::ops::BitOr>::Output + as std::ops::BitOr>::Output; + /// Argument parser options. /// Normally it is possible to designate multiple certificates. This @@ -81,6 +90,9 @@ pub struct UserIDDesignators /// The set of certificate designators. pub designators: Vec, + /// Use all self-signed user IDs. + pub all: Option, + pub add_userid: Option, arguments: std::marker::PhantomData<(Arguments, Options)>, @@ -92,6 +104,8 @@ impl std::fmt::Debug fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("UserIDDesignators") .field("designators", &self.designators) + .field("all", &self.all) + .field("add_userid", &self.add_userid) .finish() } } @@ -113,6 +127,13 @@ impl UserIDDesignators { self.designators.iter() } + /// Returns whether the all flag was set. + /// + /// If the flag was not enabled, returns `None`. + pub fn all(&self) -> Option { + self.all + } + /// Returns whether the add user ID flag was set. /// /// If the flag was not enabled, returns `None`. @@ -132,6 +153,7 @@ where let arguments = Arguments::to_usize(); let userid_arg = (arguments & UserIDArg::to_usize()) > 0; let email_arg = (arguments & EmailArg::to_usize()) > 0; + let all_arg = (arguments & AllUserIDsArg::to_usize()) > 0; let add_userid_arg = (arguments & AddUserIDArg::to_usize()) > 0; let options = Options::to_usize(); @@ -200,6 +222,18 @@ where arg_group = arg_group.arg(full_name); } + if all_arg { + let full_name = "all"; + cmd = cmd.arg( + clap::Arg::new(&full_name) + .long(&full_name) + .requires(&group) + .action(clap::ArgAction::SetTrue) + .help("\ +Uses all self-signed user IDs")); + arg_group = arg_group.arg(full_name); + } + if add_userid_arg { let full_name = "add-userid"; cmd = cmd.arg( @@ -245,6 +279,7 @@ where let arguments = Arguments::to_usize(); let userid_arg = (arguments & UserIDArg::to_usize()) > 0; let email_arg = (arguments & EmailArg::to_usize()) > 0; + let all_arg = (arguments & AllUserIDsArg::to_usize()) > 0; let add_userid_arg = (arguments & AddUserIDArg::to_usize()) > 0; let mut designators = Vec::new(); @@ -278,6 +313,16 @@ where None }; + self.all = if all_arg { + if matches.get_flag("all") { + Some(true) + } else { + Some(false) + } + } else { + None + }; + self.designators = designators; Ok(()) } @@ -288,6 +333,7 @@ where let mut designators = Self { designators: Vec::new(), arguments: std::marker::PhantomData, + all: None, add_userid: None, }; @@ -501,4 +547,49 @@ mod test { ]); assert!(m.is_err()); } + + #[test] + fn userid_designators_all() { + use clap::Parser; + use clap::CommandFactory; + use clap::FromArgMatches; + + #[derive(Parser, Debug)] + #[clap(name = "prog")] + struct CLI { + #[command(flatten)] + pub userids: UserIDDesignators, + } + + let command = CLI::command(); + + // Check if --all is recognized. + let m = command.clone().try_get_matches_from(vec![ + "prog", + "--userid", "alice", + ]); + let m = m.expect("valid arguments"); + let c = CLI::from_arg_matches(&m).expect("ok"); + assert_eq!(c.userids.designators.len(), 1); + assert_eq!(c.userids.all(), Some(false)); + + let m = command.clone().try_get_matches_from(vec![ + "prog", + "--all" + ]); + let m = m.expect("valid arguments"); + let c = CLI::from_arg_matches(&m).expect("ok"); + assert_eq!(c.userids.designators.len(), 0); + assert_eq!(c.userids.all(), Some(true)); + + let m = command.clone().try_get_matches_from(vec![ + "prog", + "--userid", "alice", + "--all", + ]); + let m = m.expect("valid arguments"); + let c = CLI::from_arg_matches(&m).expect("ok"); + assert_eq!(c.userids.designators.len(), 1); + assert_eq!(c.userids.all(), Some(true)); + } } diff --git a/src/commands/pki/link.rs b/src/commands/pki/link.rs index c0ce9dc7..906ff7b8 100644 --- a/src/commands/pki/link.rs +++ b/src/commands/pki/link.rs @@ -203,40 +203,8 @@ pub fn add(sq: Sq, c: link::AddCommand) let (cert, _from_file) = sq.resolve_cert(&c.cert, sequoia_wot::FULLY_TRUSTED)?; - let mut userids = - check_userids(&sq, &cert, true, &c.userid, &c.email) - .context("sq pki link add: Invalid User IDs")?; - userids.extend(c.petname.iter().map(|petname| { - // If it is a bare email, we wrap it in angle brackets. - if UserIDQueryParams::is_email(petname).is_ok() { - UserID::from(&format!("<{}>", petname)[..]) - } else { - UserID::from(&petname[..]) - } - })); - let vc = cert.with_policy(sq.policy, Some(sq.time))?; - - let user_supplied_userids = if userids.is_empty() { - if c.all { - userids = vc.userids().map(|ua| ua.userid().clone()).collect(); - } else { - wprintln!("No User IDs specified. \ - Pass \"--all\" or one or more User IDs. \ - {}'s self-signed User IDs are:", - cert.fingerprint()); - for (i, userid) in vc.userids().enumerate() { - wprintln!(" {}. {:?}", - i + 1, - String::from_utf8_lossy(userid.value())); - } - return Err(anyhow::anyhow!("No User IDs specified")); - } - - false - } else { - true - }; + let userids = c.userids.resolve(&vc)?; let trust_depth: u8 = if let Some(depth) = c.depth { depth @@ -293,7 +261,7 @@ pub fn add(sq: Sq, c: link::AddCommand) &cert, &userids[..], true, // Add userid. - user_supplied_userids, + true, // User-supplied user IDs. &templates, trust_depth, if star { diff --git a/src/common/types/userid_designator.rs b/src/common/types/userid_designator.rs index f07b3467..ed8b8705 100644 --- a/src/common/types/userid_designator.rs +++ b/src/common/types/userid_designator.rs @@ -1,6 +1,8 @@ use sequoia_openpgp as openpgp; +use openpgp::cert::amalgamation::ValidAmalgamation; use openpgp::cert::ValidCert; use openpgp::packet::UserID; +use openpgp::types::RevocationStatus; use crate::Result; use crate::cli::types::UserIDDesignators; @@ -16,6 +18,26 @@ impl UserIDDesignators { let mut missing = false; let mut bad = None; + if let Some(true) = self.all() { + let all_userids = vc.userids() + .filter_map(|ua| { + if let RevocationStatus::Revoked(_) = ua.revocation_status() { + None + } else { + Some(ua.userid().clone()) + } + }) + .collect::>(); + + if all_userids.is_empty() { + return Err(anyhow::anyhow!( + "{} has no valid self-signed user IDs", + vc.fingerprint())); + } + + userids.extend(all_userids); + } + for designator in self.iter() { match designator { UserIDDesignator::UserID(userid) => {