diff options
author | Neal H. Walfield <neal@pep.foundation> | 2022-01-20 13:52:35 +0100 |
---|---|---|
committer | Neal H. Walfield <neal@pep.foundation> | 2022-01-20 16:01:11 +0100 |
commit | 515604d85834ed2fa239f0a5fbd15d8a422a1271 (patch) | |
tree | 64e42ea375960a31abb7c315bd3e7b403b6f1428 | |
parent | a5c480ab9067f7b197e0b9f3d8a32bb5bc34eadf (diff) |
sq: Implement sq revoke subkey.
-rw-r--r-- | sq/src/commands/revoke.rs | 333 | ||||
-rw-r--r-- | sq/src/sq-usage.rs | 120 | ||||
-rw-r--r-- | sq/src/sq_cli.rs | 119 | ||||
-rw-r--r-- | sq/tests/sq-revoke.rs | 321 |
4 files changed, 677 insertions, 216 deletions
diff --git a/sq/src/commands/revoke.rs b/sq/src/commands/revoke.rs index 167fe149..3a375492 100644 --- a/sq/src/commands/revoke.rs +++ b/sq/src/commands/revoke.rs @@ -4,6 +4,7 @@ use std::time::SystemTime; use sequoia_openpgp as openpgp; use openpgp::armor; use openpgp::cert::prelude::*; +use openpgp::KeyHandle; use openpgp::Packet; use openpgp::packet::signature::subpacket::NotationData; use openpgp::packet::signature::subpacket::NotationDataFlags; @@ -12,6 +13,7 @@ use openpgp::parse::Parse; use openpgp::policy::NullPolicy; use openpgp::Result; use openpgp::serialize::Serialize; +use openpgp::types::KeyFlags; use openpgp::types::ReasonForRevocation; use crate::{ commands::cert_stub, @@ -23,15 +25,44 @@ use crate::{ const NP: &NullPolicy = &NullPolicy::new(); -pub fn dispatch(config: Config, m: &clap::ArgMatches) -> Result<()> { - enum Subcommand { - Certificate, - UserID, +enum Subcommand { + Certificate, + Subkey(KeyHandle), + UserID(String), +} + +impl Subcommand { + fn is_certificate(&self) -> bool { + matches!(self, Subcommand::Certificate) } + fn userid(&self) -> Option<&str> { + if let Subcommand::UserID(userid) = self { + Some(userid) + } else { + None + } + } +} + +pub fn dispatch(config: Config, m: &clap::ArgMatches) -> Result<()> { let (subcommand, m) = match m.subcommand() { ("certificate", Some(m)) => (Subcommand::Certificate, m), - ("userid", Some(m)) => (Subcommand::UserID, m), + ("subkey", Some(m)) => { + let subkey = m.value_of("subkey").expect("required"); + let kh: KeyHandle = subkey + .parse() + .context( + format!("Parsing {:?} as an OpenPGP fingerprint or Key ID", + subkey))?; + + (Subcommand::Subkey(kh), m) + } + ("userid", Some(m)) => { + let userid = m.value_of("userid").expect("required"); + + (Subcommand::UserID(userid.into()), m) + } _ => unreachable!(), }; @@ -66,7 +97,7 @@ pub fn dispatch(config: Config, m: &clap::ArgMatches) -> Result<()> { let reason = m.value_of("reason").expect("required"); let reason = match subcommand { - Subcommand::Certificate => { + Subcommand::Certificate | Subcommand::Subkey(_) => { match &*reason { "compromised" => ReasonForRevocation::KeyCompromised, "superseded" => ReasonForRevocation::KeySuperseded, @@ -75,7 +106,7 @@ pub fn dispatch(config: Config, m: &clap::ArgMatches) -> Result<()> { _ => panic!("invalid values should be caught by clap"), } } - Subcommand::UserID => { + Subcommand::UserID(_) => { match &*reason { "retired" => ReasonForRevocation::UIDRetired, "unspecified" => ReasonForRevocation::Unspecified, @@ -108,36 +139,17 @@ pub fn dispatch(config: Config, m: &clap::ArgMatches) -> Result<()> { } } - match subcommand { - Subcommand::Certificate => { - revoke( - 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, - reason, - message, - ¬ations)?; - } - } + revoke( + config, + private_key_store, + cert, + subcommand, + secret, + binary, + time, + reason, + message, + ¬ations)?; Ok(()) } @@ -145,7 +157,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>, + subcommand: Subcommand, secret: Option<openpgp::Cert>, binary: bool, time: Option<SystemTime>, @@ -202,82 +214,133 @@ key material")); }; let first_party = secret.fingerprint() == cert.fingerprint(); + let mut subkey = None; + + let rev: Packet = match subcommand { + Subcommand::UserID(ref 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 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 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.as_str()), None)?; + Packet::Signature(rev) + } + Subcommand::Subkey(ref subkey_fpr) => { 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; - } + + for k in vc.keys().subkeys() { + if subkey_fpr.aliases(KeyHandle::from(k.fingerprint())) { + subkey = Some(k); + break; } - false - }); + } - if ! present { - eprintln!("User ID: '{}' not found.\nValid User IDs:", - userid); + if let Some(ref subkey) = subkey { + let mut rev = SubkeyRevocationBuilder::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, subkey.key(), None)?; + Packet::Signature(rev) + } else { + eprintln!("Subkey {} not found.\nValid subkeys:", + subkey_fpr.to_spaced_hex()); 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); - } + for k in vc.keys().subkeys() { + have_valid = true; + eprintln!(" - {} {} [{:?}]", + k.fingerprint().to_hex(), + chrono::DateTime::<chrono::offset::Utc> + ::from(k.creation_time()) + .date(), + k.key_flags().unwrap_or_else(KeyFlags::empty)); } if ! have_valid { - eprintln!(" - Certificate has no valid User IDs."); + eprintln!(" - Certificate has no subkeys."); } 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'")); +The certificate does not contain the specified subkey.")); } } - - 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)?; + Subcommand::Certificate => { + // 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 rev = rev.build(&mut signer, &cert, None)?; - Packet::Signature(rev) }; let mut stub = None; - let packets: Vec<Packet> = if first_party && userid.is_none() { + let packets: Vec<Packet> = if first_party && subcommand.is_certificate() { vec![ rev ] } else { - let s = match cert_stub(cert.clone(), &config.policy, time, - userid.map(UserID::from).as_ref()) + let mut s = match cert_stub( + cert.clone(), &config.policy, time, + subcommand.userid().map(UserID::from).as_ref()) { Ok(stub) => stub, // We failed to create a stub. Just use the original @@ -285,6 +348,13 @@ a revocation certificate for that User ID anyways, specify '--force'")); Err(_) => cert.clone(), }; + if let Some(ref subkey) = subkey { + s = s.insert_packets([ + Packet::from(subkey.key().clone()), + Packet::from(subkey.binding_signature().clone()) + ])?; + } + stub = Some(s.clone()); s.insert_packets(rev)? @@ -298,49 +368,50 @@ a revocation certificate for that User ID anyways, specify '--force'")); .context("serializing revocation certificate")?; } } else { - // Add some helpful ASCII-armor comments. - let mut revoker_fpr = None; - let mut revoker_uid = None; - - if ! first_party { - if let Ok(secret) = secret.with_policy(&config.policy, time) { - if let Ok(uid) = secret.primary_userid() { - revoker_uid = Some(uid); - } - } - - revoker_fpr = Some(secret.fingerprint()); - } + let cert = stub.as_ref().unwrap_or(&cert); - let preface = match (revoker_fpr, revoker_uid) { - (Some(fpr), Some(uid)) => { - let uid = String::from_utf8_lossy(uid.value()); - // Truncate it, if it is too long. - if uid.len() > 40 { - &uid[..40] - } else { - &uid - }; + // Add some more helpful ASCII-armor comments. + let mut more: Vec<String> = Vec::new(); - vec![format!("Revocation certificate by {}", - fpr.to_spaced_hex()), - format!("({:?}) for:", uid)] + // First, the thing that is being revoked. + match subcommand { + Subcommand::Certificate => { + more.push( + "including a revocation for the certificate".to_string()); } - (Some(fpr), None) => { - vec![format!("Revocation certificate by {} for:", - fpr.to_spaced_hex())] + Subcommand::Subkey(_) => { + more.push( + "including a revocation to revoke the subkey".to_string()); + more.push(subkey.unwrap().fingerprint().to_spaced_hex()); } - (_, _) => { - vec![("Revocation certificate for:".into())] + Subcommand::UserID(raw) => { + more.push( + "including a revocation to revoke the User ID".to_string()); + more.push(format!("{:?}", raw)); } - }; + } + + if ! first_party { + // Then if it was issued by a third-party. + more.push("issued by".to_string()); + more.push(secret.fingerprint().to_spaced_hex()); + if let Ok(vc) = cert.with_policy(&config.policy, time) { + if let Ok(uid) = vc.primary_userid() { + let uid = String::from_utf8_lossy(uid.value()); + // Truncate it, if it is too long. + more.push( + format!("{:?}", + uid.chars().take(70).collect::<String>())); + } + } + } - let headers = stub.unwrap_or(cert).armor_headers(); - let headers: Vec<_> = preface + let headers = cert.armor_headers(); + let headers: Vec<(&str, &str)> = headers .iter() .map(|s| ("Comment", s.as_str())) .chain( - headers + more .iter() .map(|value| ("Comment", value.as_str()))) .collect(); diff --git a/sq/src/sq-usage.rs b/sq/src/sq-usage.rs index 4b3ca85d..c928c0de 100644 --- a/sq/src/sq-usage.rs +++ b/sq/src/sq-usage.rs @@ -1659,6 +1659,7 @@ //! //! SUBCOMMANDS: //! certificate Revoke a certificate +//! subkey Revoke a subkey //! userid Revoke a User ID //! help Prints this message or the help of the given //! subcommand(s) @@ -1787,6 +1788,125 @@ //! that in the future." //! ``` //! +//! ### Subcommand revoke subkey +//! +//! ```text +//! +//! Revokes a subkey +//! +//! Creates a revocation certificate for a subkey. +//! +//! 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 subkey [FLAGS] [OPTIONS] <SUBKEY> <REASON> <MESSAGE> +//! +//! FLAGS: +//! -B, --binary +//! Emits binary data +//! +//! -h, --help +//! Prints help information +//! +//! -V, --version +//! Prints version information +//! +//! +//! OPTIONS: +//! --certificate <FILE> +//! +//! Reads the certificate containing the subkey 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: +//! <SUBKEY> +//! +//! The subkey to revoke. This must either be the subkey's Key ID or +//! its +//! fingerprint. +//! <REASON> +//! +//! The reason for the revocation. This must be either: compromised, +//! superseded, retired, or unspecified: +//! +//! - compromised means that the secret key material may have been +//! compromised. Prefer this value if you suspect that the secret +//! key +//! has been leaked. +//! +//! - superseded means that the owner of the certificate has replaced +//! it +//! with a new certificate. Prefer "compromised" if the secret key +//! material has been compromised even if the certificate is also +//! being replaced! You should include the fingerprint of the new +//! certificate in the message. +//! +//! - retired means that this certificate should not be used anymore, +//! and there is no replacement. This is appropriate when someone +//! leaves an organisation. Prefer "compromised" if the secret key +//! material has been compromised even if the certificate is also +//! being retired! You should include how to contact the owner, or +//! who to contact instead in the message. +//! +//! - unspecified means that none of the three other three reasons +//! apply. OpenPGP implementations conservatively treat this type +//! of +//! revocation similar to a compromised key. +//! +//! 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 the certificate. [possible values: compromised, superseded, +//! retired, unspecified] +//! <MESSAGE> +//! +//! A short, explanatory text that is shown to a viewer of the +//! revocation +//! certificate. It explains why the subkey 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 subkey, please refresh the +//! certificate." +//! ``` +//! //! ### Subcommand revoke userid //! //! ```text diff --git a/sq/src/sq_cli.rs b/sq/src/sq_cli.rs index dad29929..865d8184 100644 --- a/sq/src/sq_cli.rs +++ b/sq/src/sq_cli.rs @@ -1450,6 +1450,125 @@ as being human readable.")) .short("B").long("binary") .help("Emits binary data")) ) + .subcommand(SubCommand::with_name("subkey") + .display_order(105) + .about("Revoke a subkey") + .long_about(" +Revokes a subkey + +Creates a revocation certificate for a subkey. + +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 containing the subkey to revoke") + .long_help(" +Reads the certificate containing the subkey 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("subkey") + .value_name("SUBKEY") + .required(true) + .help("The subkey to revoke") + .long_help(" +The subkey to revoke. This must either be the subkey's Key ID or its +fingerprint.") + ) + .arg(Arg::with_name("reason") + .value_name("REASON") + .required(true) + .possible_values(&["compromised", + "superseded", + "retired", + "unspecified"]) + .help("The reason for the revocation") + .long_help(" +The reason for the revocation. This must be either: compromised, +superseded, retired, or unspecified: + + - compromised means that the secret key material may have been + compromised. Prefer this value if you suspect that the secret key + has been leaked. + + - superseded means that the owner of the certificate has replaced it + with a new certificate. Prefer \"compromised\" if the secret key + material has been compromised even if the certificate is also + being replaced! You should include the fingerprint of the new + certificate in the message. + + - retired means that this certificate should not be used anymore, + and there is no replacement. This is appropriate when someone + leaves an organisation. Prefer \"compromised\" if the secret key + material has been compromised even if the certificate is also + being retired! You should include how to contact the owner, or + who to contact instead in the message. + + - unspecified means that none of the three other three reasons + apply. OpenPGP implementations conservatively treat this type of + revocation similar to a compromised key. + +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 the certificate.") + ) + .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 subkey 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 subkey, please refresh the certificate.\"") + ) + .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")) + ) .subcommand(SubCommand::with_name("userid") .display_order(110) .about("Revoke a User ID") diff --git a/sq/tests/sq-revoke.rs b/sq/tests/sq-revoke.rs index 3cbf0c33..d0e233ad 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::Key; use openpgp::packet |