summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNeal H. Walfield <neal@pep.foundation>2022-01-20 13:52:35 +0100
committerNeal H. Walfield <neal@pep.foundation>2022-01-20 16:01:11 +0100
commit515604d85834ed2fa239f0a5fbd15d8a422a1271 (patch)
tree64e42ea375960a31abb7c315bd3e7b403b6f1428
parenta5c480ab9067f7b197e0b9f3d8a32bb5bc34eadf (diff)
sq: Implement sq revoke subkey.
-rw-r--r--sq/src/commands/revoke.rs333
-rw-r--r--sq/src/sq-usage.rs120
-rw-r--r--sq/src/sq_cli.rs119
-rw-r--r--sq/tests/sq-revoke.rs321
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,
- &notations)?;
- }
- Subcommand::UserID => {
- let userid = m.value_of("userid").expect("required");
-
- revoke(
- config,
- private_key_store,
- cert,
- Some(userid),
- secret,
- binary,
- time,
- reason,
- message,
- &notations)?;
- }
- }
+ revoke(
+ config,
+ private_key_store,
+ cert,
+ subcommand,
+ secret,
+ binary,
+ time,
+ reason,
+ message,
+ &notations)?;
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