Files
pkh/src/utils/gpg.rs
T
vhaudiquet c2e1288bc5 build,debian: extract reusable Debian format primitives from build/
Move the generic components out of src/build/ into a new src/debian/
module so they can be reused independently of the build pipeline:
deb822 control parsing (plus the debian/control model), file checksum
registry, debian/files registry, Debian version handling and changelog
entry parsing.

Merge OpenPGP clearsigning into utils/gpg.rs next to the existing key
discovery helper, making signing available outside of builds.

Delegate changelog.rs header/footer parsing to the new
debian::changelog parser, removing the duplicate regex implementation.

src/build/ keeps only build-specific logic: the pipeline driver,
build types, environment setup and the .buildinfo/.changes writers.
2026-08-23 01:43:41 +02:00

128 lines
4.4 KiB
Rust

//! 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<Option<String>, Box<dyn std::error::Error>> {
// 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<dyn Error>> {
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<Option<gpgme::Key>, Box<dyn Error>> {
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<dyn Error>> {
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());
}
}