summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHeiko Schaefer <heiko@schaefer.name>2022-07-04 18:04:10 +0200
committerHeiko Schaefer <heiko@schaefer.name>2022-07-08 23:39:37 +0200
commited813eded4604ee306c7a48040eba00414b1b716 (patch)
tree7a0ae8b025c8ef435afe83572cd995792720c066
parent0f4b13505f7257aab32150864aa36ef3ffbccb6d (diff)
sq: Implement 'key userid strip'.heiko/workwork
-rw-r--r--sq/sq-subplot.md12
-rw-r--r--sq/src/commands/key.rs51
-rw-r--r--sq/src/sq-usage.rs59
-rw-r--r--sq/src/sq_cli.rs62
4 files changed, 182 insertions, 2 deletions
diff --git a/sq/sq-subplot.md b/sq/sq-subplot.md
index 56e8442b..d75a8e74 100644
--- a/sq/sq-subplot.md
+++ b/sq/sq-subplot.md
@@ -486,6 +486,18 @@ when I run sq inspect new.pgp
then stdout contains "UserID: Juliet"
~~~
+### Update a key by removing a User ID
+
+_Requirement: We must be able to generate a key with a User ID, and then strip the User ID._
+
+~~~scenario
+given an installed sq
+when I run sq key generate --userid "<juliet@example.org>" --export key.pgp
+when I run sq key userid strip --userid "<juliet@example.org>" --output new.pgp key.pgp
+when I run sq inspect new.pgp
+then stdout doesn't contain "UserID:"
+~~~
+
## Certificate extraction: `sq key extract-cert`
diff --git a/sq/src/commands/key.rs b/sq/src/commands/key.rs
index f29e8f8a..b132adf4 100644
--- a/sq/src/commands/key.rs
+++ b/sq/src/commands/key.rs
@@ -29,6 +29,7 @@ use crate::sq_cli::KeyGenerateCommand;
use crate::sq_cli::KeyPasswordCommand;
use crate::sq_cli::KeyUseridCommand;
use crate::sq_cli::KeyUseridAddCommand;
+use crate::sq_cli::KeyUseridStripCommand;
use crate::sq_cli::KeyExtractCertCommand;
use crate::sq_cli::KeyAdoptCommand;
use crate::sq_cli::KeyAttestCertificationsCommand;
@@ -311,6 +312,7 @@ fn extract_cert(config: Config, command: KeyExtractCertCommand) -> Result<()> {
fn userid(config: Config, command: KeyUseridCommand) -> Result<()> {
match command {
KeyUseridCommand::Add(c) => userid_add(config, c)?,
+ KeyUseridCommand::Strip(c) => userid_strip(config, c)?,
}
Ok(())
@@ -460,6 +462,55 @@ fn userid_add(config: Config, command: KeyUseridAddCommand) -> Result<()> {
Ok(())
}
+fn userid_strip(config: Config, command: KeyUseridStripCommand) -> Result<()> {
+ let input = open_or_stdin(command.io.input.as_deref())?;
+ let key = Cert::from_reader(input)?;
+
+ let orig_cert_valid = key.with_policy(&config.policy, None).is_ok();
+
+ let strip: Vec<_> = command.userid;
+
+ // Make sure that each User ID that the user requested to remove exists in
+ // `key`, and *can* be removed.
+ let key_userids: Vec<_> =
+ key.userids().map(|u| u.userid().value()).collect();
+
+ let missing: Vec<_> = strip.iter()
+ .filter(|s| !key_userids.contains(&s.as_bytes()))
+ .collect();
+ if ! missing.is_empty() {
+ return Err(anyhow::anyhow!(
+ "The certificate doesn't contain the User ID(s) {}.",
+ missing.iter().map(|s| format!("{:?}", s)).join(", ")));
+ }
+
+ let cert = key.retain_userids(|uid| {
+ // Don't keep User IDs that were selected for removal
+ ! strip.iter().any(|rm| rm.as_bytes() == uid.userid().value())
+ });
+
+ if orig_cert_valid {
+ if let Err(err) = cert.with_policy(&config.policy, None) {
+ eprintln!(
+"Removing the User ID(s) has resulted in a invalid key:
+{}
+
+You could create a direct key signature or update the self
+signatures on other User IDs to make the key valid again.",
+ err
+ );
+ }
+ }
+
+ let mut sink = config.create_or_stdout_safe(command.io.output.as_deref())?;
+ if command.binary {
+ cert.as_tsk().serialize(&mut sink)?;
+ } else {
+ cert.as_tsk().armored().serialize(&mut sink)?;
+ }
+ Ok(())
+}
+
fn adopt(config: Config, command: KeyAdoptCommand) -> Result<()> {
let input = open_or_stdin(command.certificate.as_deref())?;
let cert = Cert::from_reader(input)?;
diff --git a/sq/src/sq-usage.rs b/sq/src/sq-usage.rs
index 96bcc65b..8afcd1e2 100644
--- a/sq/src/sq-usage.rs
+++ b/sq/src/sq-usage.rs
@@ -540,7 +540,7 @@
//! ```text
//! Manages User IDs
//!
-//! Add User IDs to a key.
+//! Add User IDs to, or strip User IDs from a key.
//!
//! USAGE:
//! sq key userid <SUBCOMMAND>
@@ -552,6 +552,8 @@
//! SUBCOMMANDS:
//! add
//! Adds a User ID
+//! strip
+//! Strips a User ID
//! help
//! Print this message or the help of the given subcommand(s)
//! ```
@@ -614,6 +616,61 @@
//! --output juliet-new.key.pgp
//! ```
//!
+//! #### Subcommand key userid strip
+//!
+//! ```text
+//! Strips a User ID
+//!
+//! Note that this operation does not reliably remove User IDs from a
+//! certificate that has already been disseminated! (OpenPGP software
+//! typically appends new information it receives about a certificate
+//! to its local copy of that certificate. Systems that have obtained
+//! a copy of your certificate with the User ID that you are trying to
+//! strip will not drop that User ID from their copy.)
+//!
+//! In most cases, you will want to use the 'sq revoke userid' operation
+//! instead. That issues a revocation for a User ID, which can be used to mark
+//! the User ID as invalidated.
+//!
+//! However, this operation can be useful in very specific cases, in particular:
+//! to remove a mistakenly added User ID before it has been uploaded to key
+//! servers or otherwise shared.
+//!
+//! Stripping a User ID may change how a certificate is interpreted. This
+//! is because information about the certificate like algorithm preferences,
+//! the primary key's key flags, etc. is stored in the User ID's binding
+//! signature.
+//!
+//! USAGE:
+//! sq key userid strip [OPTIONS] [FILE]
+//!
+//! ARGS:
+//! <FILE>
+//! Reads from FILE or stdin if omitted
+//!
+//! OPTIONS:
+//! -B, --binary
+//! Emits binary data
+//!
+//! -h, --help
+//! Print help information
+//!
+//! -o, --output <FILE>
+//! Writes to FILE or stdout if omitted
+//!
+//! -u, --userid <USERID>
+//! The User IDs to strip. Values must exactly match a User ID.
+//!
+//! EXAMPLES:
+//!
+//! # First, this generates a key
+//! $ sq key generate --userid "<juliet@example.org>" --export juliet.key.pgp
+//!
+//! # Then, this strips a User ID
+//! $ sq key userid strip --userid "<juliet@example.org>" \
+//! --output juliet-new.key.pgp juliet.key.pgp
+//! ```
+//!
//! ### Subcommand key extract-cert
//!
//! ```text
diff --git a/sq/src/sq_cli.rs b/sq/src/sq_cli.rs
index e7141919..0e745570 100644
--- a/sq/src/sq_cli.rs
+++ b/sq/src/sq_cli.rs
@@ -1988,13 +1988,14 @@ pub struct KeyExtractCertCommand {
long_about =
"Manages User IDs
-Add User IDs to a key.
+Add User IDs to, or strip User IDs from a key.
",
subcommand_required = true,
arg_required_else_help = true,
)]
pub enum KeyUseridCommand {
Add(KeyUseridAddCommand),
+ Strip(KeyUseridStripCommand),
}
#[derive(Debug, Args)]
@@ -2063,6 +2064,65 @@ $ sq key userid add --userid \"Juliet\" --creation-time 20210628T1137+0200 \\
pub binary: bool,
}
+
+#[derive(Debug, Args)]
+#[clap(
+ display_order = 20,
+ about = "Strips a User ID",
+ long_about =
+"Strips a User ID
+
+Note that this operation does not reliably remove User IDs from a
+certificate that has already been disseminated! (OpenPGP software
+typically appends new information it receives about a certificate
+to its local copy of that certificate. Systems that have obtained
+a copy of your certificate with the User ID that you are trying to
+strip will not drop that User ID from their copy.)
+
+In most cases, you will want to use the 'sq revoke userid' operation
+instead. That issues a revocation for a User ID, which can be used to mark
+the User ID as invalidated.
+
+However, this operation can be useful in very specific cases, in particular:
+to remove a mistakenly added User ID before it has been uploaded to key
+servers or otherwise shared.
+
+Stripping a User ID may change how a certificate is interpreted. This
+is because information about the certificate like algorithm preferences,
+the primary key's key flags, etc. is stored in the User ID's binding
+signature.
+",
+ after_help =
+"EXAMPLES:
+
+# First, this generates a key
+$ sq key generate --userid \"<juliet@example.org>\" --export juliet.key.pgp
+
+# Then, this strips a User ID
+$ sq key userid strip --userid \"<juliet@example.org>\" \\
+ --output juliet-new.key.pgp juliet.key.pgp
+",
+)]
+pub struct KeyUseridStripCommand {
+ #[clap(flatten)]
+ pub io: IoArgs,
+ #[clap(
+ value_name = "USERID",
+ short,
+ long,
+ help = "User IDs to strip",
+ long_help = "The User IDs to strip. Values must exactly match a \
+User ID."
+ )]
+ pub userid: Vec<String>,
+ #[clap(
+ short = 'B',
+ long,
+ help = "Emits binary data",
+ )]
+ pub binary: bool,
+}
+
#[derive(Debug, Args)]
#[clap(
name = "adopt",