Align sq pki link add's user ID specification with sq pki certify.

- Align how user IDs are specified using `sq pki link add` with `sq
    pki certify`.  Specifically, add a `--add-userid` argument and
    remove the `--petname` argument.
This commit is contained in:
Neal H. Walfield 2024-10-15 17:03:05 +02:00
parent f0bfdfd1cd
commit 7dee04b9b3
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
5 changed files with 125 additions and 81 deletions

4
NEWS
View File

@ -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

View File

@ -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<String>,
#[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<String>,
#[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 `<alice@example.org>`.",
)]
pub petname: Vec<String>,
#[command(flatten)]
pub userids: UserIDDesignators<
userid_designator::MaybeSelfSignedUserIDEmailAllArgs>,
}
const ADD_EXAMPLES: Actions = Actions {

View File

@ -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
= <<UserIDArg as std::ops::BitOr<EmailArg>>::Output
as std::ops::BitOr<AddUserIDArg>>::Output;
/// Enables --userid, --email, --all, and --add-userid.
pub type MaybeSelfSignedUserIDEmailAllArgs
= <<<UserIDArg as std::ops::BitOr<EmailArg>>::Output
as std::ops::BitOr<AllUserIDsArg>>::Output
as std::ops::BitOr<AddUserIDArg>>::Output;
/// Argument parser options.
/// Normally it is possible to designate multiple certificates. This
@ -81,6 +90,9 @@ pub struct UserIDDesignators<Arguments, Options=typenum::U0>
/// The set of certificate designators.
pub designators: Vec<UserIDDesignator>,
/// Use all self-signed user IDs.
pub all: Option<bool>,
pub add_userid: Option<bool>,
arguments: std::marker::PhantomData<(Arguments, Options)>,
@ -92,6 +104,8 @@ impl<Arguments, Options> 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<Arguments, Options> UserIDDesignators<Arguments, Options> {
self.designators.iter()
}
/// Returns whether the all flag was set.
///
/// If the flag was not enabled, returns `None`.
pub fn all(&self) -> Option<bool> {
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<MaybeSelfSignedUserIDEmailAllArgs>,
}
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));
}
}

View File

@ -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 {

View File

@ -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<Arguments, Options> UserIDDesignators<Arguments, Options> {
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::<Vec<_>>();
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) => {