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.
128 lines
4.4 KiB
Rust
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());
|
|
}
|
|
}
|