Change sq key list to display more user IDs.

- Currently, `sq key list` only displays a single best user ID for
    each certificate.

  - Instead, display all user IDs that can be authenticated, or are
    self-signed.  Also indicate the degree to which they can be
    authenticated, and whether the user ID has been revoked.

  - Fixes #360.
This commit is contained in:
Neal H. Walfield 2024-11-18 23:49:20 +01:00
parent 60b369274b
commit 149254b756
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
3 changed files with 86 additions and 12 deletions

View File

@ -1,6 +1,6 @@
use std::{
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashSet},
fmt,
time::SystemTime,
};
@ -11,7 +11,7 @@ use sequoia_openpgp::{
KeyHandle,
cert::amalgamation::ValidAmalgamation,
cert::amalgamation::ValidateAmalgamation,
packet::{Key, key},
packet::{Key, key, UserID},
types::RevocationStatus,
};
@ -20,6 +20,7 @@ use keystore::Protection;
use crate::cli;
use crate::Convert;
use crate::PreferredUserID;
use crate::Sq;
use crate::Result;
use crate::Time;
@ -54,14 +55,6 @@ impl Association {
Association::Bare(k) => k,
}
}
/// Returns the best user ID, if any.
pub fn best_userid(&self, sq: &Sq) -> String {
match self {
Association::Bound(c) => sq.best_userid(c, true).to_string(),
Association::Bare(_) => "bare key".into(),
}
}
}
impl PartialOrd for Association {
@ -382,6 +375,22 @@ pub fn list(sq: Sq, mut cmd: cli::key::list::Command) -> Result<()> {
}
// Now display the keys grouped by OpenPGP certificates.
let q = sq.wot_query();
let q = q.map(|q| q.build()).ok();
let authenticate = |cert: &Cert, userid: &UserID|
-> (usize, PreferredUserID)
{
if let Some(q) = q.as_ref() {
let paths = q.authenticate(
userid, &cert.fingerprint(), sequoia_wot::FULLY_TRUSTED);
let amount = paths.amount();
(amount, PreferredUserID::from_userid(userid.clone(), amount))
} else {
(0, PreferredUserID::from_userid(userid.clone(), 0))
}
};
for (association, keys) in the_keys.iter() {
if let Some(c) = &certs {
// Skip the keys the user is not interested in.
@ -398,7 +407,69 @@ pub fn list(sq: Sq, mut cmd: cli::key::list::Command) -> Result<()> {
// Emit metadata.
wprintln!(initial_indent = " - ", "{}",
association.key().fingerprint());
wprintln!(initial_indent = " - ", "{}", association.best_userid(&sq));
// Show the user IDs that can be authenticated or are self signed.
if let Some(cert) = association.cert() {
let self_signed: HashSet<UserID> = if let Ok(vc)
= cert.with_policy(sq.policy, sq.time)
{
HashSet::from_iter(vc.userids().map(|ua| ua.userid()).cloned())
} else {
Default::default()
};
let mut userids = Vec::with_capacity(cert.userids().count());
for ua in cert.userids() {
let revoked = if let RevocationStatus::Revoked(_)
= ua.revocation_status(sq.policy, sq.time)
{
true
} else {
false
};
let self_signed = self_signed.contains(&ua.userid());
let (amount, userid) = authenticate(cert, ua.userid());
if amount > 0 || self_signed {
userids.push((revoked, amount, self_signed, userid));
}
}
if userids.is_empty() {
wprintln!(initial_indent = " - ", "no user IDs");
} else {
let userid_count = userids.len();
if userid_count > 1 {
wprintln!(initial_indent = " - ", "user IDs:");
}
userids.sort_by(
|(a_revoked, a_amount, a_self_signed, a_userid),
(b_revoked, b_amount, b_self_signed, b_userid) |
{
a_revoked.cmp(b_revoked)
.then(a_amount.cmp(b_amount).reverse())
.then(a_self_signed.cmp(b_self_signed))
.then(a_userid.cmp(b_userid))
});
for (revoked, amount, self_signed, userid)
in userids.into_iter()
{
if amount > 0 || self_signed {
if userid_count == 1 {
wprintln!(initial_indent = " - ", "user ID: {}{}",
userid,
if revoked { " revoked" } else { "" });
} else {
wprintln!(initial_indent = " - ", "{}{}",
userid,
if revoked { " revoked" } else { "" });
}
}
}
}
}
wprintln!(initial_indent = " - ", "created {}",
association.key().creation_time().convert());

View File

@ -30,6 +30,7 @@ pub const NULL_POLICY: &NullPolicy = &NullPolicy::new();
/// Something like a User ID.
///
/// This is used to avoid unnecessary allocations.
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
enum UserIDLike {
UserID(UserID),
String(String),
@ -44,6 +45,7 @@ enum UserIDLike {
/// ```text
/// format!("{:.70}", userid);
/// ```
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
pub struct PreferredUserID {
userid: UserIDLike,
trust_amount: usize,

View File

@ -355,7 +355,8 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
/// Returns a web-of-trust query builder.
///
/// The trust roots are already set appropriately.
fn wot_query(&self) -> Result<wot::NetworkBuilder<&WotStore<'store, 'rstore>>>
pub fn wot_query(&self)
-> Result<wot::NetworkBuilder<&WotStore<'store, 'rstore>>>
{
let cert_store = self.cert_store_or_else()?;
let network = wot::NetworkBuilder::rooted(cert_store,