Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

provenance/src/pgp.rs

//! **OpenPGP detached signatures** (Maven Central's `.asc`), verified offline.
//!
//! Maven Central requires every file of a release to carry a detached, armored
//! OpenPGP signature beside it (`foo-1.0.jar.asc`). Central checks that the
//! signature verifies with SOME key on a public keyserver; it does not say whose
//! key it should be. So here a signature counts only when, in this order:
//!
//! 1. **It parses** as one or more OpenPGP signature packets of type binary or
//!    text, with a hash no weaker than SHA-224 (MD5, SHA-1 and RIPEMD-160 are
//!    refused by name).
//! 2. **Its issuer is pinned.** The issuer fingerprint (or, for an old signature,
//!    the 64-bit key id) names the primary key or a signing subkey of a key that
//!    the trust bundle pins (`pgp-keys/<name>`, [`crate::trust`]) or the floor's
//!    `keys` list as `alg: openpgp`. A signature by any other key is
//!    **unverified** (`holger-provenance-4`), never a pass, however well it verifies.
//! 3. **It verifies** over the exact bytes of the artifact with that key.
//!
//! A pinned key is read once, when it is pinned: its subkey bindings (and a
//! signing subkey's back signature) must verify, and a key that carries a
//! revocation signature is refused. Nothing here dials a keyserver.
//!
//! **Opt-in** (T9b, 2026-10-25): rpgp costs the default `cargo install holger` about 96
//! crates, so the verifier is built only with this crate's `pgp` feature (server-lib's
//! and the CLI's `maven-pgp`; the CLI's `online` and `full`, which build the Maven
//! pull-through, carry it). A build without it FAILS CLOSED, never ignoring a pin it
//! cannot check: [`PgpKey::from_armored`] refuses every key by name
//! (`[provenance-pgp-not-built]`), so a trust bundle's `pgp-keys/<name>` and a floor key
//! of `alg: openpgp` are refused at load; [`crate::check_tightens`] refuses a source's
//! `require(pgp_keys: [...])`; and an `.asc` is "unverified", never a pass.

#[cfg(feature = "pgp")]
mod rpgp;
#[cfg(feature = "pgp")]
pub use rpgp::{verify_detached, PgpKey};

#[cfg(not(feature = "pgp"))]
mod not_built;
#[cfg(not(feature = "pgp"))]
pub use not_built::{verify_detached, PgpKey};

/// Is the OpenPGP verifier built into this binary (this crate's `pgp` feature)?
pub const BUILT: bool = cfg!(feature = "pgp");

/// The name a refusal carries when the verifier is not built, `[provenance-pgp-not-built]`.
pub const NOT_BUILT_TAG: &str = "provenance-pgp-not-built";

/// The refusal's words for `what` (a key, a source's pin, an `.asc`) in a build without
/// the verifier: the tag, and the build feature that would check it.
pub fn not_built(what: &str) -> String {
    format!(
        "[{NOT_BUILT_TAG}] {what} needs OpenPGP verification, which this Holger was built without; \
         build it with the `maven-pgp` feature (`cargo install holger --features maven-pgp`; `online` and `full` carry it)"
    )
}

const ARMOR_SIG: &str = "-----BEGIN PGP SIGNATURE-----";

/// Does this material look like an armored OpenPGP signature (Maven's `.asc`)?
pub fn is_signature(name: &str, bytes: &[u8]) -> bool {
    let _ = name;
    bytes.trim_ascii_start().starts_with(ARMOR_SIG.as_bytes())
}

/// A signature that verified with a pinned key.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedSignature {
    /// The pinned key's name.
    pub key: String,
    /// The pinned (primary) key's fingerprint, lower-case hex.
    pub fingerprint: String,
    /// The fingerprint of the key that signed, the primary or a subkey.
    pub signer_fingerprint: String,
    /// `rsa-sha512`, `ecdsa-sha512`, `eddsa-sha256`, …
    pub alg: String,
    /// The signature's creation time, Unix seconds (the signer's claim).
    pub created: Option<i64>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_refusal_names_the_tag_and_the_feature() {
        let s = not_built("an OpenPGP key");
        assert!(
            s.starts_with("[provenance-pgp-not-built] an OpenPGP key needs OpenPGP verification"),
            "{s}"
        );
        assert!(
            s.contains("`maven-pgp`") && s.contains("cargo install holger --features maven-pgp"),
            "{s}"
        );
        assert_eq!(BUILT, cfg!(feature = "pgp"));
    }

    #[test]
    fn an_asc_is_told_from_other_material_with_or_without_the_verifier() {
        assert!(is_signature(
            "a.pom.asc",
            b"\n-----BEGIN PGP SIGNATURE-----\n"
        ));
        assert!(!is_signature("a.pom", b"<project/>"));
    }
}