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/>"));
}
}