From 17253b2e7fa297426a841042fa955ca69955f00f Mon Sep 17 00:00:00 2001 From: Justus Winter Date: Wed, 6 Jan 2021 14:58:04 +0100 Subject: sq: Implement 'certring filter'. --- sq/src/commands/certring.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++ sq/src/sq-usage.rs | 59 ++++++++++++++++++++++++++--- sq/src/sq_cli.rs | 40 ++++++++++++++++++++ 3 files changed, 184 insertions(+), 6 deletions(-) diff --git a/sq/src/commands/certring.rs b/sq/src/commands/certring.rs index 351aad7c..f102eaf1 100644 --- a/sq/src/commands/certring.rs +++ b/sq/src/commands/certring.rs @@ -13,6 +13,11 @@ use openpgp::{ Cert, CertParser, }, + packet::{ + UserID, + UserAttribute, + Key, + }, parse::Parse, serialize::Serialize, }; @@ -24,6 +29,92 @@ use crate::{ pub fn dispatch(m: &clap::ArgMatches, force: bool) -> Result<()> { match m.subcommand() { + ("filter", Some(m)) => { + let any_uid_predicates = + m.is_present("name") + || m.is_present("email") + || m.is_present("domain"); + let uid_predicate = |uid: &UserID| { + let mut keep = false; + + if let Some(names) = m.values_of("name") { + for name in names { + keep |= uid + .name().unwrap_or(None) + .map(|n| n == name) + .unwrap_or(false); + } + } + + if let Some(emails) = m.values_of("email") { + for email in emails { + keep |= uid + .email().unwrap_or(None) + .map(|n| n == email) + .unwrap_or(false); + } + } + + if let Some(domains) = m.values_of("domain") { + for domain in domains { + keep |= uid + .email().unwrap_or(None) + .map(|n| n.ends_with(&format!("@{}", domain))) + .unwrap_or(false); + } + } + + keep + }; + + let any_ua_predicates = false; + let ua_predicate = |_ua: &UserAttribute| false; + + let any_key_predicates = false; + let key_predicate = |_key: &Key<_, _>| false; + + let filter_fn = |c: Cert| -> Option { + if ! (c.userids().any(|c| uid_predicate(&c)) + || c.user_attributes().any(|c| ua_predicate(&c)) + || c.keys().subkeys().any(|c| key_predicate(&c))) { + None + } else if m.is_present("prune-certs") { + let c = c + .retain_userids(|c| { + ! any_uid_predicates || uid_predicate(&c) + }) + .retain_user_attributes(|c| { + ! any_ua_predicates || ua_predicate(&c) + }) + .retain_subkeys(|c| { + ! any_key_predicates || key_predicate(&c) + }); + if c.userids().count() == 0 + && c.user_attributes().count() == 0 + && c.keys().subkeys().count() == 0 + { + // We stripped all components, omit this cert. + None + } else { + Some(c) + } + } else { + Some(c) + } + }; + + // XXX: Armor type selection is a bit problematic. If any + // of the certificates contain a secret key, it would be + // better to use Kind::SecretKey here. However, this + // requires buffering all certs, which has its own + // problems. + let mut output = create_or_stdout_pgp(m.value_of("output"), + force, + m.is_present("binary"), + armor::Kind::PublicKey)?; + filter(m.values_of("input"), &mut output, filter_fn)?; + output.finalize() + }, ("join", Some(m)) => { // XXX: Armor type selection is a bit problematic. If any // of the certificates contain a secret key, it would be diff --git a/sq/src/sq-usage.rs b/sq/src/sq-usage.rs index 59844351..9519d098 100644 --- a/sq/src/sq-usage.rs +++ b/sq/src/sq-usage.rs @@ -438,14 +438,61 @@ //! sq certring //! //! FLAGS: -//! -h, --help Prints help information -//! -V, --version Prints version information +//! -h, --help +//! Prints help information +//! +//! -V, --version +//! Prints version information +//! //! //! SUBCOMMANDS: -//! help Prints this message or the help of the given subcommand(s) -//! join Joins certs into a certring -//! list Lists certs in a certring -//! split Splits a certring into individual certs +//! filter Joins certs into a certring applying a filter +//! help Prints this message or the help of the given subcommand(s) +//! join Joins certs into a certring +//! list Lists certs in a certring +//! split Splits a certring into individual certs +//! ``` +//! +//! ### Subcommand certring filter +//! +//! ```text +//! If multiple predicates are given, they are or'ed, i.e. a certificate matches if any of the predicates match. To require +//! all predicates to match, chain multiple invocations of this command. +//! +//! USAGE: +//! sq certring filter [FLAGS] [OPTIONS] [--] [FILE]... +//! +//! FLAGS: +//! -B, --binary +//! Don't ASCII-armor the certring +//! +//! -h, --help +//! Prints help information +//! +//! -P, --prune-certs +//! Remove certificate components not matching the filter +//! +//! -V, --version +//! Prints version information +//! +//! +//! OPTIONS: +//! --domain ... +//! Match on this email domain name +//! +//! --email
... +//! Match on this email address +//! +//! --name ... +//! Match on this name +//! +//! -o, --output +//! Sets the output file to use +//! +//! +//! ARGS: +//! ... +//! Sets the input files to use //! ``` //! //! ### Subcommand certring join diff --git a/sq/src/sq_cli.rs b/sq/src/sq_cli.rs index e924b8ab..11496248 100644 --- a/sq/src/sq_cli.rs +++ b/sq/src/sq_cli.rs @@ -516,6 +516,46 @@ pub fn build() -> App<'static, 'static> { SubCommand::with_name("certring") .about("Manipulates certificate rings") .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand( + SubCommand::with_name("filter") + .about("Joins certs into a certring applying a filter") + .long_about( + "If multiple predicates are given, they are \ + or'ed, i.e. a certificate matches if any \ + of the predicates match. To require all \ + predicates to match, chain multiple \ + invocations of this command.") + .arg(Arg::with_name("input").value_name("FILE") + .multiple(true) + .help("Sets the input files to use")) + .arg(Arg::with_name("output").value_name("FILE") + .long("output") + .short("o") + .help("Sets the output file to use")) + .arg(Arg::with_name("name").value_name("NAME") + .long("name") + .multiple(true) + .number_of_values(1) + .help("Match on this name")) + .arg(Arg::with_name("email").value_name("ADDRESS") + .long("email") + .multiple(true) + .number_of_values(1) + .help("Match on this email address")) + .arg(Arg::with_name("domain").value_name("FQDN") + .long("domain") + .multiple(true) + .number_of_values(1) + .help("Match on this email domain name")) + .arg(Arg::with_name("prune-certs") + .long("prune-certs") + .short("P") + .help("Remove certificate components not matching \ + the filter")) + .arg(Arg::with_name("binary") + .long("binary") + .short("B") + .help("Don't ASCII-armor the certring"))) .subcommand( SubCommand::with_name("join") .about("Joins certs into a certring") -- cgit v1.2.3