diff options
Diffstat (limited to 'sq')
-rw-r--r-- | sq/src/commands/certring.rs | 97 | ||||
-rw-r--r-- | sq/src/commands/mod.rs | 1 | ||||
-rw-r--r-- | sq/src/sq-usage.rs | 38 | ||||
-rw-r--r-- | sq/src/sq.rs | 2 | ||||
-rw-r--r-- | sq/src/sq_cli.rs | 17 |
5 files changed, 155 insertions, 0 deletions
diff --git a/sq/src/commands/certring.rs b/sq/src/commands/certring.rs new file mode 100644 index 00000000..2971ce3f --- /dev/null +++ b/sq/src/commands/certring.rs @@ -0,0 +1,97 @@ +use std::{ + fs::File, + io, + path::PathBuf, +}; +use anyhow::Context; + +use sequoia_openpgp as openpgp; +use openpgp::{ + Result, + cert::CertParser, + parse::Parse, + serialize::Serialize, +}; + +use crate::{ + open_or_stdin, +}; + +pub fn dispatch(m: &clap::ArgMatches) -> Result<()> { + match m.subcommand() { + ("split", Some(m)) => { + let mut input = open_or_stdin(m.value_of("input"))?; + let prefix = + // The prefix is either specified explicitly... + m.value_of("prefix").map(|p| p.to_owned()) + .unwrap_or( + // ... or we derive it from the input file... + m.value_of("input").and_then(|i| { + let p = PathBuf::from(i); + // (but only use the filename) + p.file_name().map(|f| String::from(f.to_string_lossy())) + }) + // ... or we use a generic prefix... + .unwrap_or(String::from("output")) + // ... finally, add a hyphen to the derived prefix. + + "-"); + split(&mut input, &prefix) + }, + + _ => unreachable!(), + } +} + +/// Splits a certring into individual certs. +fn split(input: &mut (dyn io::Read + Sync + Send), prefix: &str) + -> Result<()> { + for (i, cert) in CertParser::from_reader(input)?.enumerate() { + let cert = cert.context("Malformed certificate in certring")?; + let filename = format!( + "{}{}-{:X}", + prefix, + i, + cert.fingerprint()); + + // Try to be more helpful by including the first userid in the + // filename. + let mut sink = if let Some(f) = cert.userids().nth(0) + .and_then(|uid| uid.email().unwrap_or(None)) + .and_then(to_filename_fragment) + { + let filename_email = format!("{}-{}", filename, f); + if let Ok(s) = File::create(filename_email) { + s + } else { + // Degrade gracefully in case our sanitization + // produced an invalid filename on this system. + File::create(&filename) + .context(format!("Writing cert to {:?} failed", filename))? + } + } else { + File::create(&filename) + .context(format!("Writing cert to {:?} failed", filename))? + }; + + cert.armored().serialize(&mut sink)?; + } + Ok(()) +} + +/// Sanitizes a string to a safe filename fragment. +fn to_filename_fragment<S: AsRef<str>>(s: S) -> Option<String> { + let mut r = String::with_capacity(s.as_ref().len()); + + s.as_ref().chars().filter_map(|c| match c { + '/' | ':' | '\\' => None, + c if c.is_ascii_whitespace() => None, + c if c.is_ascii() => Some(c), + _ => None, + }).for_each(|c| r.push(c)); + + if r.len() > 0 { + Some(r) + } else { + None + } +} diff --git a/sq/src/commands/mod.rs b/sq/src/commands/mod.rs index 59a9914d..2cd73fe4 100644 --- a/sq/src/commands/mod.rs +++ b/sq/src/commands/mod.rs @@ -40,6 +40,7 @@ pub use self::inspect::inspect; pub mod key; pub mod merge_signatures; pub use self::merge_signatures::merge_signatures; +pub mod certring; /// Returns suitable signing keys from a given list of Certs. fn get_signing_keys(certs: &[openpgp::Cert], p: &dyn Policy, diff --git a/sq/src/sq-usage.rs b/sq/src/sq-usage.rs index 731dcfbf..d257b58a 100644 --- a/sq/src/sq-usage.rs +++ b/sq/src/sq-usage.rs @@ -30,6 +30,7 @@ //! merge-signatures Merges two signatures //! keyserver Interacts with keyservers //! autocrypt Autocrypt support +//! certring Manipulates certificate rings //! dearmor Removes ASCII Armor from a file //! enarmor Applies ASCII Armor to a file //! help Prints this message or the help of the given subcommand(s) @@ -428,6 +429,43 @@ //! <FILE> Sets the input file to use //! ``` //! +//! ## Subcommand certring +//! +//! ```text +//! Manipulates certificate rings +//! +//! USAGE: +//! sq certring <SUBCOMMAND> +//! +//! FLAGS: +//! -h, --help Prints help information +//! -V, --version Prints version information +//! +//! SUBCOMMANDS: +//! help Prints this message or the help of the given subcommand(s) +//! split Splits a certring into individual certs +//! ``` +//! +//! ### Subcommand certring split +//! +//! ```text +//! Splits a certring into individual certs +//! +//! USAGE: +//! sq certring split [OPTIONS] [FILE] +//! +//! FLAGS: +//! -h, --help Prints help information +//! -V, --version Prints version information +//! +//! OPTIONS: +//! -p, --prefix <FILE> Sets the prefix to use for output files (defaults to the input filename with a dash, or +//! 'output' if certring is read from stdin) +//! +//! ARGS: +//! <FILE> Sets the input file to use +//! ``` +//! //! ## Subcommand dearmor //! //! ```text diff --git a/sq/src/sq.rs b/sq/src/sq.rs index f1bd4832..62d3c5ff 100644 --- a/sq/src/sq.rs +++ b/sq/src/sq.rs @@ -452,6 +452,8 @@ fn main() -> Result<()> { commands::inspect(m, policy, &mut output)?; }, + ("certring", Some(m)) => commands::certring::dispatch(m)?, + ("packet", Some(m)) => match m.subcommand() { ("dump", Some(m)) => { let mut input = open_or_stdin(m.value_of("input"))?; diff --git a/sq/src/sq_cli.rs b/sq/src/sq_cli.rs index 7397f20e..cb3176ed 100644 --- a/sq/src/sq_cli.rs +++ b/sq/src/sq_cli.rs @@ -512,6 +512,23 @@ pub fn build() -> App<'static, 'static> { .help("The certificate to add keys to.")) )) + .subcommand( + SubCommand::with_name("certring") + .about("Manipulates certificate rings") + .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand( + SubCommand::with_name("split") + .about("Splits a certring into individual certs") + .arg(Arg::with_name("input").value_name("FILE") + .help("Sets the input file to use")) + .arg(Arg::with_name("prefix").value_name("FILE") + .long("prefix") + .short("p") + .help("Sets the prefix to use for output files \ + (defaults to the input filename with a \ + dash, or 'output' if certring is read \ + from stdin)")))) + .subcommand(SubCommand::with_name("packet") .about("OpenPGP Packet manipulation") .setting(AppSettings::SubcommandRequiredElseHelp) |