diff options
author | Nora Widdecke <nora@sequoia-pgp.org> | 2020-07-29 18:28:10 +0200 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2021-01-05 15:02:36 +0100 |
commit | 2786957fd6c03ec18d2bd5765c46b6aceb549e1a (patch) | |
tree | e5d7dbae0023b33fbc2c0a9a12b3532fb349d03f | |
parent | c6a81d92b35efb8175139b177ee5fadcf5ba6a24 (diff) |
tool: Add merge signatures command.
-rw-r--r-- | sq/src/commands/merge_signatures.rs | 156 | ||||
-rw-r--r-- | sq/src/commands/mod.rs | 2 | ||||
-rw-r--r-- | sq/src/sq-usage.rs | 51 | ||||
-rw-r--r-- | sq/src/sq.rs | 6 | ||||
-rw-r--r-- | sq/src/sq_cli.rs | 13 |
5 files changed, 213 insertions, 15 deletions
diff --git a/sq/src/commands/merge_signatures.rs b/sq/src/commands/merge_signatures.rs new file mode 100644 index 00000000..d7d38bb7 --- /dev/null +++ b/sq/src/commands/merge_signatures.rs @@ -0,0 +1,156 @@ +use anyhow::Context as _; +use std::io; + +extern crate sequoia_openpgp as openpgp; +use crate::create_or_stdout; +use crate::openpgp::packet::Literal; +use crate::openpgp::packet::Tag; +use crate::openpgp::parse::{PacketParser, PacketParserResult, Parse}; +use crate::openpgp::serialize::stream::{LiteralWriter, Message}; +use crate::openpgp::serialize::Serialize; +use crate::openpgp::{Packet, Result}; + +pub fn merge_signatures( + input1: &mut (dyn io::Read + Send + Sync), + input2: &mut (dyn io::Read + Send + Sync), + output_path: Option<&str>, +) -> Result<()> { + let parser1 = + PacketParser::from_reader(input1).context("Failed to build parser")?; + let parser2 = + PacketParser::from_reader(input2).context("Failed to build parser")?; + let mut output = create_or_stdout(output_path, false)?; + + let mut sink = Message::new(&mut output); + + let (ops1, post_ops_parser1) = read_while_by_tag(parser1, Tag::OnePassSig)?; + let (ops2, post_ops_parser2) = read_while_by_tag(parser2, Tag::OnePassSig)?; + + let ops1 = ops1 + .into_iter() + .map(ops_with_last_false) + .collect::<Result<Vec<_>>>()?; + + write_packets(ops1, &mut sink)?; + write_packets(ops2, &mut sink)?; + + let (sink_new, post_literal_parser1, post_literal_parser2) = + compare_and_write_literal(sink, post_ops_parser1, post_ops_parser2)?; + sink = sink_new; + + let (sigs2, _) = read_while_by_tag(post_literal_parser2, Tag::Signature)?; + let (sigs1, _) = read_while_by_tag(post_literal_parser1, Tag::Signature)?; + write_packets(sigs2, &mut sink)?; + write_packets(sigs1, &mut sink)?; + + sink.finalize().context("Failed to write data")?; + Ok(()) +} + +fn ops_with_last_false(p: Packet) -> Result<Packet> { + if let Packet::OnePassSig(mut ops) = p { + ops.set_last(false); + Ok(Packet::OnePassSig(ops)) + } else { + Err(anyhow::anyhow!("Not a OnePassSig packet")) + } +} + +fn write_packets(packets: Vec<Packet>, mut sink: &mut Message) -> Result<()> { + for packet in packets { + packet.serialize(&mut sink)?; + } + Ok(()) +} + +fn compare_and_write_literal<'a, 'b, 'c>( + sink: Message<'a>, + ppr1: PacketParserResult<'b>, + ppr2: PacketParserResult<'c>, +) -> Result<(Message<'a>, PacketParserResult<'b>, PacketParserResult<'c>)> { + // We want to compare the bodies of the literal packets, by comparing their digests. + // Digests are only known after reading the packets, so: + // First, move both parsers past the literal packet, copy out the body of one of them. + // Second, compare the packets which now include the correct hashes, + // normalize to ignore metadata. + let (mut lp1, ppr1) = read_while_by_tag(ppr1, Tag::Literal)?; + let lp1 = lp1.remove(0); + + let (sink, lp2, ppr2) = write_literal_(sink, ppr2)?; + + let lp1 = normalize_literal(lp1)?; + let lp2 = normalize_literal(lp2)?; + eprintln!("lp1: {:?}", lp1); + eprintln!("lp2: {:?}", lp2); + + if lp1 == lp2 { + Ok((sink, ppr1, ppr2)) + } else { + Err(anyhow::anyhow!("Literal Packets differ, aborting!")) + } +} + +// Clear date and filename. +fn normalize_literal(p: Packet) -> Result<Literal> { + if let Packet::Literal(mut l) = p { + l.set_date(None)?; + l.set_filename(&[])?; + Ok(l) + } else { + Err(anyhow::anyhow!("Not a literal packet")) + } +} + +fn write_literal_<'a, 'b>( + mut sink: Message<'a>, + ppr: PacketParserResult<'b>, +) -> Result<(Message<'a>, Packet, PacketParserResult<'b>)> { + if let PacketParserResult::Some(mut pp) = ppr { + // Assemble a new Literal packet. + // Cannot use packet.serialize because that does not include the body. + if let Packet::Literal(l) = pp.packet.clone() { + // Create a literal writer to wrap the data in a literal + // message packet. + let mut literal = LiteralWriter::new(sink) + .format(l.format()) + .build() + .context("Failed to create literal writer")?; + // Do not add any metadata as it is unprotected anyway. + + // Just copy all the data. + io::copy(&mut pp, &mut literal).context("Failed to copy data")?; + + // Pop the literal writer. + sink = literal + .finalize_one() + .context("Failed to write literal packet")? + .unwrap(); + } + + let foo = pp.recurse()?; //TODO rename + Ok((sink, foo.0, foo.1)) + } else { + Err(anyhow::anyhow!("Unexpected end of file")) + } +} + +fn read_while_by_tag( + mut ppr: PacketParserResult, + tag: Tag, +) -> Result<(Vec<Packet>, PacketParserResult)> { + let mut result = vec![]; + + while let PacketParserResult::Some(pp) = ppr { + let next_tag_matches = pp.header().ctb().tag() == tag; + if !next_tag_matches { + return Ok((result, PacketParserResult::Some(pp))); + } + + // Start parsing the next packet, recursing. + let (packet, next_ppr) = pp.recurse()?; + ppr = next_ppr; + result.push(packet); + } + + Ok((result, ppr)) +} diff --git a/sq/src/commands/mod.rs b/sq/src/commands/mod.rs index 06110976..59a9914d 100644 --- a/sq/src/commands/mod.rs +++ b/sq/src/commands/mod.rs @@ -38,6 +38,8 @@ pub use self::dump::dump; mod inspect; pub use self::inspect::inspect; pub mod key; +pub mod merge_signatures; +pub use self::merge_signatures::merge_signatures; /// 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 71ac8053..731dcfbf 100644 --- a/sq/src/sq-usage.rs +++ b/sq/src/sq-usage.rs @@ -22,21 +22,22 @@ //! -p, --policy <NETWORK-POLICY> Sets the network policy to use //! //! SUBCOMMANDS: -//! decrypt Decrypts an OpenPGP message -//! encrypt Encrypts a message -//! sign Signs a message -//! verify Verifies a message -//! mapping Interacts with key mappings -//! keyserver Interacts with keyservers -//! autocrypt Autocrypt support -//! 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) -//! inspect Inspects a sequence of OpenPGP packets -//! key Manipulates keys -//! list Lists key mappings and known keys -//! packet OpenPGP Packet manipulation -//! wkd Interacts with Web Key Directories +//! decrypt Decrypts an OpenPGP message +//! encrypt Encrypts a message +//! sign Signs a message +//! verify Verifies a message +//! mapping Interacts with key mappings +//! merge-signatures Merges two signatures +//! keyserver Interacts with keyservers +//! autocrypt Autocrypt support +//! 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) +//! inspect Inspects a sequence of OpenPGP packets +//! key Manipulates keys +//! list Lists key mappings and known keys +//! packet OpenPGP Packet manipulation +//! wkd Interacts with Web Key Directories //! ``` //! //! ## Subcommand decrypt @@ -291,6 +292,26 @@ //! <LABEL> Label to use //! ``` //! +//! ## Subcommand merge-signatures +//! +//! ```text +//! Merges two signatures +//! +//! USAGE: +//! sq merge-signatures [OPTIONS] [ARGS] +//! +//! FLAGS: +//! -h, --help Prints help information +//! -V, --version Prints version information +//! +//! OPTIONS: +//! -o, --output <FILE> Sets the output file to use +//! +//! ARGS: +//! <FILE> Sets the first input file to use +//! <FILE> Sets the second input file to use +//! ``` +//! //! ## Subcommand keyserver //! //! ```text diff --git a/sq/src/sq.rs b/sq/src/sq.rs index 42fc6dc3..f1bd4832 100644 --- a/sq/src/sq.rs +++ b/sq/src/sq.rs @@ -345,6 +345,12 @@ fn main() -> Result<()> { m.is_present("use-expired-subkey"), )?; }, + ("merge-signatures", Some(m)) => { + let mut input1 = open_or_stdin(m.value_of("input1"))?; + let mut input2 = open_or_stdin(m.value_of("input2"))?; + let output = m.value_of("output"); + commands::merge_signatures(&mut input1, &mut input2, output)?; + }, ("sign", Some(m)) => { let mut input = open_or_stdin(m.value_of("input"))?; let output = m.value_of("output"); diff --git a/sq/src/sq_cli.rs b/sq/src/sq_cli.rs index 316126e0..7397f20e 100644 --- a/sq/src/sq_cli.rs +++ b/sq/src/sq_cli.rs @@ -152,6 +152,19 @@ pub fn build() -> App<'static, 'static> { to using the one that expired last")) ) + .subcommand(SubCommand::with_name("merge-signatures") + .display_order(31) + .about("Merges two signatures") + .arg(Arg::with_name("input1").value_name("FILE") + .help("Sets the first input file to use")) + .arg(Arg::with_name("input2").value_name("FILE") + .help("Sets the second input file to use")) + .arg(Arg::with_name("output").value_name("FILE") + .long("output") + .short("o") + .help("Sets the output file to use")) + ) + .subcommand(SubCommand::with_name("sign") .display_order(25) .about("Signs a message") |