summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNeal H. Walfield <neal@pep.foundation>2021-01-22 09:15:49 +0100
committerNeal H. Walfield <neal@pep.foundation>2021-01-22 09:18:55 +0100
commite5af90ca2a9ca0552d2b7c21384d6faa51323224 (patch)
tree01c04ada209f5c1f6908aa3620c4747c214e1983
parent02fc0a45dd1a34368c5a5526756c5416cf997b83 (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.rs23
-rw-r--r--sq/src/commands/sign.rs46
-rw-r--r--sq/src/sq-usage.rs45
-rw-r--r--sq/src/sq.rs27
-rw-r--r--sq/src/sq_cli.rs31
-rw-r--r--sq/tests/sq-certify.rs83
-rw-r--r--sq/tests/sq-sign.rs81
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, &notations, 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");