//! GPG / OpenPGP helpers: secret key discovery and inline (clear) signing //! of Debian artifacts such as `.dsc`, `.buildinfo` and `.changes` files. use std::error::Error; use std::io::Read; use std::path::Path; use gpgme::{Context, Data, Protocol}; /// Check if a GPG key matching 'email' exists /// Returns the key ID if found, None otherwise pub fn find_signing_key_for_email( email: &str, ) -> Result, Box> { // Create a new GPG context let mut ctx = Context::from_protocol(Protocol::OpenPgp)?; // List all secret keys let keys = ctx.secret_keys()?; // Find a key that matches the email and can sign for key_result in keys { let key = key_result?; // Check if the key has signing capability if key.can_sign() { // Check user IDs for email match for user_id in key.user_ids() { if let Ok(userid_email) = user_id.email() && userid_email.eq_ignore_ascii_case(email) && let Ok(fingerprint) = key.fingerprint() { return Ok(Some(fingerprint.to_string())); } } } } Ok(None) } /// Validate an OpenPGP key id / fingerprint like dpkg does. /// /// Short (<= 8 hex chars) key IDs are rejected outright, long (16 hex chars) /// key IDs produce a warning; anything else must be a v4 (40) or v6 (64) /// fingerprint length. pub fn validate_key_id(keyid: &str) -> Result<(), Box> { let len = keyid.len(); if len <= 8 { return Err( "short OpenPGP key IDs are broken; use a key fingerprint instead".into(), ); } else if len <= 16 { log::warn!( "long OpenPGP key IDs are strongly discouraged; \ use a key fingerprint instead" ); } else if len != 40 && len != 64 { log::warn!("OpenPGP key ID has unknown v4 or v6 fingerprint length"); } Ok(()) } /// Find a secret key whose fingerprint matches `keyid` (suffix matching /// allows passing a long key id instead of the full fingerprint). fn find_secret_key(ctx: &mut Context, keyid: &str) -> Result, Box> { for key_result in ctx.secret_keys()? { let key = key_result?; if let Ok(fingerprint) = key.fingerprint() { if fingerprint.ends_with(keyid) { return Ok(Some(key)); } } } Ok(None) } /// Clearsign a file in place: the original content becomes the payload of an /// armored inline-signed document which atomically replaces the file. /// /// This is the same operation as dpkg's `inline_sign` + rename sequence used /// when signing `.dsc`, `.buildinfo` or `.changes` files. pub fn clearsign_file(path: &Path, keyid: &str) -> Result<(), Box> { let content = std::fs::read(path) .map_err(|e| format!("cannot read '{}' for signing: {}", path.display(), e))?; let mut ctx = Context::from_protocol(Protocol::OpenPgp) .map_err(|e| format!("cannot initialize GPGME: {}", e))?; ctx.set_armor(true); let key = find_secret_key(&mut ctx, keyid)? .ok_or_else(|| format!("no secret key matching '{}' found", keyid))?; ctx.add_signer(&key) .map_err(|e| format!("cannot add signer '{}': {}", keyid, e))?; let input = Data::from_bytes(&content)?; let mut output = Data::new()?; ctx.sign_clear(input, &mut output) .map_err(|e| format!("clear-signing '{}' failed: {}", path.display(), e))?; // gpgme leaves the output buffer cursor at the end after writing. use std::io::Seek; use std::io::SeekFrom; output.seek(SeekFrom::Start(0))?; let mut signed = Vec::new(); output.read_to_end(&mut signed)?; // Atomic replace, like dpkg's signfile (write .asc then move). let tmp = path.with_extension("asc.tmp"); std::fs::write(&tmp, &signed) .map_err(|e| format!("cannot write '{}': {}", tmp.display(), e))?; std::fs::rename(&tmp, path) .map_err(|e| format!("cannot install signed '{}': {}", path.display(), e).into()) } #[cfg(test)] mod tests { use super::*; #[test] fn key_id_validation() { assert!(validate_key_id("12345678").is_err()); // short: rejected assert!(validate_key_id("1234567890ABCDEF").is_ok()); // long: warns assert!(validate_key_id(&"a".repeat(40)).is_ok()); assert!(validate_key_id(&"b".repeat(64)).is_ok()); } }