summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNeal H. Walfield <neal@pep.foundation>2021-01-20 13:55:17 +0100
committerNeal H. Walfield <neal@pep.foundation>2021-01-20 14:01:57 +0100
commitc2f802dd59a71f04f6010b25a897e36a017497ef (patch)
tree6f702fdea7fe1852c03bccd32f71ae29fd3d8a33
parentab3261cac0e6b017e7bd2fd9df8abc311a327f0f (diff)
sq: Add sq certify.
- Add the command 'sq certify' to certify a (User ID, Certificate).
-rw-r--r--sq/src/commands/certify.rs142
-rw-r--r--sq/src/commands/mod.rs1
-rw-r--r--sq/src/sq-usage.rs49
-rw-r--r--sq/src/sq.rs5
-rw-r--r--sq/src/sq_cli.rs84
-rw-r--r--sq/tests/sq-certify.rs166
6 files changed, 447 insertions, 0 deletions
diff --git a/sq/src/commands/certify.rs b/sq/src/commands/certify.rs
new file mode 100644
index 00000000..22a0772e
--- /dev/null
+++ b/sq/src/commands/certify.rs
@@ -0,0 +1,142 @@
+use std::time::{SystemTime, Duration};
+
+use sequoia_openpgp as openpgp;
+use openpgp::Result;
+use openpgp::cert::prelude::*;
+use openpgp::packet::prelude::*;
+use openpgp::parse::Parse;
+use openpgp::policy::Policy;
+use openpgp::serialize::Serialize;
+use openpgp::types::SignatureType;
+
+use crate::parse_duration;
+use crate::SECONDS_IN_YEAR;
+
+pub fn certify(p: &impl Policy, m: &clap::ArgMatches, _force: bool)
+ -> Result<()>
+{
+ let certifier = m.value_of("certifier").unwrap();
+ let cert = m.value_of("certificate").unwrap();
+ let userid = m.value_of("userid").unwrap();
+
+ let certifier = Cert::from_file(certifier)?;
+ let cert = Cert::from_file(cert)?;
+ let vc = cert.with_policy(p, None)?;
+
+ let trust_depth: u8 = m.value_of("depth")
+ .map(|s| s.parse()).unwrap_or(Ok(0))?;
+ let trust_amount: u8 = m.value_of("amount")
+ .map(|s| s.parse()).unwrap_or(Ok(120))?;
+ let regex = m.values_of("regex").map(|v| v.collect::<Vec<_>>())
+ .unwrap_or(vec![]);
+ if trust_depth == 0 && regex.len() > 0 {
+ return Err(
+ anyhow::format_err!("A regex only makes sense \
+ if the trust depth is greater than 0"));
+ }
+
+ let local = m.is_present("local");
+ let non_revocable = m.is_present("non-revocable");
+ let expires = m.value_of("expires");
+ let expires_in = m.value_of("expires-in");
+
+
+ // Find the matching User ID.
+ let mut u = None;
+ for ua in vc.userids() {
+ if let Ok(a_userid) = std::str::from_utf8(ua.userid().value()) {
+ if a_userid == userid {
+ u = Some(ua.userid());
+ break;
+ }
+ }
+ }
+
+ let userid = if let Some(userid) = u {
+ userid
+ } else {
+ eprintln!("User ID: '{}' not found.\nValid User IDs:", userid);
+ let mut have_valid = false;
+ for ua in vc.userids() {
+ if let Ok(u) = std::str::from_utf8(ua.userid().value()) {
+ have_valid = true;
+ eprintln!(" - {}", u);
+ }
+ }
+ if ! have_valid {
+ eprintln!(" - Certificate has no valid User IDs.");
+ }
+ return Err(anyhow::format_err!("No matching User ID found"));
+ };
+
+ // Create the certification.
+ let mut builder
+ = SignatureBuilder::new(SignatureType::GenericCertification);
+
+ if trust_depth != 0 || trust_amount != 120 {
+ builder = builder.set_trust_signature(trust_depth, trust_amount)?;
+ }
+
+ for regex in regex {
+ builder = builder.add_regular_expression(regex)?;
+ }
+
+ if local {
+ builder = builder.set_exportable_certification(false)?;
+ }
+
+ if non_revocable {
+ builder = builder.set_revocable(false)?;
+ }
+
+ match (expires, expires_in) {
+ (None, None) =>
+ // Default expiration.
+ builder = builder.set_signature_validity_period(
+ Duration::new(5 * SECONDS_IN_YEAR, 0))?,
+ (Some(t), None) if t == "never" =>
+ // The default is no expiration; there is nothing to do.
+ (),
+ (Some(t), None) => {
+ let now = builder.signature_creation_time()
+ .unwrap_or_else(std::time::SystemTime::now);
+ let expiration = SystemTime::from(
+ crate::parse_iso8601(t, chrono::NaiveTime::from_hms(0, 0, 0))?);
+ let validity = expiration.duration_since(now)?;
+ builder = builder.set_signature_creation_time(now)?
+ .set_signature_validity_period(validity)?;
+ },
+ (None, Some(d)) if d == "never" =>
+ // The default is no expiration; there is nothing to do.
+ (),
+ (None, Some(d)) => {
+ let d = parse_duration(d)?;
+ builder = builder.set_signature_validity_period(d)?;
+ },
+ (Some(_), Some(_)) => unreachable!("conflicting args"),
+ }
+
+
+ // Sign it.
+ let mut signer = certifier.primary_key().key().clone()
+ .parts_into_secret()?.into_keypair()?;
+
+ let certification = builder
+ .sign_userid_binding(
+ &mut signer,
+ cert.primary_key().component(),
+ userid)?;
+ let cert = cert.insert_packets(certification.clone())?;
+ assert!(cert.clone().into_packets().any(|p| {
+ match p {
+ Packet::Signature(sig) => sig == certification,
+ _ => false,
+ }
+ }));
+
+
+ // And export it.
+ cert.armored().serialize(&mut std::io::stdout())?;
+
+ Ok(())
+}
diff --git a/sq/src/commands/mod.rs b/sq/src/commands/mod.rs
index 83a4f398..45c1201c 100644
--- a/sq/src/commands/mod.rs
+++ b/sq/src/commands/mod.rs
@@ -44,6 +44,7 @@ pub use self::merge_signatures::merge_signatures;
pub mod certring;
#[cfg(feature = "net")]
pub mod net;
+pub mod certify;
/// 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 3c9d7d50..79b82885 100644
--- a/sq/src/sq-usage.rs
+++ b/sq/src/sq-usage.rs
@@ -35,6 +35,7 @@
//! dearmor Removes ASCII Armor from a file
//! inspect Inspects a sequence of OpenPGP packets
//! packet OpenPGP Packet manipulation
+//! certify Certify a User ID for a Certificate
//! help Prints this message or the help of the given
//! subcommand(s)
//! ```
@@ -791,6 +792,54 @@
//! ARGS:
//! <FILE> Sets the input file to use
//! ```
+//!
+//! ## Subcommand certify
+//!
+//! ```text
+//! Certify a User ID for a Certificate
+//!
+//! USAGE:
+//! sq certify [FLAGS] [OPTIONS] <CERTIFIER> <CERTIFICATE> <USERID>
+//!
+//! FLAGS:
+//! -h, --help Prints help information
+//! -l, --local Makes the certification a local certification.
+//! Normally, local certifications are not exported.
+//! --non-revocable Marks the certification as being non-revocable. That
+//! is, you cannot later revoke this certification. This
+//! should normally only be used with an expiration.
+//! -V, --version Prints version information
+//!
+//! OPTIONS:
+//! -a, --amount <TRUST_AMOUNT>
+//! The amount of trust. Values between 1 and 120 are meaningful. 120
+//! means fully trusted. Values less than 120 indicate the degree of
+//! trust. 60 is usually used for partially trusted. The default is
+//! 120.
+//! -d, --depth <TRUST_DEPTH>
+//! The trust depth (sometimes referred to as the trust level). 0 means
+//! a normal certification of <CERTIFICATE, USERID>. 1 means
+//! CERTIFICATE is also a trusted introducer, 2 means CERTIFICATE is a
+//! meta-trusted introducer, etc. The default is 0.
+//! --expires <TIME>
+//! Absolute time when the certification should expire, or 'never'.
+//!
+//! --expires-in <DURATION>
+//! Relative time when the certification should expire. Either
+//! 'N[ymwd]', for N years, months, weeks, or days, or 'never'. The
+//! default is 5 years.
+//! -r, --regex <REGEX>...
+//! Adds a regular expression to constrain what a trusted introducer can
+//! certify. The regular expression must match the certified User ID in
+//! all intermediate introducers, and the certified certificate.
+//! Multiple regular expressions may be specified. In that case, at
+//! least one must match.
+//!
+//! ARGS:
+//! <CERTIFIER> The key to certify the certificate.
+//! <CERTIFICATE> The certificate to certify.
+//! <USERID> The User ID to certify.
+//! ```
#![doc(html_favicon_url = "https://docs.sequoia-pgp.org/favicon.png")]
#![doc(html_logo_url = "https://docs.sequoia-pgp.org/logo.svg")]
diff --git a/sq/src/sq.rs b/sq/src/sq.rs
index b9d8e6ef..6a9f9a13 100644
--- a/sq/src/sq.rs
+++ b/sq/src/sq.rs
@@ -573,6 +573,11 @@ fn main() -> Result<()> {
#[cfg(feature = "net")]
("wkd", Some(m)) => commands::net::dispatch_wkd(config, m)?,
+
+ ("certify", Some(m)) => {
+ commands::certify::certify(policy, m, force)?;
+ },
+
_ => unreachable!(),
}
diff --git a/sq/src/sq_cli.rs b/sq/src/sq_cli.rs
index 692a1937..da527804 100644
--- a/sq/src/sq_cli.rs
+++ b/sq/src/sq_cli.rs
@@ -461,6 +461,90 @@ pub fn configure(app: App<'static, 'static>) -> App<'static, 'static> {
from stdin)")))
)
+
+ .subcommand(SubCommand::with_name("certify")
+ .about("Certify a User ID for a Certificate")
+ .arg(Arg::with_name("depth")
+ .value_name("TRUST_DEPTH")
+ .help("The trust depth (sometimes referred to as \
+ the trust level). 0 means a normal \
+ certification of <CERTIFICATE, USERID>. \
+ 1 means CERTIFICATE is also a trusted \
+ introducer, 2 means CERTIFICATE is a \
+ meta-trusted introducer, etc. The \
+ default is 0.")
+ .long("depth")
+ .short("d"))
+ .arg(Arg::with_name("amount")
+ .value_name("TRUST_AMOUNT")
+ .help("The amount of trust. \
+ Values between 1 and 120 are meaningful. \
+ 120 means fully trusted. \
+ Values less than 120 indicate the degree \
+ of trust. 60 is usually used for partially \
+ trusted. The default is 120.")
+ .long("amount")
+ .short("a"))
+ .arg(Arg::with_name("regex")
+ .value_name("REGEX")
+ .help("Adds a regular expression to constrain \
+ what a trusted introducer can certify. \
+ The regular expression must match \
+ the certified User ID in all intermediate \
+ introducers, and the certified certificate. \
+ Multiple regular expressions may be \
+ specified. In that case, at least \
+ one must match.")
+ .long("regex")
+ .short("r")
+ .multiple(true))
+ .arg(Arg::with_name("local")
+ .help("Makes the certification a local \
+ certification. Normally, local \
+ certifications are not exported.")
+ .long("local")
+ .short("l"))
+ .arg(Arg::with_name("non-revocable")
+ .help("Marks the certification as being non-revocable. \
+ That is, you cannot later revoke this \
+ certification. This should normally only \
+ be used with an expiration.")
+ .long("non-revocable"))
+
+ .group(ArgGroup::with_name("expiration-group")
+ .args(&["expires", "expires-in"]))
+ .arg(Arg::with_name("expires")
+ .value_name("TIME")
+ .long("expires")
+ .help("Absolute time when the certification should \
+ expire, or 'never'."))
+ .arg(Arg::with_name("expires-in")
+ .value_name("DURATION")
+ .long("expires-in")
+ // Catch negative numbers.
+ .allow_hyphen_values(true)
+ .help("Relative time when the certification should \
+ expire. Either 'N[ymwd]', for N years, \
+ months, weeks, or days, or 'never'. The \
+ default is 5 years."))
+
+ .arg(Arg::with_name("certifier")
+ .value_name("CERTIFIER")
+ .required(true)
+ .index(1)
+ .help("The key to certify the certificate."))
+ .arg(Arg::with_name("certificate")
+ .value_name("CERTIFICATE")
+ .required(true)
+ .index(2)
+ .help("The certificate to certify."))
+ .arg(Arg::with_name("userid")
+ .value_name("USERID")
+ .required(true)
+ .index(3)
+ .help("The User ID to certify."))
+ )
+
.subcommand(SubCommand::with_name("packet")
.display_order(610)
.about("OpenPGP Packet manipulation")
diff --git a/sq/tests/sq-certify.rs b/sq/tests/sq-certify.rs
new file mode 100644
index 00000000..7dbc8d99
--- /dev/null
+++ b/sq/tests/sq-certify.rs
@@ -0,0 +1,166 @@
+use std::fs::File;
+use std::time::Duration;
+
+use assert_cli;
+use assert_cli::Assert;
+use tempfile;
+use tempfile::TempDir;
+
+use sequoia_openpgp as openpgp;
+use openpgp::Result;
+use openpgp::cert::prelude::*;
+use openpgp::parse::Parse;
+use openpgp::serialize::Serialize;
+use openpgp::policy::StandardPolicy;
+
+#[test]
+fn sq_certify() -> Result<()> {
+ let tmp_dir = TempDir::new().unwrap();
+ let alice_pgp = tmp_dir.path().join("alice.pgp");
+ let bob_pgp = tmp_dir.path().join("bob.pgp");
+
+ let (alice, _) =
+ CertBuilder::general_purpose(None, Some("alice@example.org"))
+ .generate()?;
+ let mut file = File::create(&alice_pgp)?;
+ alice.as_tsk().serialize(&mut file)?;
+
+ let (bob, _) =
+ CertBuilder::general_purpose(None, Some("bob@example.org"))
+ .generate()?;
+ let mut file = File::create(&bob_pgp)?;
+ bob.serialize(&mut file)?;
+
+
+ // A simple certification.
+ Assert::cargo_binary("sq")
+ .with_args(
+ &["certify",
+ alice_pgp.to_str().unwrap(),
+ bob_pgp.to_str().unwrap(),
+ "bob@example.org",
+ ])
+ .stdout().satisfies(|output| {
+ let p = &StandardPolicy::new();
+
+ let cert = Cert::from_bytes(output).unwrap();
+ 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());
+
+ return true;
+ }
+ }
+
+ false
+ },
+ "Bad certification")
+ .unwrap();
+
+ // No expiry.
+ Assert::cargo_binary("sq")
+ .with_args(
+ &["certify",
+ alice_pgp.to_str().unwrap(),
+ bob_pgp.to_str().unwrap(),
+ "bob@example.org",
+ "--expires", "never"
+ ])
+ .stdout().satisfies(|output| {
+ let p = &StandardPolicy::new();
+
+ let cert = Cert::from_bytes(output).unwrap();
+ 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);
+ assert!(c.signature_validity_period().is_none());
+
+ return true;
+ }
+ }
+
+ false
+ },
+ "Bad certification")
+ .unwrap();
+
+ // Have alice certify bob@example.org for 0xB0B.
+ Assert::cargo_binary("sq")
+ .with_args(
+ &["certify",
+ alice_pgp.to_str().unwrap(),
+ bob_pgp.to_str().unwrap(),
+ "bob@example.org",
+ "--depth", "10",
+ "--amount", "5",
+ "--regex", "a",
+ "--regex", "b",
+ "--local",
+ "--non-revocable",
+ "--expires-in", "1d",
+ ])
+ .stdout().satisfies(|output| {
+ let p = &StandardPolicy::new();
+
+ let cert = Cert::from_bytes(output).unwrap();
+ 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(), Some((10, 5)));
+ assert_eq!(&c.regular_expressions().collect::<Vec<_>>()[..],
+ &[ b"a", b"b" ]);
+ assert_eq!(c.revocable(), Some(false));
+ assert_eq!(c.exportable_certification(), Some(false));
+ assert_eq!(c.signature_validity_period(),
+ Some(Duration::new(24 * 60 * 60, 0)));
+
+ return true;
+ }
+ }
+
+ false
+ },
+ "Bad certification")
+ .unwrap();
+
+ // It should fail if the User ID doesn't exist.
+ Assert::cargo_binary("sq")
+ .with_args(
+ &["certify",
+ alice_pgp.to_str().unwrap(),
+ bob_pgp.to_str().unwrap(),
+ "bob",
+ ])
+ .fails()
+ .unwrap();
+
+ Ok(())
+}