diff options
author | Neal H. Walfield <neal@pep.foundation> | 2022-01-18 13:10:32 +0100 |
---|---|---|
committer | Neal H. Walfield <neal@pep.foundation> | 2022-01-20 14:28:32 +0100 |
commit | 2ba7985bef91c670a1375d60ff40da4f2c106134 (patch) | |
tree | c20340c40c7482a72db82ac281e2a7c5b7df508c | |
parent | 2e5c48a52db6284be98e9bc7b6541c98106ec580 (diff) |
sq: Implement sq revoke userid.
- Generalize the existing code to handle revoking both certificates
and User IDs.
-rw-r--r-- | sq/src/commands/revoke.rs | 134 | ||||
-rw-r--r-- | sq/src/sq-usage.rs | 129 | ||||
-rw-r--r-- | sq/src/sq_cli.rs | 111 | ||||
-rw-r--r-- | sq/tests/sq-revoke.rs | 349 |
4 files changed, 640 insertions, 83 deletions
diff --git a/sq/src/commands/revoke.rs b/sq/src/commands/revoke.rs index 47b9c1ab..0aabf3a6 100644 --- a/sq/src/commands/revoke.rs +++ b/sq/src/commands/revoke.rs @@ -7,7 +7,9 @@ use openpgp::cert::prelude::*; use openpgp::Packet; use openpgp::packet::signature::subpacket::NotationData; use openpgp::packet::signature::subpacket::NotationDataFlags; +use openpgp::packet::UserID; use openpgp::parse::Parse; +use openpgp::policy::NullPolicy; use openpgp::Result; use openpgp::serialize::Serialize; use openpgp::types::ReasonForRevocation; @@ -19,13 +21,17 @@ use crate::{ parse_iso8601, }; +const NP: &NullPolicy = &NullPolicy::new(); + pub fn dispatch(config: Config, m: &clap::ArgMatches) -> Result<()> { enum Subcommand { Certificate, + UserID, } let (subcommand, m) = match m.subcommand() { ("certificate", Some(m)) => (Subcommand::Certificate, m), + ("userid", Some(m)) => (Subcommand::UserID, m), _ => unreachable!(), }; @@ -69,6 +75,13 @@ pub fn dispatch(config: Config, m: &clap::ArgMatches) -> Result<()> { _ => panic!("invalid values should be caught by clap"), } } + Subcommand::UserID => { + match &*reason { + "retired" => ReasonForRevocation::UIDRetired, + "unspecified" => ReasonForRevocation::Unspecified, + _ => panic!("invalid values should be caught by clap"), + } + } }; let message: &str = m.value_of("message").expect("required"); @@ -102,6 +115,22 @@ pub fn dispatch(config: Config, m: &clap::ArgMatches) -> Result<()> { config, private_key_store, cert, + None, + secret, + binary, + time, + reason, + message, + ¬ations)?; + } + Subcommand::UserID => { + let userid = m.value_of("userid").expect("required"); + + revoke( + config, + private_key_store, + cert, + Some(userid), secret, binary, time, @@ -117,6 +146,7 @@ pub fn dispatch(config: Config, m: &clap::ArgMatches) -> Result<()> { fn revoke(config: Config, private_key_store: Option<&str>, cert: openpgp::Cert, + userid: Option<&str>, secret: Option<openpgp::Cert>, binary: bool, time: Option<SystemTime>, @@ -157,29 +187,91 @@ key material")); let first_party = secret.fingerprint() == cert.fingerprint(); - let mut rev = CertRevocationBuilder::new() - .set_reason_for_revocation(reason, message.as_bytes())?; - if let Some(time) = time { - rev = rev.set_signature_creation_time(time)?; - } - for (critical, notation) in notations { - rev = rev.add_notation(notation.name(), - notation.value(), - Some(notation.flags().clone()), - *critical)?; - } - let rev = rev.build(&mut signer, &cert, None)?; - let rev = Packet::Signature(rev); + let rev: Packet = if let Some(userid) = userid { + // Create a revocation for a User ID. + + // Unless force is specified, we require the User ID to have a + // valid self signature under the Null policy. We use the + // Null policy and not the standard policy, because it is + // still useful to revoke a User ID whose self signature is no + // longer valid. For instance, the binding signature may use + // SHA-1. + if ! config.force { + let vc = cert.with_policy(NP, None)?; + let present = vc.userids().any(|u| { + if let Ok(u) = String::from_utf8(u.value().to_vec()) { + if u == userid { + return true; + } + } + false + }); + + if ! present { + 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::anyhow!("\ +The certificate does not contain the specified User ID. To create +a revocation certificate for that User ID anyways, specify '--force'")); + } + } + + let mut rev = UserIDRevocationBuilder::new() + .set_reason_for_revocation(reason, message.as_bytes())?; + if let Some(time) = time { + rev = rev.set_signature_creation_time(time)?; + } + for (critical, notation) in notations { + rev = rev.add_notation(notation.name(), + notation.value(), + Some(notation.flags().clone()), + *critical)?; + } + let rev = rev.build(&mut signer, &cert, &UserID::from(userid), None)?; + Packet::Signature(rev) + } else { + // Create a revocation for the certificate. + let mut rev = CertRevocationBuilder::new() + .set_reason_for_revocation(reason, message.as_bytes())?; + if let Some(time) = time { + rev = rev.set_signature_creation_time(time)?; + } + for (critical, notation) in notations { + rev = rev.add_notation(notation.name(), + notation.value(), + Some(notation.flags().clone()), + *critical)?; + } + let rev = rev.build(&mut signer, &cert, None)?; + Packet::Signature(rev) + }; - let packets: Vec<Packet> = if first_party { + let mut stub = None; + let packets: Vec<Packet> = if first_party && userid.is_none() { vec![ rev ] } else { - cert_stub(cert.clone(), &config.policy, time, None) - // If we fail to minimize the the certificate, just use as - // it. - .unwrap_or_else(|_err| cert.clone()) - // Now add the revocation certificate. - .insert_packets(rev)? + let s = match cert_stub(cert.clone(), &config.policy, time, + userid.map(UserID::from).as_ref()) + { + Ok(stub) => stub, + // We failed to create a stub. Just use the original + // certificate as is. + Err(_) => cert.clone(), + }; + + stub = Some(s.clone()); + + s.insert_packets(rev)? .into_packets() .collect() }; @@ -227,7 +319,7 @@ key material")); } }; - let headers = cert.armor_headers(); + let headers = stub.unwrap_or(cert).armor_headers(); let headers: Vec<_> = preface .iter() .map(|s| ("Comment", s.as_str())) diff --git a/sq/src/sq-usage.rs b/sq/src/sq-usage.rs index 0951f6f5..4b3ca85d 100644 --- a/sq/src/sq-usage.rs +++ b/sq/src/sq-usage.rs @@ -1659,6 +1659,7 @@ //! //! SUBCOMMANDS: //! certificate Revoke a certificate +//! userid Revoke a User ID //! help Prints this message or the help of the given //! subcommand(s) //! @@ -1667,6 +1668,10 @@ //! # Revoke a certificate. //! $ sq revoke certificate --time 20220101 --certificate juliet.pgp \ //! compromised "My parents went through my things, and found my backup." +//! +//! # Revoke a User ID. +//! $ sq revoke userid --time 20220101 --certificate juliet.pgp \ +//! "Juliet <juliet@capuleti.it>" retired "I've left the family." //! ``` //! //! ### Subcommand revoke certificate @@ -1782,6 +1787,110 @@ //! that in the future." //! ``` //! +//! ### Subcommand revoke userid +//! +//! ```text +//! +//! Revokes a User ID +//! +//! Creates a revocation certificate for a User ID. +//! +//! If "--revocation-key" is provided, then that key is used to create +//! the signature. If that key is different from the certificate being +//! revoked, this creates a third-party revocation. This is normally only +//! useful if the owner of the certificate designated the key to be a +//! designated revoker. +//! +//! If "--revocation-key" is not provided, then the certificate must +//! include a certification-capable key. +//! +//! USAGE: +//! sq revoke userid [FLAGS] [OPTIONS] <USERID> <REASON> <MESSAGE> +//! +//! FLAGS: +//! -B, --binary +//! Emits binary data +//! +//! -h, --help +//! Prints help information +//! +//! -V, --version +//! Prints version information +//! +//! +//! OPTIONS: +//! --certificate <FILE> +//! +//! Reads the certificate to revoke from FILE or stdin, if omitted. It +//! is +//! an error for the file to contain more than one certificate. +//! --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. +//! --private-key-store <KEY_STORE> +//! Provides parameters for private key store +//! +//! --revocation-key <FILE> +//! +//! Signs the revocation certificate using KEY. If the key is different +//! from the certificate, this creates a third-party revocation. If +//! this +//! option is not provided, and the certificate includes secret key +//! material, +//! then that key is used to sign the revocation certificate. +//! -t, --time <TIME> +//! +//! Chooses keys valid at the specified time and sets the revocation +//! certificate's creation time +//! +//! ARGS: +//! <USERID> +//! +//! +//! The User ID to revoke. By default, this must exactly match a +//! self-signed User ID. Use --force to generate a revocation +//! certificate +//! for a User ID, which is not self signed. +//! <REASON> +//! +//! The reason for the revocation. This must be either: retired, or +//! unspecified: +//! +//! - retired means that this User ID is no longer valid. This is +//! appropriate when someone leaves an organisation, and the +//! organisation does not have their secret key material. For +//! instance, if someone was part of Debian and retires, they would +//! use this to indicate that a Debian-specific User ID is no longer +//! valid. +//! +//! - unspecified means that a different reason applies. +//! +//! If the reason happened in the past, you should specify that using +//! the +//! --time argument. This allows OpenPGP implementations to more +//! accurately reason about objects whose validity depends on the +//! validity +//! of a User ID. [possible values: retired, unspecified] +//! <MESSAGE> +//! +//! A short, explanatory text that is shown to a viewer of the +//! revocation +//! certificate. It explains why the certificate has been revoked. For +//! instance, if Alice has created a new key, she would generate a +//! 'superceded' revocation certificate for her old key, and might +//! include +//! the message "I've created a new certificate, FINGERPRINT, please use +//! that in the future." +//! ``` +//! //! ### Subcommand revoke EXAMPLES: //! //! ```text @@ -1811,6 +1920,26 @@ //! //! For more information try --help //! ``` +//! +//! ### Subcommand revoke # +//! +//! ```text +//! +//! USAGE: +//! sq revoke <SUBCOMMAND> +//! +//! For more information try --help +//! ``` +//! +//! ### Subcommand revoke "Juliet +//! +//! ```text +//! +//! USAGE: +//! sq revoke <SUBCOMMAND> +//! +//! For more information try --help +//! ``` #![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_cli.rs b/sq/src/sq_cli.rs index 600bef3a..dad29929 100644 --- a/sq/src/sq_cli.rs +++ b/sq/src/sq_cli.rs @@ -1334,6 +1334,10 @@ certificate to a keyserver. # Revoke a certificate. $ sq revoke certificate --time 20220101 --certificate juliet.pgp \\ compromised \"My parents went through my things, and found my backup.\" + +# Revoke a User ID. +$ sq revoke userid --time 20220101 --certificate juliet.pgp \\ + \"Juliet <juliet@capuleti.it>\" retired \"I've left the family.\" ") .setting(AppSettings::SubcommandRequiredElseHelp) .subcommand(SubCommand::with_name("certificate") @@ -1445,6 +1449,113 @@ as being human readable.")) .arg(Arg::with_name("binary") .short("B").long("binary") .help("Emits binary data")) + ) + .subcommand(SubCommand::with_name("userid") + .display_order(110) + .about("Revoke a User ID") + .long_about(" +Revokes a User ID + +Creates a revocation certificate for a User ID. + +If \"--revocation-key\" is provided, then that key is used to create +the signature. If that key is different from the certificate being +revoked, this creates a third-party revocation. This is normally only +useful if the owner of the certificate designated the key to be a +designated revoker. + +If \"--revocation-key\" is not provided, then the certificate must +include a certification-capable key.") + + .arg(Arg::with_name("input") + .value_name("FILE") + .long("certificate") + .alias("cert") + .help(" +The certificate contain the User ID to revoke") + .long_help(" +Reads the certificate to revoke from FILE or stdin, if omitted. It is +an error for the file to contain more than one certificate.") + ) + .arg(Arg::with_name("secret-key-file") + .long("revocation-key").value_name("FILE") + .help("Signs the revocation certificate using KEY") + .long_help(" +Signs the revocation certificate using KEY. If the key is different +from the certificate, this creates a third-party revocation. If this +option is not provided, and the certificate includes secret key material, +then that key is used to sign the revocation certificate.") + ) + .arg(Arg::with_name("private-key-store") + .long("private-key-store").value_name("KEY_STORE") + .help("Provides parameters for private key store") + ) + .arg(Arg::with_name("userid") + .value_name("USERID") + .required(true) + .help("The User ID to revoke") + .long_help(" + +The User ID to revoke. By default, this must exactly match a +self-signed User ID. Use --force to generate a revocation certificate +for a User ID, which is not self signed.") + ) + .arg(Arg::with_name("reason") + .value_name("REASON") + .required(true) + .possible_values(&["retired", + "unspecified"]) + .help("The reason for the revocation") + .long_help(" +The reason for the revocation. This must be either: retired, or +unspecified: + + - retired means that this User ID is no longer valid. This is + appropriate when someone leaves an organisation, and the + organisation does not have their secret key material. For + instance, if someone was part of Debian and retires, they would + use this to indicate that a Debian-specific User ID is no longer + valid. + + - unspecified means that a different reason applies. + +If the reason happened in the past, you should specify that using the +--time argument. This allows OpenPGP implementations to more +accurately reason about objects whose validity depends on the validity +of a User ID.") + ) + .arg(Arg::with_name("message") + .value_name("MESSAGE") + .required(true) + .help("A short, explanatory text") + .long_help(" +A short, explanatory text that is shown to a viewer of the revocation +certificate. It explains why the certificate has been revoked. For +instance, if Alice has created a new key, she would generate a +'superceded' revocation certificate for her old key, and might include +the message \"I've created a new certificate, FINGERPRINT, please use +that in the future.\"") + ) + .arg(Arg::with_name("time") + .short("t").long("time").value_name("TIME") + .help(" +Chooses keys valid at the specified time and sets the revocation +certificate'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.")) + .arg(Arg::with_name("binary") + .short("B").long("binary") + .help("Emits binary data")) ) ); diff --git a/sq/tests/sq-revoke.rs b/sq/tests/sq-revoke.rs index 649ff746..3cbf0c33 100644 --- a/sq/tests/sq-revoke.rs +++ b/sq/tests/sq-revoke.rs @@ -12,6 +12,7 @@ use openpgp::Result; use openpgp::cert::prelude::*; use openpgp::parse::Parse; use openpgp::Packet; +use openpgp::packet::UserID; use openpgp::PacketPile; use openpgp::policy::StandardPolicy; use openpgp::serialize::Serialize; @@ -24,10 +25,17 @@ const TRACE: bool = false; mod integration { use super::*; - const _P: StandardPolicy = StandardPolicy::new(); - const P: &StandardPolicy = &_P; + const P: &StandardPolicy = &StandardPolicy::new(); - fn t(reason: ReasonForRevocation, + const ALICE: &str = "<alice@example.org>"; + + // USERIDS is a vector of User IDs. If it is empty, then a + // default User ID will be used and the *certificate* will be + // revoked. If it contains at least one entry, then each entry + // will be added as a User ID, and the last User ID will be + // revoked. + fn t(mut userids: Vec<&str>, + reason: ReasonForRevocation, reason_message: &str, stdin: bool, third_party: bool, @@ -40,21 +48,28 @@ mod integration { t - Duration::nanoseconds(t.timestamp_subsec_nanos() as i64) }); - // We're going to revoke alice's certificate. If we're doing - // it via a third-party revocation, then bob is the revoker. - // Otherwise, it's alice. - let (alice, _) = - CertBuilder::general_purpose(None, Some("alice@example.org")) - .set_creation_time( - time.map(|t| (t - Duration::hours(1)).into())) - .generate()?; + let gen = |userids: &[&str]| { + let mut builder = CertBuilder::new() + .set_creation_time( + time.map(|t| (t - Duration::hours(1)).into())); + for &u in userids { + builder = builder.add_userid(u); + } + builder.generate().map(|(key, _rev)| key) + }; - let (bob, _) = - CertBuilder::general_purpose(None, Some("bob@example.org")) - .set_creation_time( - time.map(|t| (t - Duration::hours(1)).into())) - .generate()?; + let userid = if userids.len() == 0 { + userids = vec![ ALICE ]; + None + } else { + Some(userids[userids.len() - 1]) + }; + // We're going to revoke alice's certificate or a User ID. If + // we're doing it via a third-party revocation, then bob is + // the revoker. Otherwise, it's alice. + let alice = gen(&userids)?; + let bob = gen(&[ "<revoker@some.org>" ])?; let mut cert = Vec::new(); alice.serialize(&mut cert)?; @@ -69,18 +84,30 @@ mod integration { // Build up the command line. let mut cmd = Command::cargo_bin("sq")?; - cmd.args([ - "revoke", - "certificate", - match reason { - ReasonForRevocation::KeyCompromised => "compromised", - ReasonForRevocation::KeyRetired => "retired", - ReasonForRevocation::KeySuperseded => "superseded", - ReasonForRevocation::Unspecified => "unspecified", - _ => panic!("Invalid reason: {}", reason), - }, - reason_message - ]); + cmd.arg("revoke"); + if let Some(userid) = userid { + cmd.args([ + "userid", userid, + match reason { + ReasonForRevocation::UIDRetired => "retired", + ReasonForRevocation::Unspecified => "unspecified", + _ => panic!("Invalid reason: {}", reason), + }, + reason_message + ]); + } else { + cmd.args([ + "certificate", + match reason { + ReasonForRevocation::KeyCompromised => "compromised", + ReasonForRevocation::KeyRetired => "retired", + ReasonForRevocation::KeySuperseded => "superseded", + ReasonForRevocation::Unspecified => "unspecified", + _ => panic!("Invalid reason: {}", reason), + }, + reason_message + ]); + } let _tmp_dir = match (third_party, stdin) { (true, true) => { @@ -177,35 +204,102 @@ mod integration { } // Get the revocation certificate. + { + let vc = alice.with_policy(P, time.map(Into::into)).unwrap(); + assert!(matches!(vc.revocation_status(), + RevocationStatus::NotAsFarAsWeKnow)); + + if let Some(userid) = userid { + let mut found = false; + for u in vc.userids() { + if u.value() == userid.as_bytes() { + assert!(matches!(u.revocation_status(), + RevocationStatus::NotAsFarAsWeKnow)); + + found = true; + break; + } + } + assert!(found, "User ID {} not found on certificate", userid); + } + } - assert!(matches!( - alice.with_policy(P, time.map(Into::into)).unwrap() - .revocation_status(), - RevocationStatus::NotAsFarAsWeKnow)); - - let sig = if third_party { + let sig = if third_party || userid.is_some() { // We should get a certificate stub. let result = Cert::from_bytes(&*stdout)?; - let status = result.with_policy(P, time.map(Into::into))? - .revocation_status(); - if let RevocationStatus::CouldBe(sigs) = status { - assert_eq!(sigs.len(), 1); - let sig = sigs.into_iter().next().unwrap(); - // Bob issued the revocation. - assert_eq!(sig.get_issuers().into_iter().next(), - Some(bob.fingerprint().into())); + let vc = result.with_policy(P, time.map(Into::into))?; - // Verify the revocation. - sig.clone() - .verify_primary_key_revocation( - &bob.primary_key(), - &alice.primary_key()) - .context("revocation is not valid")?; + // Make sure the certificate stub only contains the + // revoked User ID (the rest should be striped). + assert_eq!(vc.userids().count(), 1); - sig.clone() + // Get the revocation status of the revoked object. + let status = if let Some(userid) = userid { + let mut status = None; + for u in vc.userids() { + if u.value() == userid.as_bytes() { + status = Some(u.revocation_status()); + break; + } + } + if let Some(status) = status { + status + } else { + panic!("Revoked user ID {} not found on revoked certificate", + userid); + } + } else { + vc.revocation_status() + }; + + // Make sure the revocation status is sane. + if third_party { + if let RevocationStatus::CouldBe(sigs) = status { + assert_eq!(sigs.len(), 1); + let sig = sigs.into_iter().next().unwrap(); + + // Bob issued the revocation. + assert_eq!(sig.get_issuers().into_iter().next(), + Some(bob.fingerprint().into())); + + // Verify the revocation. + if let Some(userid) = userid { + sig.clone() + .verify_userid_revocation( + &bob.primary_key(), + &alice.primary_key(), + &UserID::from(userid)) + .context("revocation is not valid")?; + } else { + sig.clone() + .verify_primary_key_revocation( + &bob.primary_key(), + &alice.primary_key()) + .context("revocation is not valid")?; + } + + sig.clone() + } else { + panic!("Unexpected revocation status: {:?}", status); + } } else { - panic!("Unexpected revocation status: {:?}", status); + assert!(userid.is_some()); + if let RevocationStatus::Revoked(sigs) = status { + assert_eq!(sigs.len(), 1); + let sig = sigs.into_iter().next().unwrap(); + + // Alice issued the revocation. + assert_eq!(sig.get_issuers().into_iter().next(), + Some(alice.fingerprint().into())); + + // Since it is a self-siganture, sig has already + // been validated. + + sig.clone() + } else { + panic!("Unexpected revocation status: {:?}", status); + } } } else { // We should get just a single signature packet. @@ -234,7 +328,11 @@ mod integration { |