Use cert designators for sq cert list.

- This aligns it with `sq key list` and `sq pki link list`.

  - Fixes #446.
This commit is contained in:
Justus Winter 2024-12-11 16:22:09 +01:00
parent 3d4554a5a4
commit ebef0cf9ee
No known key found for this signature in database
GPG Key ID: 686F55B4AB2B3386
10 changed files with 144 additions and 49 deletions

5
NEWS
View File

@ -2,6 +2,11 @@
#+TITLE: sequoia-sq NEWS history of user-visible changes
#+STARTUP: content hidestars
* Changes in 1.0.0
** Notable changes
- `sq cert list` now takes cert designators, like `--cert-email`
instead of `--email`.
* Changes in 0.41.0
** New functionality
- `sq encrypt --for-self` now adds the certs configured under

View File

@ -8,8 +8,7 @@ use crate::cli::pki::CertificationNetworkArg;
use crate::cli::pki::GossipArg;
use crate::cli::pki::RequiredTrustAmountArg;
use crate::cli::pki::ShowPathsArg;
use crate::cli::types::UserIDDesignators;
use crate::cli::types::userid_designator;
use crate::cli::types::cert_designator::*;
const EXAMPLES: Actions = Actions {
actions: &[
@ -29,13 +28,15 @@ const EXAMPLES: Actions = Actions {
Action::example().comment(
"List all authenticated bindings for User IDs containing a specific email address."
).command (&[
"sq", "cert", "list", "--email", "alice@example.org",
"sq", "cert", "list",
"--cert-email=alice@example.org",
]).build(),
Action::example().comment(
"List all paths to certificates containing a specific email address."
).command (&[
"sq", "cert", "list", "--gossip", "--show-paths", "--email", "alice@example.org",
"sq", "cert", "list", "--gossip", "--show-paths",
"--cert-email=alice@example.org",
]).build(),
]
};
@ -62,18 +63,24 @@ test_examples!(sq_cert_list, EXAMPLES);
)]
pub struct Command {
#[command(flatten)]
pub userid: UserIDDesignators<
userid_designator::AnyUserIDEmailArgs,
userid_designator::OptionalValueNoLinting>,
pub certs: CertDesignators<CertUserIDEmailDomainGrepArgs,
CertPrefix,
OptionalValue,
ListCertDoc>,
/// A pattern to select the bindings to authenticate.
///
/// The pattern is treated as a UTF-8 encoded string and a
/// case insensitive substring search (using the current
/// locale) is performed against each User ID. If a User ID
/// is not valid UTF-8, the binding is ignored.
#[clap(
conflicts_with_all = &[ "userid", "email" ]
value_name = "FINGERPRINT|KEYID|PATTERN",
help = "A pattern to filter the displayed certificates",
long_help = "\
A pattern to filter the displayed certificates.
If the pattern appears to be a fingerprint or key ID, it is treated as \
if it were passed to `--cert`, and matches on the certificate's \
fingerprint. Otherwise, it is treated as if it were passed via \
`--cert-grep`, and matches on user IDs.
",
conflicts_with_all = &["cert", "cert-userid", "cert-email",
"cert-domain", "cert-grep"],
)]
pub pattern: Option<String>,
@ -89,3 +96,13 @@ pub struct Command {
#[command(flatten)]
pub trust_amount: RequiredTrustAmountArg,
}
/// Documentation for the cert designators for the cert list.
pub struct ListCertDoc {}
impl AdditionalDocs for ListCertDoc {
fn help(_: &'static str, help: &'static str) -> clap::builder::StyledStr {
debug_assert!(help.starts_with("Use certificates"));
help.replace("Use certificates", "List certs").into()
}
}

View File

@ -118,9 +118,6 @@ pub type AllMatchesNonSelfSigned = typenum::U8;
pub type OneValueNoLinting
= <OneValue as std::ops::BitOr<NoLinting>>::Output;
pub type OptionalValueNoLinting
= <OptionalValue as std::ops::BitOr<NoLinting>>::Output;
pub type AllMatchesNonSelfSignedNoLinting
= <AllMatchesNonSelfSigned as std::ops::BitOr<NoLinting>>::Output;

View File

