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.
This commit is contained in:
+96
-1
@@ -1,4 +1,11 @@
|
||||
use gpgme::{Context, Protocol};
|
||||
//! 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
|
||||
@@ -30,3 +37,91 @@ pub fn find_signing_key_for_email(
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user