Skip to main content

confium_publish/
sign.rs

1// PGP signing of the generated manifest.
2//
3// detached ASCII-armored PGP signatures stored under `sigs/<publisher>.asc`.
4// We shell out to the system `gpg` rather than embedding a crypto
5// library: the publish ceremony already trusts the author's local GPG
6// keyring, and shelling avoids pulling PGP into the Confium binary
7// dependency surface.
8
9use std::path::Path;
10use std::process::Command;
11
12use snafu::{ResultExt, Snafu};
13
14#[derive(Snafu, Debug)]
15pub enum SignError {
16    #[snafu(display("gpg not found on PATH; install GnuPG to sign releases"))]
17    GpgMissing { source: std::io::Error },
18
19    #[snafu(display(
20        "gpg exited with status {exit_code} while signing {}\nstderr: {stderr}",
21        manifest_path.display()
22    ))]
23    GpgFailed {
24        exit_code: i32,
25        stderr: String,
26        manifest_path: Box<Path>,
27    },
28}
29
30pub type Result<T> = std::result::Result<T, SignError>;
31
32/// Sign the manifest at `manifest_path` with the publisher's key,
33/// returning the detached ASCII-armored signature bytes.
34///
35/// `signing_key` is passed to `gpg --local-user`. It may be a key-id,
36/// fingerprint, or path to a key file that gpg knows how to resolve.
37/// When `dry_run` is true, returns a placeholder signature string
38/// without invoking gpg or touching disk.
39pub fn sign_manifest(manifest_path: &Path, signing_key: &str, dry_run: bool) -> Result<Vec<u8>> {
40    if dry_run {
41        return Ok(
42            b"-----BEGIN PGP SIGNATURE-----\n[dry-run placeholder]\n-----END PGP SIGNATURE-----\n"
43                .to_vec(),
44        );
45    }
46
47    let output = Command::new("gpg")
48        .args([
49            "--detach-sign",
50            "--armor",
51            "--batch",
52            "--yes",
53            "--local-user",
54            signing_key,
55            "--output",
56            "-", // stream signature to stdout
57        ])
58        .arg(manifest_path)
59        .output()
60        .context(GpgMissingSnafu)?;
61
62    if !output.status.success() {
63        let manifest_path_boxed: Box<Path> = Box::from(manifest_path);
64        return GpgFailedSnafu {
65            exit_code: output.status.code().unwrap_or(-1),
66            stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
67            manifest_path: manifest_path_boxed,
68        }
69        .fail();
70    }
71    Ok(output.stdout)
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn dry_run_returns_placeholder_without_gpg() {
80        let got = sign_manifest(std::path::Path::new("/nonexistent"), "key", true).unwrap();
81        let s = String::from_utf8(got).unwrap();
82        assert!(s.contains("BEGIN PGP SIGNATURE"));
83        assert!(s.contains("dry-run placeholder"));
84    }
85}