@ -1,12 +1,16 @@
//! Operations on certs.
use sequoia_openpgp as openpgp;
use openpgp::KeyHandle;
use openpgp::{
Fingerprint,
KeyHandle,
};
use crate::{
Sq,
Result,
cli::cert::{Command, list, Subcommands},
cli::types::cert_designator,
common::pki::authenticate,
};
@ -25,28 +29,53 @@ pub fn dispatch(sq: Sq, command: Command) -> Result<()>
// List all authenticated bindings.
Subcommands::List(list::Command {
userid, pattern, gossip, certification_network, trust_amount,
mut certs, pattern, gossip, certification_network, trust_amount,
show_paths,
}) => {
let userid = userid.designators.into_iter().next();
let pattern = if let Some(pattern) = pattern {
let mut d = None;
if let Ok(kh) = pattern.parse::<KeyHandle>() {
if matches!(kh, KeyHandle::Fingerprint(Fingerprint::Invalid(_))) {
let hex = pattern.chars()
.map(|c| {
if c == ' ' { 0 } else { 1 }
})
.sum::<usize>();
if let Some(kh) = pattern.as_ref()
.and_then(|p| p.parse::<KeyHandle>().ok())
{
let cert = sq.resolve_cert(&kh.into(), 0)?.0;
authenticate(
&mut std::io::stdout(),
&sq, false, None,
*gossip, *certification_network, *trust_amount,
userid.as_ref(), Some(&cert), *show_paths)
if hex >= 16 {
weprintln!("Warning: {} looks like a fingerprint or key ID, \
but its invalid. Treating it as a text pattern.",
pattern);
}
} else {
d = Some(cert_designator::CertDesignator::Cert(kh));
}
};
if let Some(d) = d {
certs.push(d);
None
} else {
certs.push(
cert_designator::CertDesignator::Grep(pattern.clone()));
Some(pattern)
}
} else {
authenticate(
&mut std::io::stdout(),
&sq, pattern.is_none(), pattern,
*gossip, *certification_network, *trust_amount,
userid.as_ref(), None, *show_paths)
}
}
None
};
let certs = sq.resolve_certs_or_fail(
&certs, trust_amount.map(|t| t.amount())
.unwrap_or(sequoia_wot::FULLY_TRUSTED))?;
authenticate(
&mut std::io::stdout(),
&sq, certs.is_empty(), pattern,
*gossip, *certification_network, *trust_amount,
None, None,
(! certs.is_empty()).then_some(certs),
*show_paths)
},
Subcommands::Lint(command) =>
lint::lint(sq, command),

View File

@ -403,6 +403,7 @@ pub fn dispatch(sq: Sq, c: download::Command)
Some(TrustAmount::Full), // trust amount
None, // user ID
Some(&cert),
None,
true, // show paths
).is_ok();

View File

@ -34,7 +34,7 @@ pub fn dispatch(sq: Sq, cli: cli::pki::Command, matches: &ArgMatches)
&mut std::io::stdout(),
&sq, false, None,
*gossip, *certification_network, *trust_amount,
Some(&userid), Some(&cert), *show_paths,
Some(&userid), Some(&cert), None, *show_paths,
)?
}
@ -51,7 +51,7 @@ pub fn dispatch(sq: Sq, cli: cli::pki::Command, matches: &ArgMatches)
&mut std::io::stdout(),
&sq, false, None,
*gossip, *certification_network, *trust_amount,
Some(&userid), None, *show_paths)?;
Some(&userid), None, None, *show_paths)?;
}
// Find and list all authenticated bindings for a given
@ -66,7 +66,7 @@ pub fn dispatch(sq: Sq, cli: cli::pki::Command, matches: &ArgMatches)
&mut std::io::stdout(),
&sq, false, None,
*gossip, *certification_network, *trust_amount,
None, Some(&cert), *show_paths)?;
None, Some(&cert), None, *show_paths)?;
}
// Authenticates a given path.

View File

