From c74dcd0f0100c25d0f0f5b9f4f747656b339da72 Mon Sep 17 00:00:00 2001 From: Niklas Elsbrock Date: Sat, 9 May 2026 16:27:59 +0200 Subject: [PATCH] add `--userid` option --- README.md | 4 ++-- src/lib.rs | 57 +++++++++++++++++++++++++++++++++++++++++------------ src/main.rs | 22 ++++++++++++++++++++- 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f16b509..7094995 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,11 @@ at `secret.asc`. The regex for this is `^C0FFEE`. Now, simply use the following the search: ```sh -fingerprunk -r '^C0FFEE' >> secret.asc +fingerprunk -r '^C0FFEE' -u "Your Name " >> secret.asc ``` Fingerprunk will now generate many keys and write out all keys with matching fingerprints to -standard output (here: `secret.asc`). +standard output (here: `secret.asc`), adding the provided user ID. If you want Fingerprunk to output password-encrypted keys use the `-p` flag and you will be prompted for a password. diff --git a/src/lib.rs b/src/lib.rs index 6c2d3a6..e0db146 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ use std::{ mpsc, }, thread, - time::{Duration, Instant}, + time::{Duration, Instant, SystemTime}, }; use fancy_regex::Regex; @@ -18,7 +18,7 @@ use sequoia_openpgp::{ Cert, Packet, armor, crypto::Password, packet::{ - Key, + Key, UserID, key::{Key4, PrimaryRole, SecretParts}, prelude::SignatureBuilder, }, @@ -40,6 +40,7 @@ pub struct Config { pub status_enabled: bool, pub stop_after: Option, pub password: Option, + pub userids: Vec, } #[derive(Debug)] @@ -160,21 +161,19 @@ impl Fingerprunk { } fn key_to_cert(&self, key: &SecretKey) -> anyhow::Result { - let sig = SignatureBuilder::new(SignatureType::DirectKey) - .set_hash_algo(HashAlgorithm::SHA512) - .set_preferred_hash_algorithms(vec![HashAlgorithm::SHA512, HashAlgorithm::SHA256])? - .set_preferred_symmetric_algorithms(vec![ - SymmetricAlgorithm::AES256, - SymmetricAlgorithm::AES128, - ])?; + let creation_time = SystemTime::now(); let mut signer = key .clone() .into_keypair() .expect("key should have a secret"); - let sig = sig.sign_direct_key(&mut signer, key.parts_as_public())?; - let secret_key_packet = Packet::SecretKey({ + // Sign keypair + let key_sig = create_sig_builder(SignatureType::DirectKey, creation_time)? + .sign_direct_key(&mut signer, key.parts_as_public())?; + + // Create certificate + let mut cert = Cert::try_from(Packet::SecretKey({ let mut key = key.clone(); if let Some(ref password) = self.config.password { let (k, mut secret) = key.take_secret(); @@ -182,9 +181,27 @@ impl Fingerprunk { key = k.add_secret(secret).0; } key - }); + }))?; - Cert::try_from(vec![secret_key_packet, Packet::from(sig)]) + let mut packets = vec![Packet::from(key_sig)]; + + // Sign user IDs + let mut next_is_primary = true; + for user_id in self.config.userids.iter().cloned() { + let mut sig_builder = + create_sig_builder(SignatureType::PositiveCertification, creation_time)?; + if next_is_primary { + sig_builder = sig_builder.set_primary_userid(true)?; + next_is_primary = false; + } + let sig = user_id.bind(&mut signer, &cert, sig_builder)?; + + packets.push(user_id.into()); + packets.push(sig.into()); + } + + cert = cert.insert_packets(packets)?.0; + Ok(cert) } fn serialize_cert(&self, cert: Cert, to: impl io::Write) -> anyhow::Result<()> { @@ -256,3 +273,17 @@ impl Fingerprunk { ); } } + +fn create_sig_builder( + typ: SignatureType, + creation_time: SystemTime, +) -> Result { + SignatureBuilder::new(typ) + .set_signature_creation_time(creation_time)? + .set_hash_algo(HashAlgorithm::SHA512) + .set_preferred_hash_algorithms(vec![HashAlgorithm::SHA512, HashAlgorithm::SHA256])? + .set_preferred_symmetric_algorithms(vec![ + SymmetricAlgorithm::AES256, + SymmetricAlgorithm::AES128, + ]) +} diff --git a/src/main.rs b/src/main.rs index 5a9c915..9540c72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use anyhow::{Context as AnyhowContext, anyhow}; use clap::{ArgAction, Parser, ValueEnum}; use fancy_regex::Regex; use fingerprunk::Fingerprunk; +use sequoia_openpgp::packet::UserID; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -33,12 +34,22 @@ struct Args { #[arg(long)] stop_after: Option, - /// Prompt for a password and use it to encrypt found keys. + /// Prompt for a password and use it to encrypt matching keys. /// /// By default, found keys are printed to stdout unencrypted. Use this if you actually plan to /// use generated keys. #[arg(short, long, action = ArgAction::SetTrue)] password: bool, + + /// Add the given user ID to matching keys. + #[arg(short, long = "userid")] + userid: Vec, + + /// Explicitly do not add user IDs to matching keys. + /// + /// Disables the warning about importing keys without user IDs into GnuPG. + #[arg(long, conflicts_with = "userid", action = ArgAction::SetTrue)] + no_userid: bool, } #[derive(ValueEnum, Clone, Copy, Debug, Default)] @@ -62,6 +73,14 @@ impl StatusEnabled { fn main() -> anyhow::Result<()> { let args = Args::parse(); + if !args.no_userid && args.userid.is_empty() { + eprintln!( + "WARNING: No user ID was provided.\n\ + You may experience problems importing generated keys into GnuPG.\n\ + Use the --userid option to add a user ID.\n" + ) + } + let password = if args.password { let password = rpassword::prompt_password( "Enter password for encrypting found keys (leave empty for no encryption): ", @@ -87,6 +106,7 @@ fn main() -> anyhow::Result<()> { status_enabled: args.status.evaluate(), stop_after: args.stop_after, password, + userids: args.userid, }; Fingerprunk::new_from_config(config).run()?;