diff options
author | Heiko Schaefer <heiko@schaefer.name> | 2022-07-04 18:04:10 +0200 |
---|---|---|
committer | Heiko Schaefer <heiko@schaefer.name> | 2022-07-08 17:44:46 +0200 |
commit | 0f4b13505f7257aab32150864aa36ef3ffbccb6d (patch) | |
tree | fb11935b668154ecd097ad5f41654331967687d9 | |
parent | 3e95c56ee010d91968d23df1e5eafbce601803b5 (diff) |
sq: Implement 'key userid add'.
-rw-r--r-- | sq/sq-subplot.md | 12 | ||||
-rw-r--r-- | sq/src/commands/key.rs | 158 | ||||
-rw-r--r-- | sq/src/commands/mod.rs | 1 | ||||
-rw-r--r-- | sq/src/sq-usage.rs | 81 | ||||
-rw-r--r-- | sq/src/sq_cli.rs | 85 |
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", |