@ -76,6 +76,7 @@ pub fn authenticate<'store, 'rstore>(
trust_amount: Option<TrustAmount<usize>>,
userid_designator: Option<&userid_designator::UserIDDesignator>,
certificate: Option<&Cert>,
certs: Option<Vec<Cert>>,
show_paths: bool,
) -> Result<()>
where 'store: 'rstore,
@ -169,6 +170,14 @@ pub fn authenticate<'store, 'rstore>(
.into_iter()
.map(|fpr| (fpr, UserID::from(userid)))
.collect();
} else if let Some(certs) = certs {
// List all certs.
t!("Authenticating given certs");
bindings = certs.iter().flat_map(|cert| {
let fp = cert.fingerprint();
let userids = n.certified_userids_of(&fp);
userids.into_iter().map(move |uid| (fp.clone(), uid))
}).collect();
} else {
// No User ID, no Fingerprint.
// List everything.

View File

@ -285,6 +285,25 @@ impl UserIDArg<'_> {
cmd.arg("--email-or-add").arg(email),
};
}
/// Add the argument to a `Command` as a cert designator.
///
/// Note: only a subset of user ID arguments are expressible as
/// cert designators.
pub fn as_cert_designator(&self, cmd: &mut Command) {
match self {
UserIDArg::UserID(userid) =>
cmd.arg("--cert-userid").arg(userid),
UserIDArg::Email(email) =>
cmd.arg("--cert-email").arg(email),
UserIDArg::Name(name) =>
unreachable!(),
UserIDArg::AddUserID(userid) =>
unreachable!(),
UserIDArg::AddEmail(email) =>
unreachable!(),
};
}
}
impl std::fmt::Display for UserIDArg<'_> {

View File

@ -18,11 +18,11 @@ fn list() {
// By user ID.
sq.cert_list(&[userid]);
sq.cert_list(&["--userid", userid]);
sq.cert_list(&["--cert-userid", userid]);
// By email.
sq.cert_list(&[email]);
sq.cert_list(&["--email", email]);
sq.cert_list(&["--cert-email", email]);
// By name.
sq.cert_list(&[name]);
@ -34,10 +34,10 @@ fn list() {
sq.cert_list(&["ExAmPlE.Or"]);
// When we use --userid, then we don't do substring matching.
assert!(sq.cert_list_maybe(&["--userid", &userid[1..]]).is_err());
assert!(sq.cert_list_maybe(&["--cert-userid", &userid[1..]]).is_err());
// When we use --email, then we don't do substring matching.
assert!(sq.cert_list_maybe(&["--email", &email[1..]]).is_err());
assert!(sq.cert_list_maybe(&["--cert-email", &email[1..]]).is_err());
}
#[test]

View File

@ -121,13 +121,31 @@ where
cmd.arg(&target.to_string());
}
if let Some(userid) = userid {
userid.as_arg(&mut cmd);
if command == "list" {
userid.as_cert_designator(&mut cmd);
} else {
userid.as_arg(&mut cmd);
}
}
for arg in args {
cmd.arg(arg);
}
cmd.args(&["--amount", &format!("{}", amount)]);
eprintln!("Running: {}",
std::iter::once(cmd.get_program())
.chain(cmd.get_args())
.map(|arg| {
let arg = arg.to_string_lossy();
if arg.contains(" ") {
format!("{:?}", arg)
} else {
arg.into_owned()
}
})
.collect::<Vec<_>>()
.join(" "));
if success {
let assertion = cmd.assert();
let assertion = assertion.success();
@ -140,8 +158,8 @@ where
assert_eq!(
occurrences, *expected_occurrences,
"Failed to find: '{}' {} times\n\
in output:\n\
"Failed to find: '{}' {} times \
in output:\n\n\
{}",
s, expected_occurrences,
String::from_utf8_lossy(haystack),
@ -160,8 +178,8 @@ where
assert_eq!(
occurrences, *expected_occurrences,
"Failed to find: '{}' {} times\n\
in output:\n\
"Failed to find: '{}' {} times \
in output:\n\n\
{}",
s, expected_occurrences,
String::from_utf8_lossy(haystack),
@ -1316,7 +1334,7 @@ fn list_pattern() -> Result<()> {
.map(|(userid, target)| {
(1, format!("- {} {}", HR_OK, userid).to_string())
})
.chain(vec![(bindings.len(), HR_OK.to_string())].into_iter())
.chain(vec![(3, HR_OK.to_string())].into_iter())
.collect::<Vec<_>>();
test(