summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNeal H. Walfield <neal@pep.foundation>2021-01-21 17:29:14 +0100
committerNeal H. Walfield <neal@pep.foundation>2021-01-21 17:41:12 +0100
commitc8d0d8a08d2ad8101e545eb866e095392a88a624 (patch)
treec6df5647708058c4fe904912c7ed324c5a8a1b29
parent8ea5c7a74d3b9d15f130c1e49b7a98249b782731 (diff)
sq: Add new command 'sq certring merge'
- Unlike 'sq certring join', this merges multiple version of the same certificate.
-rw-r--r--sq/src/commands/certring.rs59
-rw-r--r--sq/src/sq-usage.rs56
-rw-r--r--sq/src/sq_cli.rs26
3 files changed, 133 insertions, 8 deletions
diff --git a/sq/src/commands/certring.rs b/sq/src/commands/certring.rs
index f102eaf1..4b5572a7 100644
--- a/sq/src/commands/certring.rs
+++ b/sq/src/commands/certring.rs
@@ -1,4 +1,6 @@
use std::{
+ collections::HashMap,
+ collections::hash_map::Entry,
fs::File,
io,
path::PathBuf,
@@ -13,6 +15,7 @@ use openpgp::{
Cert,
CertParser,
},
+ Fingerprint,
packet::{
UserID,
UserAttribute,
@@ -128,6 +131,14 @@ pub fn dispatch(m: &clap::ArgMatches, force: bool) -> Result<()> {
filter(m.values_of("input"), &mut output, |c| Some(c))?;
output.finalize()
},
+ ("merge", Some(m)) => {
+ let mut output = create_or_stdout_pgp(m.value_of("output"),
+ force,
+ m.is_present("binary"),
+ armor::Kind::PublicKey)?;
+ merge(m.values_of("input"), &mut output)?;
+ output.finalize()
+ },
("list", Some(m)) => {
let mut input = open_or_stdin(m.value_of("input"))?;
list(&mut input)
@@ -236,6 +247,54 @@ fn split(input: &mut (dyn io::Read + Sync + Send), prefix: &str)
Ok(())
}
+/// Merge multiple certrings.
+fn merge(inputs: Option<clap::Values>, output: &mut dyn io::Write)
+ -> Result<()>
+{
+ let mut certs: HashMap<Fingerprint, Option<Cert>> = HashMap::new();
+
+ if let Some(inputs) = inputs {
+ for name in inputs {
+ for cert in CertParser::from_file(name)? {
+ let cert = cert.context(
+ format!("Malformed certificate in certring {:?}", name))?;
+ match certs.entry(cert.fingerprint()) {
+ e @ Entry::Vacant(_) => {
+ e.or_insert(Some(cert));
+ }
+ Entry::Occupied(mut e) => {
+ let e = e.get_mut();
+ let curr = e.take().unwrap();
+ *e = Some(curr.merge_public_and_secret(cert)
+ .expect("Same certificate"));
+ }
+ }
+ }
+ }
+ } else {
+ for cert in CertParser::from_reader(io::stdin())? {
+ let cert = cert.context("Malformed certificate in certring")?;
+ match certs.entry(cert.fingerprint()) {
+ e @ Entry::Vacant(_) => {
+ e.or_insert(Some(cert));
+ }
+ Entry::Occupied(mut e) => {
+ let e = e.get_mut();
+ let curr = e.take().unwrap();
+ *e = Some(curr.merge_public_and_secret(cert)
+ .expect("Same certificate"));
+ }
+ }
+ }
+ }
+
+ for (_, cert) in certs.iter_mut() {
+ cert.take().unwrap().as_tsk().serialize(output)?;
+ }
+
+ 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());
diff --git a/sq/src/sq-usage.rs b/sq/src/sq-usage.rs
index 6a6df264..1185dfce 100644
--- a/sq/src/sq-usage.rs
+++ b/sq/src/sq-usage.rs
@@ -328,8 +328,9 @@
//! SUBCOMMANDS:
//! 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
+//! join Joins certs or certrings into a single certring
//! list Lists certs in a certring
+//! merge Merges certs or certrings into a single certring
//! split Splits a certring into individual certs
//! ```
//!
@@ -367,21 +368,31 @@
//! ### Subcommand certring join
//!
//! ```text
-//! Joins certs into a certring
+//! Unlike 'sq certring merge', multiple versions of the same certificate are not
+//! merged together.
//!
//! USAGE:
//! sq certring join [FLAGS] [OPTIONS] [FILE]...
//!
//! FLAGS:
-//! -B, --binary Emits binary data
-//! -h, --help Prints help information
-//! -V, --version Prints version information
+//! -B, --binary
+//! Don't ASCII-armor the certring
+//!
+//! -h, --help
+//! Prints help information
+//!
+//! -V, --version
+//! Prints version information
+//!
//!
//! OPTIONS:
-//! -o, --output <FILE> Writes to FILE or stdout if omitted
+//! -o, --output <FILE>
+//! Sets the output file to use
+//!
//!
//! ARGS:
-//! <FILE>... Reads from FILE
+//! <FILE>...
+//! Sets the input files to use
//! ```
//!
//! ### Subcommand certring list
@@ -400,6 +411,37 @@
//! <FILE> Reads from FILE or stdin if omitted
//! ```
//!
+//! ### Subcommand certring merge
+//!
+//! ```text
+//! Unlike 'sq certring join', the certificates are buffered and multiple versions
+//! of the same certificate are merged together. Where data is replaced (e.g.,
+//! secret key material), data from the later certificate is preferred.
+//!
+//! USAGE:
+//! sq certring merge [FLAGS] [OPTIONS] [FILE]...
+//!
+//! FLAGS:
+//! -B, --binary
+//! Emits binary data
+//!
+//! -h, --help
+//! Prints help information
+//!
+//! -V, --version
+//! Prints version information
+//!
+//!
+//! OPTIONS:
+//! -o, --output <FILE>
+//! Writes to FILE or stdout if omitted
+//!
+//!
+//! ARGS:
+//! <FILE>...
+//! Reads from FILE
+//! ```
+//!
//! ### Subcommand certring split
//!
//! ```text
diff --git a/sq/src/sq_cli.rs b/sq/src/sq_cli.rs
index bd37788c..6fa3201a 100644
--- a/sq/src/sq_cli.rs
+++ b/sq/src/sq_cli.rs
@@ -459,7 +459,31 @@ pub fn configure(app: App<'static, 'static>) -> App<'static, 'static> {
)
.subcommand(
SubCommand::with_name("join")
- .about("Joins certs into a certring")
+ .about("Joins certs or certrings into a single certring")
+ .long_about(
+ "Unlike 'sq certring merge', multiple versions \
+ of the same certificate are not merged \
+ together.")
+ .arg(Arg::with_name("input")
+ .value_name("FILE")
+ .multiple(true)
+ .help("Sets the input files to use"))
+ .arg(Arg::with_name("output")
+ .short("o").long("output").value_name("FILE")
+ .help("Sets the output file to use"))
+ .arg(Arg::with_name("binary")
+ .short("B").long("binary")
+ .help("Don't ASCII-armor the certring"))
+ )
+ .subcommand(
+ SubCommand::with_name("merge")
+ .about("Merges certs or certrings into a single certring")
+ .long_about(
+ "Unlike 'sq certring join', the certificates \
+ are buffered and multiple versions of the same \
+ certificate are merged together. Where data \
+ is replaced (e.g., secret key material), data \
+ from the later certificate is preferred.")
.arg(Arg::with_name("input")
.value_name("FILE")
.multiple(true)