diff options
author | Neal H. Walfield <neal@pep.foundation> | 2021-01-22 09:15:49 +0100 |
---|---|---|
committer | Neal H. Walfield <neal@pep.foundation> | 2021-01-22 09:18:55 +0100 |
commit | e5af90ca2a9ca0552d2b7c21384d6faa51323224 (patch) | |
tree | 01c04ada209f5c1f6908aa3620c4747c214e1983 | |
parent | 02fc0a45dd1a34368c5a5526756c5416cf997b83 (diff) |
sq: Add an option to add notations to signatures and certifications.
- Add the `--notation` option to `sq sign` and `sq certify` to add
notations to the generated signature.
-rw-r--r-- | sq/src/commands/certify.rs | 23 | ||||
-rw-r--r-- | sq/src/commands/sign.rs | 46 | ||||
-rw-r--r-- | sq/src/sq-usage.rs | 45 | ||||
-rw-r--r-- | sq/src/sq.rs | 27 | ||||
-rw-r--r-- | sq/src/sq_cli.rs | 31 | ||||
-rw-r--r-- | sq/tests/sq-certify.rs | 83 | ||||
-rw-r--r-- | sq/tests/sq-sign.rs | 81 |
7 files changed, 317 insertions, 19 deletions
diff --git a/sq/src/commands/certify.rs b/sq/src/commands/certify.rs index 1a19b8f9..11c23a5b 100644 --- a/sq/src/commands/certify.rs +++ b/sq/src/commands/certify.rs @@ -4,6 +4,7 @@ use sequoia_openpgp as openpgp; use openpgp::Result; use openpgp::cert::prelude::*; use openpgp::packet::prelude::*; +use openpgp::packet::signature::subpacket::NotationDataFlags; use openpgp::parse::Parse; use openpgp::policy::Policy; use openpgp::serialize::Serialize; @@ -117,6 +118,28 @@ pub fn certify(config: Config, p: &impl Policy, m: &clap::ArgMatches) (Some(_), Some(_)) => unreachable!("conflicting args"), } + // Each --notation takes two values. The iterator returns them + // one at a time, however. + if let Some(mut n) = m.values_of("notation") { + while let Some(name) = n.next() { + let value = n.next().unwrap(); + + let (critical, name) = if name.len() > 0 + && Some('!') == name.chars().next() + { + (true, &name[1..]) + } else { + (false, name) + }; + + builder = builder.add_notation( + name, + value, + NotationDataFlags::empty().set_human_readable(), + critical)?; + } + } + // Sign it. let mut signer = certifier.primary_key().key().clone() diff --git a/sq/src/commands/sign.rs b/sq/src/commands/sign.rs index 5c72d15a..df86ade4 100644 --- a/sq/src/commands/sign.rs +++ b/sq/src/commands/sign.rs @@ -8,7 +8,8 @@ use tempfile::NamedTempFile; use sequoia_openpgp as openpgp; use crate::openpgp::armor; use crate::openpgp::{Packet, Result}; -use crate::openpgp::packet::Signature; +use crate::openpgp::packet::prelude::*; +use crate::openpgp::packet::signature::subpacket::NotationData; use crate::openpgp::parse::{ Parse, PacketParserResult, @@ -18,6 +19,7 @@ use crate::openpgp::serialize::stream::{ Message, Armorer, Signer, LiteralWriter, }; use crate::openpgp::policy::Policy; +use crate::openpgp::types::SignatureType; use crate::{ create_or_stdout, create_or_stdout_pgp, @@ -28,22 +30,25 @@ pub fn sign(policy: &dyn Policy, output_path: Option<&str>, secrets: Vec<openpgp::Cert>, detached: bool, binary: bool, append: bool, notarize: bool, time: Option<SystemTime>, + notations: &[(bool, NotationData)], force: bool) -> Result<()> { match (detached, append|notarize) { (_, false) | (true, true) => sign_data(policy, input, output_path, secrets, detached, binary, - append, time, force), + append, time, notations, force), (false, true) => sign_message(policy, input, output_path, secrets, binary, notarize, - time, force), + time, notations, force), } } fn sign_data(policy: &dyn Policy, input: &mut dyn io::Read, output_path: Option<&str>, secrets: Vec<openpgp::Cert>, detached: bool, binary: bool, - append: bool, time: Option<SystemTime>, force: bool) + append: bool, time: Option<SystemTime>, + notations: &[(bool, NotationData)], + force: bool) -> Result<()> { let (mut output, prepend_sigs, tmp_path): (Box<dyn io::Write + Sync + Send>, Vec<Signature>, Option<PathBuf>) = @@ -103,7 +108,17 @@ fn sign_data(policy: &dyn Policy, Packet::Signature(sig).serialize(&mut message)?; } - let mut signer = Signer::new(message, keypairs.pop().unwrap()); + let mut builder = SignatureBuilder::new(SignatureType::Binary); + for (critical, n) in notations.iter() { + builder = builder.add_notation( + n.name(), + n.value(), + Some(n.flags().clone()), + *critical)?; + } + + let mut signer = Signer::with_template( + message, keypairs.pop().unwrap(), builder); for s in keypairs { signer = signer.add_signer(s); if let Some(time) = time { @@ -144,13 +159,15 @@ fn sign_message(policy: &dyn Policy, input: &mut (dyn io::Read + Sync + Send), output_path: Option<&str>, secrets: Vec<openpgp::Cert>, binary: bool, notarize: bool, - time: Option<SystemTime>, force: bool) + time: Option<SystemTime>, + notations: &[(bool, NotationData)], + force: bool) -> Result<()> { let mut output = create_or_stdout_pgp(output_path, force, binary, armor::Kind::Message)?; - sign_message_(policy, input, &mut output, secrets, notarize, time)?; + sign_message_(policy, input, &mut output, secrets, notarize, time, notations)?; output.finalize()?; Ok(()) } @@ -159,7 +176,8 @@ fn sign_message_(policy: &dyn Policy, input: &mut (dyn io::Read + Sync + Send), output: &mut (dyn io::Write + Sync + Send), secrets: Vec<openpgp::Cert>, notarize: bool, - time: Option<SystemTime>) + time: Option<SystemTime>, + notations: &[(bool, NotationData)]) -> Result<()> { let mut keypairs = super::get_signing_keys(&secrets, policy, time)?; @@ -231,7 +249,17 @@ fn sign_message_(policy: &dyn Policy, State::AfterFirstSigGroup => { // After the first signature group, we push the signer // onto the writer stack. - let mut signer = Signer::new(sink, keypairs.pop().unwrap()); + let mut builder = SignatureBuilder::new(SignatureType::Binary); + for (critical, n) in notations.iter() { + builder = builder.add_notation( + n.name(), + n.value(), + Some(n.flags().clone()), + *critical)?; + } + + let mut signer = Signer::with_template( + sink, keypairs.pop().unwrap(), builder); for s in keypairs.drain(..) { signer = signer.add_signer(s); if let Some(time) = time { diff --git a/sq/src/sq-usage.rs b/sq/src/sq-usage.rs index 8b2147df..1e214bc0 100644 --- a/sq/src/sq-usage.rs +++ b/sq/src/sq-usage.rs @@ -144,24 +144,46 @@ //! sq sign [FLAGS] [OPTIONS] [--] [FILE] //! //! FLAGS: -//! -a, --append Appends a signature to existing signature -//! -B, --binary Emits binary data -//! --detached Creates a detached signature -//! -h, --help Prints help information -//! -n, --notarize Signs a message and all existing signatures +//! -a, --append +//! Appends a signature to existing signature +//! +//! -B, --binary +//! Emits binary data +//! +//! --detached +//! Creates a detached signature +//! +//! -h, --help +//! Prints help information +//! +//! -n, --notarize +//! Signs a message and all existing signatures +//! //! //! OPTIONS: //! --merge <SIGNED-MESSAGE> //! Merges signatures from the input and SIGNED-MESSAGE //! -//! -o, --output <FILE> Writes to FILE or stdout if omitted -//! --signer-key <KEY>... Signs using KEY +//! --notation <NAME> <VALUE> +//! Adds a notation to the certification. A user-defined notation's +//! name must be of the form 'name@a.domain.you.control.org'. If the +//! notation's name starts with a !, then the notation is marked as +//! being critical. If a consumer of a signature doesn't understand a +//! critical notation, then it will ignore the signature. The notation +//! is marked as being human readable. +//! -o, --output <FILE> +//! Writes to FILE or stdout if omitted +//! +//! --signer-key <KEY>... +//! Signs using KEY +//! //! -t, --time <TIME> //! Chooses keys valid at the specified time and sets the signature's //! creation time //! //! ARGS: -//! <FILE> Reads from FILE or stdin if omitted +//! <FILE> +//! Reads from FILE or stdin if omitted //! ``` //! //! ## Subcommand verify @@ -514,6 +536,13 @@ //! --expires-in <DURATION> //! Makes the certification expire after DURATION. Either 'N[ymwd]', for //! N years, months, weeks, or days, or 'never'. [default: 5y] +//! --notation <NAME> <VALUE> +//! Adds a notation to the certification. A user-defined notation's +//! name must be of the form 'name@a.domain.you.control.org'. If the +//! notation's name starts with a !, then the notation is marked as +//! being critical. If a consumer of a signature doesn't understand a +//! critical notation, then it will ignore the signature. The notation +//! is marked as being human readable. //! -o, --output <FILE> //! Writes to FILE or stdout if omitted //! diff --git a/sq/src/sq.rs b/sq/src/sq.rs index a62c4922..15bb3f23 100644 --- a/sq/src/sq.rs +++ b/sq/src/sq.rs @@ -21,6 +21,8 @@ use crate::openpgp::fmt::hex; use crate::openpgp::types::KeyFlags; use crate::openpgp::packet::prelude::*; use crate::openpgp::parse::{Parse, PacketParser, PacketParserResult}; +use crate::openpgp::packet::signature::subpacket::NotationData; +use crate::openpgp::packet::signature::subpacket::NotationDataFlags; use crate::openpgp::serialize::{Serialize, stream::{Message, Armorer}}; use crate::openpgp::cert::prelude::*; use crate::openpgp::policy::StandardPolicy as P; @@ -436,8 +438,31 @@ fn main() -> Result<()> { let mut input2 = open_or_stdin(Some(merge))?; commands::merge_signatures(&mut input, &mut input2, output)?; } else { + // Each --notation takes two values. The iterator + // returns them one at a time, however. + let mut notations: Vec<(bool, NotationData)> = Vec::new(); + if let Some(mut n) = m.values_of("notation") { + while let Some(name) = n.next() { + let value = n.next().unwrap(); + + let (critical, name) = if name.len() > 0 + && Some('!') == name.chars().next() + { + (true, &name[1..]) + } else { + (false, name) + }; + + notations.push( + (critical, + NotationData::new( + name, value, + NotationDataFlags::empty().set_human_readable()))); + } + } + commands::sign(policy, &mut input, output, secrets, detached, - binary, append, notarize, time, force)?; + binary, append, notarize, time, ¬ations, force)?; } }, ("verify", Some(m)) => { diff --git a/sq/src/sq_cli.rs b/sq/src/sq_cli.rs index d92c458a..f1cc9253 100644 --- a/sq/src/sq_cli.rs +++ b/sq/src/sq_cli.rs @@ -180,6 +180,22 @@ pub fn configure(app: App<'static, 'static>) -> App<'static, 'static> { .short("t").long("time").value_name("TIME") .help("Chooses keys valid at the specified time and \ sets the signature's creation time")) + .arg(Arg::with_name("notation") + .value_names(&["NAME", "VALUE"]) + .long("notation") + .multiple(true).number_of_values(2) + .help("Adds a notation to the certification.") + .long_help( + "Adds a notation to the certification. \ + A user-defined notation's name must be of \ + the form 'name@a.domain.you.control.org'. \ + If the notation's name starts with a !, \ + then the notation is marked as being \ + critical. If a consumer of a signature \ + doesn't understand a critical notation, \ + then it will ignore the signature. The \ + notation is marked as being human readable.") + .conflicts_with("merge")) ) .subcommand(SubCommand::with_name("verify") @@ -598,6 +614,21 @@ pub fn configure(app: App<'static, 'static>) -> App<'static, 'static> { That is, you cannot later revoke this \ certification. This should normally only \ be used with an expiration.")) + .arg(Arg::with_name("notation") + .value_names(&["NAME", "VALUE"]) + .long("notation") + .multiple(true).number_of_values(2) + .help("Adds a notation to the certification.") + .long_help( + "Adds a notation to the certification. \ + A user-defined notation's name must be of \ + the form 'name@a.domain.you.control.org'. \ + If the notation's name starts with a !, \ + then the notation is marked as being \ + critical. If a consumer of a signature \ + doesn't understand a critical notation, \ + then it will ignore the signature. The \ + notation is marked as being human readable.")) .group(ArgGroup::with_name("expiration-group") .args(&["expires", "expires-in"])) diff --git a/sq/tests/sq-certify.rs b/sq/tests/sq-certify.rs index 81a2fdfe..ad50662f 100644 --- a/sq/tests/sq-certify.rs +++ b/sq/tests/sq-certify.rs @@ -9,9 +9,11 @@ use tempfile::TempDir; use sequoia_openpgp as openpgp; use openpgp::Result; use openpgp::cert::prelude::*; +use openpgp::packet::signature::subpacket::NotationData; +use openpgp::packet::signature::subpacket::NotationDataFlags; use openpgp::parse::Parse; -use openpgp::serialize::Serialize; use openpgp::policy::StandardPolicy; +use openpgp::serialize::Serialize; #[test] fn sq_certify() -> Result<()> { @@ -166,5 +168,84 @@ fn sq_certify() -> Result<()> { .fails() .unwrap(); + // With a notation. + Assert::cargo_binary("sq") + .with_args( + &["certify", + "--notation", "foo", "bar", + "--notation", "!foo", "xyzzy", + "--notation", "hello@example.org", "1234567890", + alice_pgp.to_str().unwrap(), + bob_pgp.to_str().unwrap(), + "bob@example.org", + ]) + .stdout().satisfies(|output| { + let p = &mut StandardPolicy::new(); + + let cert = Cert::from_bytes(output).unwrap(); + + // The standard policy will reject the + // certification, because it has an unknown + // critical notation. + let vc = cert.with_policy(p, None).unwrap(); + for ua in vc.userids() { + if ua.userid().value() == b"bob@example.org" { + let certifications: Vec<_> + = ua.certifications().collect(); + assert_eq!(certifications.len(), 0); + } + } + + // Accept the critical notation. + p.good_critical_notations(&["foo"]); + let vc = cert.with_policy(p, None).unwrap(); + + for ua in vc.userids() { + if ua.userid().value() == b"bob@example.org" { + let certifications: Vec<_> + = ua.certifications().collect(); + assert_eq!(certifications.len(), 1); + + let c = certifications[0]; + + assert_eq!(c.trust_signature(), None); + assert_eq!(c.regular_expressions().count(), 0); + assert_eq!(c.revocable().unwrap_or(true), true); + assert_eq!(c.exportable_certification().unwrap_or(true), true); + // By default, we set a duration. + assert!(c.signature_validity_period().is_some()); + + let hr = NotationDataFlags::empty().set_human_readable(); + let notations = &mut [ + (NotationData::new("foo", "bar", hr.clone()), false), + (NotationData::new("foo", "xyzzy", hr.clone()), false), + (NotationData::new("hello@example.org", "1234567890", hr), false) + ]; + + for n in c.notation_data() { + if n.name() == "salt@notations.sequoia-pgp.org" { + continue; + } + + for (m, found) in notations.iter_mut() { + if n == m { + assert!(!*found); + *found = true; + } + } + } + for (n, found) in notations.iter() { + assert!(found, "Missing: {:?}", n); + } + + return true; + } + } + + false + }, + "Bad certification") + .unwrap(); + Ok(()) } diff --git a/sq/tests/sq-sign.rs b/sq/tests/sq-sign.rs index a27ecd43..d9253124 100644 --- a/sq/tests/sq-sign.rs +++ b/sq/tests/sq-sign.rs @@ -10,6 +10,8 @@ use sequoia_openpgp as openpgp; use crate::openpgp::{Packet, PacketPile, Cert}; use crate::openpgp::crypto::KeyPair; use crate::openpgp::packet::key::SecretKeyMaterial; +use crate::openpgp::packet::signature::subpacket::NotationData; +use crate::openpgp::packet::signature::subpacket::NotationDataFlags; use crate::openpgp::types::{CompressionAlgorithm, SignatureType}; use crate::openpgp::parse::Parse; use crate::openpgp::serialize::stream::{Message, Signer, Compressor, LiteralWriter}; @@ -69,6 +71,85 @@ fn sq_sign() { } #[test] +fn sq_sign_with_notations() { + let tmp_dir = TempDir::new().unwrap(); + let sig = tmp_dir.path().join("sig0"); + + // Sign message. + Assert::cargo_binary("sq") + .with_args( + &["sign", + "--signer-key", + &artifact("keys/dennis-simon-anton-private.pgp"), + "--output", + &sig.to_string_lossy(), + "--notation", "foo", "bar", + "--notation", "!foo", "xyzzy", + "--notation", "hello@example.org", "1234567890", + &artifact("messages/a-cypherpunks-manifesto.txt")]) + .unwrap(); + + // Check that the content is sane. + let packets: Vec<Packet> = + PacketPile::from_file(&sig).unwrap().into_children().collect(); + assert_eq!(packets.len(), 3); + if let Packet::OnePassSig(ref ops) = packets[0] { + assert!(ops.last()); + assert_eq!(ops.typ(), SignatureType::Binary); + } else { + panic!("expected one pass signature"); + } + if let Packet::Literal(_) = packets[1] { + // Do nothing. + } else { + panic!("expected literal"); + } + if let Packet::Signature(ref sig) = packets[2] { + assert_eq!(sig.typ(), SignatureType::Binary); + + eprintln!("{:?}", sig); + + let hr = NotationDataFlags::empty().set_human_readable(); + let notations = &mut [ + (NotationData::new("foo", "bar", hr.clone()), false), + (NotationData::new("foo", "xyzzy", hr.clone()), false), + (NotationData::new("hello@example.org", "1234567890", hr), false) + ]; + + for n in sig.notation_data() { + if n.name() == "salt@notations.sequoia-pgp.org" { + continue; + } + + for (m, found) in notations.iter_mut() { + if n == m { + assert!(!*found); + *found = true; + } + } + } + for (n, found) in notations.iter() { + assert!(found, "Missing: {:?}", n); + } + } else { + panic!("expected signature"); + } + + let content = fs::read(&sig).unwrap(); + assert!(&content[..].starts_with(b"-----BEGIN PGP MESSAGE-----\n\n")); + + // Verify signed message. + Assert::cargo_binary("sq") + .with_args( + &["--known-notation", "foo", + "verify", + "--signer-cert", + &artifact("keys/dennis-simon-anton.pgp"), + &sig.to_string_lossy()]) + .unwrap(); +} + +#[test] fn sq_sign_append() { let tmp_dir = TempDir::new().unwrap(); let sig0 = tmp_dir.path().join("sig0"); |