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 17:44:46 +0200
commit0f4b13505f7257aab32150864aa36ef3ffbccb6d (patch)
treefb11935b668154ecd097ad5f41654331967687d9
parent3e95c56ee010d91968d23df1e5eafbce601803b5 (diff)
sq: Implement 'key userid add'.
-rw-r--r--sq/sq-subplot.md12
-rw-r--r--sq/src/commands/key.rs158
-rw-r--r--sq/src/commands/mod.rs1
-rw-r--r--sq/src/sq-usage.rs81
-rw-r--r--sq/src/sq_cli.rs85
5 files changed, 335 insertions, 2 deletions
diff --git a/sq/sq-subplot.md b/sq/sq-subplot.md
index 269716f4..56e8442b 100644
--- a/sq/sq-subplot.md
+++ b/sq/sq-subplot.md
@@ -474,6 +474,18 @@ when I run sq inspect key.pgp
then stdout contains "Secret key: Encrypted"
~~~
+### Update a key by adding a User ID
+
+_Requirement: We must be able to generate a key and add a User ID to it._
+
+~~~scenario
+given an installed sq
+when I run sq key generate --export key.pgp
+when I run sq key userid add --userid "Juliet" --output new.pgp key.pgp
+when I run sq inspect new.pgp
+then stdout contains "UserID: Juliet"
+~~~
+
## Certificate extraction: `sq key extract-cert`
diff --git a/sq/src/commands/key.rs b/sq/src/commands/key.rs
index dec0102d..f29e8f8a 100644
--- a/sq/src/commands/key.rs
+++ b/sq/src/commands/key.rs
@@ -10,7 +10,7 @@ use crate::openpgp::cert::prelude::*;
use crate::openpgp::packet::prelude::*;
use crate::openpgp::packet::signature::subpacket::SubpacketTag;
use crate::openpgp::parse::Parse;
-use crate::openpgp::policy::Policy;
+use crate::openpgp::policy::{Policy, HashAlgoSecurity};
use crate::openpgp::serialize::Serialize;
use crate::openpgp::types::KeyFlags;
use crate::openpgp::types::SignatureType;
@@ -18,6 +18,7 @@ use crate::openpgp::types::SignatureType;
use crate::{
open_or_stdin,
};
+use crate::commands::get_primary_keys;
use crate::Config;
use crate::SECONDS_IN_YEAR;
use crate::parse_duration;
@@ -26,6 +27,8 @@ use crate::decrypt_key;
use crate::sq_cli::KeyCommand;
use crate::sq_cli::KeyGenerateCommand;
use crate::sq_cli::KeyPasswordCommand;
+use crate::sq_cli::KeyUseridCommand;
+use crate::sq_cli::KeyUseridAddCommand;
use crate::sq_cli::KeyExtractCertCommand;
use crate::sq_cli::KeyAdoptCommand;
use crate::sq_cli::KeyAttestCertificationsCommand;
@@ -35,6 +38,7 @@ pub fn dispatch(config: Config, command: KeyCommand) -> Result<()> {
match command.subcommand {
Generate(c) => generate(config, c)?,
Password(c) => password(config, c)?,
+ Userid(c) => userid(config, c)?,
ExtractCert(c) => extract_cert(config, c)?,
Adopt(c) => adopt(config, c)?,
AttestCertifications(c) => attest_certifications(config, c)?,
@@ -304,6 +308,158 @@ fn extract_cert(config: Config, command: KeyExtractCertCommand) -> Result<()> {
Ok(())
}
+fn userid(config: Config, command: KeyUseridCommand) -> Result<()> {
+ match command {
+ KeyUseridCommand::Add(c) => userid_add(config, c)?,
+ }
+
+ Ok(())
+}
+
+fn userid_add(config: Config, command: KeyUseridAddCommand) -> Result<()> {
+ let input = open_or_stdin(command.io.input.as_deref())?;
+ let key = Cert::from_reader(input)?;
+
+ // Fail if any of the User IDs to add already exist in the ValidCert
+ let key_userids: Vec<_> =
+ key.userids().map(|u| u.userid().value()).collect();
+ let exists: Vec<_> = command.userid.iter()
+ .filter(|s| key_userids.contains(&s.as_bytes()))
+ .collect();
+ if ! exists.is_empty() {
+ return Err(anyhow::anyhow!(
+ "The certificate already contains the User ID(s) {}.",
+ exists.iter().map(|s| format!("{:?}", s)).join(", ")));
+ }
+
+ let creation_time =
+ command.creation_time.map(|t| SystemTime::from(t.time));
+
+ // If a password is needed to use the key, the user will be prompted.
+ let pk = get_primary_keys(&[key.clone()], &config.policy,
+ command.private_key_store.as_deref(),
+ creation_time, None)?;
+
+ assert_eq!(pk.len(), 1, "Expect exactly one result from get_primary_keys()");
+ let mut pk = pk.into_iter().next().unwrap();
+
+ let vcert = key.with_policy(&config.policy, creation_time)
+ .with_context(|| format!("Certificate {} is not valid",
+ key.fingerprint()))?;
+
+ // Use the primary User ID or direct key signature as template for the
+ // SignatureBuilder.
+ //
+ // XXX: Long term, this functionality belongs next to
+ // openpgp/src/cert/builder/key.rs.
+ let mut sb =
+ if let Ok(primary_user_id) = vcert.primary_userid() {
+ SignatureBuilder::from(primary_user_id.binding_signature().clone())
+ } else if let Ok(direct_key_sig) = vcert.direct_key_signature() {
+ SignatureBuilder::from(direct_key_sig.clone())
+ .set_type(SignatureType::PositiveCertification)
+ } else {
+ // If there is neither a valid uid binding signature, nor a
+ // valid direct key signature, we shouldn't have gotten a
+ // ValidCert above.
+ unreachable!("ValidCert has to have one of the above.")
+ };
+
+ // Remove bad algorithms from preferred algorithm subpackets,
+ // and make sure preference lists contain at least one good algorithm.
+
+ // - symmetric_algorithms
+ let mut symmetric_algorithms: Vec<_> =
+ sb.preferred_symmetric_algorithms().unwrap_or(&[]).to_vec();
+ symmetric_algorithms
+ .retain(|algo| config.policy.symmetric_algorithm(*algo).is_ok());
+ if symmetric_algorithms.is_empty() {
+ symmetric_algorithms.push(Default::default());
+ }
+ sb = sb.set_preferred_symmetric_algorithms(symmetric_algorithms)?;
+
+ // - hash_algorithms
+ let mut hash_algorithms: Vec<_> =
+ sb.preferred_hash_algorithms().unwrap_or(&[]).to_vec();
+ hash_algorithms.retain(|algo|
+ config.policy
+ .hash_cutoff(*algo, HashAlgoSecurity::CollisionResistance)
+ .map(|cutoff| cutoff.lt(&SystemTime::now()))
+ .unwrap_or(true)
+ );
+ if hash_algorithms.is_empty() {
+ hash_algorithms.push(Default::default());
+ }
+ sb = sb.set_preferred_hash_algorithms(hash_algorithms)?;
+
+
+ // Remove the following types of SubPacket, if they exist
+ const REMOVE_SUBPACKETS: &[SubpacketTag] = &[
+ // The Signature should be exportable.
+ // https://openpgp-wg.gitlab.io/rfc4880bis/#name-exportable-certification
+ // "If this packet is not present, the certification is exportable;
+ // it is equivalent to a flag containing a 1."
+ SubpacketTag::ExportableCertification,
+
+ // PreferredAEADAlgorithms has been removed by WG.
+ // It was replaced by `39 Preferred AEAD Ciphersuites`,
+ // see https://openpgp-wg.gitlab.io/rfc4880bis/#section-5.2.3.5-7)
+ SubpacketTag::PreferredAEADAlgorithms,
+
+ // Strip the primary userid SubPacket
+ // (don't implicitly make a User ID primary)
+ SubpacketTag::PrimaryUserID,
+
+ // Other SubPacket types that shouldn't be in use in this context
+ SubpacketTag::TrustSignature,
+ SubpacketTag::RegularExpression,
+ SubpacketTag::SignersUserID,
+ SubpacketTag::ReasonForRevocation,
+ SubpacketTag::SignatureTarget,
+ SubpacketTag::EmbeddedSignature,
+ SubpacketTag::AttestedCertifications,
+ ];
+
+ sb = sb.modify_hashed_area(|mut subpacket_area| {
+ REMOVE_SUBPACKETS.iter()
+ .for_each(|sp| subpacket_area.remove_all(*sp));
+
+ Ok(subpacket_area)
+ })?;
+
+ // New User ID should only be made primary if explicitly specified by user.
+ // xxx: add a parameter to set a new user id as primary?
+
+
+ // Collect packets to add to the key (new User IDs and binding signatures)
+ let mut add: Vec<Packet> = vec![];
+
+ // Make new User IDs and binding signatures
+ for uid in command.userid {
+ let uid: UserID = uid.into();
+ add.push(uid.clone().into());
+
+ // Creation time.
+ if let Some(t) = creation_time {
+ sb = sb.set_signature_creation_time(t)?;
+ };
+
+ let binding = uid.bind(&mut pk, &key, sb.clone())?;
+ add.push(binding.into());
+ }
+
+ // Merge additional User IDs into key
+ let cert = key.insert_packets(add)?;
+
+ 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/commands/mod.rs b/sq/src/commands/mod.rs
index 7b0ecbf1..ca91bc58 100644
--- a/sq/src/commands/mod.rs
+++ b/sq/src/commands/mod.rs
@@ -206,7 +206,6 @@ fn get_keys<C>(certs: &[C], p: &dyn Policy,
///
/// This returns one key for each Cert. If a Cert doesn't have an
/// appropriate key, then this returns an error.
-#[allow(unused)]
fn get_primary_keys<C>(certs: &[C], p: &dyn Policy,
private_key_store: Option<&str>,
timestamp: Option<SystemTime>,
diff --git a/sq/src/sq-usage.rs b/sq/src/sq-usage.rs
index 0bfec102..96bcc65b 100644
--- a/sq/src/sq-usage.rs
+++ b/sq/src/sq-usage.rs
@@ -375,6 +375,8 @@
//! Generates a new key
//! password
//! Changes password protecting secrets
+//! userid
+//! Manages User IDs
//! extract-cert
//! Converts a key to a cert
//! attest-certifications
@@ -533,6 +535,85 @@
//! $ sq key password --clear < juliet.encrypted_key.pgp > juliet.decrypted_key.pgp
//! ```
//!
+//! ### Subcommand key userid
+//!
+//! ```text
+//! Manages User IDs
+//!
+//! Add User IDs to a key.
+//!
+//! USAGE:
+//! sq key userid <SUBCOMMAND>
+//!
+//! OPTIONS:
+//! -h, --help
+//! Print help information
+//!
+//! SUBCOMMANDS:
+//! add
+//! Adds a User ID
+//! help
+//! Print this message or the help of the given subcommand(s)
+//! ```
+//!
+//! #### Subcommand key userid add
+//!
+//! ```text
+//! Adds a User ID
+//!
+//! A User ID can contain a name, like "Juliet" or an email address, like
+//! "<juliet@example.org>". Historically, a name and email address were often
+//! combined as a single User ID, like "Juliet <juliet@example.org>".
+//!
+//! USAGE:
+//! sq key userid add [OPTIONS] [FILE]
+//!
+//! ARGS:
+//! <FILE>
+//! Reads from FILE or stdin if omitted
+//!
+//! OPTIONS:
+//! -B, --binary
+//! Emits binary data
+//!
+//! --creation-time <CREATION_TIME>
+//! Sets the creation time of this User ID's binding signature to TIME.
+//! TIME is interpreted as an ISO 8601 timestamp. To set the creation
+//! time to June 28, 2022 at midnight UTC, you can do:
+//!
+//! $ sq key userid add --userid "Juliet" --creation-time 20210628 \
+//! juliet.key.pgp --output juliet-new.key.pgp
+//!
+//! To include a time, add a T, the time and optionally the timezone
+//! (the
+//! default timezone is UTC):
+//!
+//! $ sq key userid add --userid "Juliet" --creation-time
+//! 20210628T1137+0200 \
+//! juliet.key.pgp --output juliet-new.key.pgp
+//!
+//! -h, --help
+//! Print help information
+//!
+//! -o, --output <FILE>
+//! Writes to FILE or stdout if omitted
+//!
+//! --private-key-store <KEY_STORE>
+//! Provides parameters for private key store
+//!
+//! -u, --userid <USERID>
+//! User ID to add
+//!
+//! EXAMPLES:
+//!
+//! # First, this generates a key
+//! $ sq key generate --userid "<juliet@example.org>" --export juliet.key.pgp
+//!
+//! # Then, this adds a User ID
+//! $ sq key userid add --userid "Juliet" juliet.key.pgp \
+//! --output juliet-new.key.pgp
+//! ```
+//!
//! ### Subcommand key extract-cert
//!
//! ```text
diff --git a/sq/src/sq_cli.rs b/sq/src/sq_cli.rs
index 98db4f14..e7141919 100644
--- a/sq/src/sq_cli.rs
+++ b/sq/src/sq_cli.rs
@@ -1720,6 +1720,8 @@ pub struct KeyCommand {
pub enum KeySubcommands {
Generate(KeyGenerateCommand),
Password(KeyPasswordCommand),
+ #[clap(subcommand)]
+ Userid(KeyUseridCommand),
ExtractCert(KeyExtractCertCommand),
Adopt(KeyAdoptCommand),
AttestCertifications(KeyAttestCertificationsCommand),
@@ -1978,6 +1980,89 @@ pub struct KeyExtractCertCommand {
pub binary: bool,
}
+#[derive(Debug, Subcommand)]
+#[clap(
+ name = "userid",
+ display_order = 105,
+ about = "Manages User IDs",
+ long_about =
+"Manages User IDs
+
+Add User IDs to a key.
+",
+ subcommand_required = true,
+ arg_required_else_help = true,
+)]
+pub enum KeyUseridCommand {
+ Add(KeyUseridAddCommand),
+}
+
+#[derive(Debug, Args)]
+#[clap(
+ display_order = 10,
+ about = "Adds a User ID",
+ long_about =
+"Adds a User ID
+
+A User ID can contain a name, like \"Juliet\" or an email address, like
+\"<juliet@example.org>\". Historically, a name and email address were often
+combined as a single User ID, like \"Juliet <juliet@example.org>\".
+",
+ after_help =
+"EXAMPLES:
+
+# First, this generates a key
+$ sq key generate --userid \"<juliet@example.org>\" --export juliet.key.pgp
+
+# Then, this adds a User ID
+$ sq key userid add --userid \"Juliet\" juliet.key.pgp \\
+ --output juliet-new.key.pgp
+",
+)]
+pub struct KeyUseridAddCommand {
+ #[clap(flatten)]
+ pub io: IoArgs,
+ #[clap(
+ value_name = "USERID",
+ short,
+ long,
+ help = "User ID to add",
+ )]
+ pub userid: Vec<String>,
+ #[clap(
+ long = "creation-time",
+ value_name = "CREATION_TIME",
+ help = "Sets the binding signature creation time to TIME (as ISO 8601)",
+ long_help = "\
+Sets the creation time of this User ID's binding signature to TIME.
+TIME is interpreted as an ISO 8601 timestamp. To set the creation
+time to June 28, 2022 at midnight UTC, you can do:
+
+$ sq key userid add --userid \"Juliet\" --creation-time 20210628 \\
+ juliet.key.pgp --output juliet-new.key.pgp
+
+To include a time, add a T, the time and optionally the timezone (the
+default timezone is UTC):
+
+$ sq key userid add --userid \"Juliet\" --creation-time 20210628T1137+0200 \\
+ juliet.key.pgp --output juliet-new.key.pgp
+",
+ )]
+ pub creation_time: Option<CliTime>,
+ #[clap(
+ long = "private-key-store",
+ value_name = "KEY_STORE",
+ help = "Provides parameters for private key store",
+ )]
+ pub private_key_store: Option<String>,
+ #[clap(
+ short = 'B',
+ long,
+ help = "Emits binary data",
+ )]
+ pub binary: bool,
+}
+
#[derive(Debug, Args)]
#[clap(
name = "adopt",