summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2021-01-06 12:15:49 +0100
committerJustus Winter <justus@sequoia-pgp.org>2021-01-06 12:30:16 +0100
commit1bbad042ec6047737e214d37aeb261431f170d39 (patch)
treed7679c14e61f7b3adacbb1f44c703fa2a85b494d
parenta5d65cd0b0b10006eb744e8516efa20243bf0afb (diff)
sq: Implement 'certring split'.
- Fixes #554.
-rw-r--r--sq/src/commands/certring.rs97
-rw-r--r--sq/src/commands/mod.rs1
-rw-r--r--sq/src/sq-usage.rs38
-rw-r--r--sq/src/sq.rs2
-rw-r--r--sq/src/sq_cli.rs17
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)