provenance/tests/pgp_build_feature.rs
//! **OpenPGP verification is opt-in, and it fails closed when it is not built** (T9b).
//!
//! rpgp (T9's `.asc` verifier) cost the default `cargo install holger` about 96 crates,
//! so it sits behind holger-provenance's `pgp` feature (`maven-pgp` in server-lib and the
//! CLI; the CLI's `online` and `full` carry it). A build without it must never ignore a pin it cannot check:
//!
//! * a trust bundle that pins `pgp-keys/<name>` is refused by name at load;
//! * a floor key of `alg: openpgp` is refused by name at load;
//! * a source's `require(pgp_keys: [...])` is refused by name at load;
//! * an `.asc` is "unverified", never a pass.
//!
//! The file takes no feature, so it is never dark: each test checks the side this build is
//! on. `cargo xtask suite-check` runs it without `pgp` (`trust:pgp-not-built`); the
//! workspace run and `trust:maven-pgp` run it with `pgp`.
use holger_provenance::pgp::{PgpKey, BUILT, NOT_BUILT_TAG};
use holger_provenance::{
check_floor, check_tightens, effective, verify_artifact, KeyAlg, ProvenanceFloor,
ProvenanceGate, TrustRoot, TrustedKey, Verdict,
};
const FX: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/maven");
const POVIRK: &str = "povirk-BDB5FA4FE719D787FB3D3197F6D4A1D411E9D1AE.asc";
const GUAVA: &str = "guava-33.3.1-jre.pom";
fn read(f: &str) -> Vec<u8> {
std::fs::read(format!("{FX}/{f}")).unwrap_or_else(|e| panic!("{f}: {e}"))
}
/// The refusal names the missing build feature, in the words an operator searches for.
fn refused_by_name(e: &holger_provenance::ProvenanceError) {
let s = e.tagged();
assert!(
s.contains(&format!("[{NOT_BUILT_TAG}]")),
"the refusal names {NOT_BUILT_TAG}: {s}"
);
assert!(
s.contains("`maven-pgp`"),
"the refusal names the feature to build: {s}"
);
}
fn gate_pgp(keys: &[&str]) -> ProvenanceGate {
ProvenanceGate::Require {
identity: None,
identities: vec![],
builders: vec![],
keys: vec![],
apk_keys: vec![],
pgp_keys: keys.iter().map(|s| s.to_string()).collect(),
slsa_min: None,
}
}
fn openpgp_floor() -> ProvenanceFloor {
ProvenanceFloor {
keys: vec![TrustedKey {
id: "guava-release".into(),
alg: KeyAlg::Openpgp,
public_key: String::from_utf8(read(POVIRK)).unwrap(),
}],
..Default::default()
}
}
#[test]
fn the_feature_says_whether_it_is_built() {
assert_eq!(BUILT, cfg!(feature = "pgp"));
assert_eq!(NOT_BUILT_TAG, "provenance-pgp-not-built");
}
#[test]
fn a_trust_bundle_that_pins_a_pgp_key_is_refused_by_name_without_the_feature() {
let pinned = TrustRoot::default().with_pgp_keys(&[(POVIRK.to_string(), read(POVIRK))]);
let direct = PgpKey::from_armored(POVIRK, &read(POVIRK));
if BUILT {
assert_eq!(pinned.unwrap().pgp_keys.len(), 1);
assert!(direct.is_ok());
} else {
let e = pinned.unwrap_err();
assert_eq!(
e.code_str(),
"holger-provenance-10",
"a trust-root refusal: {e}"
);
refused_by_name(&e);
refused_by_name(&direct.unwrap_err());
}
}
#[test]
fn a_floor_key_of_alg_openpgp_is_refused_by_name_without_the_feature() {
let floor = openpgp_floor();
let gate = ProvenanceGate::Require {
identity: None,
identities: vec![],
builders: vec![],
keys: vec!["guava-release".into()],
apk_keys: vec![],
pgp_keys: vec![],
slsa_min: None,
};
if BUILT {
check_floor(&floor).unwrap();
check_tightens(&floor, &gate, "central").unwrap();
} else {
refused_by_name(&check_floor(&floor).unwrap_err());
refused_by_name(&check_tightens(&floor, &gate, "central").unwrap_err());
}
}
#[test]
fn a_source_that_requires_pgp_keys_is_refused_by_name_without_the_feature() {
let r = check_tightens(&ProvenanceFloor::default(), &gate_pgp(&[POVIRK]), "central");
if BUILT {
r.unwrap();
} else {
let e = r.unwrap_err();
assert_eq!(
e.code_str(),
"holger-provenance-11",
"a policy refusal: {e}"
);
assert!(
e.to_string().contains("\"central\""),
"it names the source: {e}"
);
refused_by_name(&e);
}
// A gate with no pgp pin is untouched either way.
check_tightens(
&ProvenanceFloor::default(),
&ProvenanceGate::Prefer,
"central",
)
.unwrap();
}
#[test]
fn an_asc_without_the_feature_is_unverified_never_a_pass() {
let require = effective(
&ProvenanceFloor {
require_signed: true,
require_provenance: true,
..Default::default()
},
&ProvenanceGate::Off,
);
let materials = [(format!("{GUAVA}.asc"), read(&format!("{GUAVA}.asc")))];
let r = verify_artifact(&read(GUAVA), &materials, &require, None);
assert_eq!(
(r.verdict, r.code.as_deref()),
(Verdict::Block, Some("holger-provenance-4")),
"{r:#?}"
);
let c = &r.candidates[0];
assert!(!c.verified);
let why = c.error.as_deref().unwrap_or_default();
assert!(why.contains("unverified"), "{why}");
if !BUILT {
assert!(why.contains(&format!("[{NOT_BUILT_TAG}]")), "{why}");
}
// Under prefer it is a warning, never a pass.
let prefer = effective(&ProvenanceFloor::default(), &ProvenanceGate::Prefer);
let r = verify_artifact(&read(GUAVA), &materials, &prefer, None);
assert_eq!(r.verdict, Verdict::Warn, "{r:#?}");
}