server/lib/src/pull_intake/provenance.rs
//! **The provenance seam of the pull-through.** What came WITH a fetched
//! package (an image's cosign `.sig` and `.att` layers and its OCI 1.1
//! referrers; a wheel's PEP 740 attestation) is handed here with the package's
//! bytes, and the answer fills the `provenance` evidence slot of the SCANNED
//! transition.
//!
//! The verifier is lane G3a's `holger-provenance` (scanner id
//! [`SCANNER_ID`]), as [`HolgerProvenance`]. [`NotInBuild`] stays as the
//! fail-closed stand-in a test can install.
/// The scanner id the evidence slot names (G3a's `holger_provenance::SCANNER_ID`).
pub const SCANNER_ID: &str = holger_provenance::SCANNER_ID;
/// One file of provenance material, as it travelled with the package.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Material {
/// `cosign.json` (`holger.cosign-attachments/v1`), `<file>.provenance.json`
/// (PEP 740), `referrer-<digest>.sigstore.json`.
pub name: String,
pub bytes: Vec<u8>,
}
/// How strictly one property is demanded (G3a's `Mode`, G0's `ProvenanceMode`).
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
Off,
Prefer,
Require,
}
impl Mode {
pub fn as_str(self) -> &'static str {
match self {
Mode::Off => "off",
Mode::Prefer => "prefer",
Mode::Require => "require",
}
}
}
/// A signer identity: an OIDC issuer with an exact subject or a subject regex.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Identity {
pub issuer: String,
#[serde(default)]
pub subject: Option<String>,
#[serde(default)]
pub subject_regex: Option<String>,
}
/// What the verifier is asked to hold the package to: the floor plus the
/// source's gate.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Demand {
/// Verified provenance (an SLSA or publish statement).
pub provenance: Mode,
/// A verified signature of any kind.
pub signed: Mode,
pub identities: Vec<Identity>,
pub slsa_min: u8,
/// The pinned OpenPGP keys (by name) this source's `.asc` may be signed
/// with; empty = any key the trust root pins.
pub pgp_keys: Vec<String>,
/// `kind: apk`: the pinned apk keys that may sign the index. Empty = any pinned one.
pub apk_keys: Vec<String>,
}
impl Demand {
pub fn is_off(&self) -> bool {
self.provenance == Mode::Off && self.signed == Mode::Off
}
pub fn summary(&self) -> String {
let s = format!(
"provenance {}, signed {}, slsa_min {}, {} identit{}",
self.provenance.as_str(),
self.signed.as_str(),
self.slsa_min,
self.identities.len(),
if self.identities.len() == 1 {
"y"
} else {
"ies"
}
);
if self.pgp_keys.is_empty() {
s
} else {
format!("{s}, pgp keys {:?}", self.pgp_keys)
}
}
}
/// The verifier's decision.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Pass,
Warn,
Block,
}
/// What the verifier answered, for the evidence slot and the audit line.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outcome {
pub verdict: Verdict,
/// One sentence: why.
pub detail: String,
/// `holger-provenance-<n>` when refused.
pub code: Option<String>,
/// How many signatures or attestations verified.
pub verified: usize,
}
/// **The verifier.** `subject` is what the signatures are about: an image's
/// manifest (or index) bytes, whose sha256 is the image digest; a wheel's bytes.
pub trait Verifier: Send + Sync {
fn id(&self) -> &str {
SCANNER_ID
}
fn verify(&self, subject: &[u8], materials: &[Material], demand: &Demand) -> Outcome;
}
/// **The fail-closed placeholder** while G3a's `holger-provenance` is not in
/// this build. Anything that asks for provenance is BLOCKED by name; a demand
/// that is off asks nothing and is not called.
#[derive(Debug, Default, Clone, Copy)]
pub struct NotInBuild;
impl Verifier for NotInBuild {
fn verify(&self, _subject: &[u8], materials: &[Material], demand: &Demand) -> Outcome {
let detail = format!(
"the provenance verifier ({SCANNER_ID}) is not in this build, so the {} demanded \
({}) cannot be checked; {} piece(s) of material were fetched and kept; fail closed",
if demand.provenance == Mode::Require || demand.signed == Mode::Require {
"requirement"
} else {
"preference"
},
demand.summary(),
materials.len()
);
let verdict = if demand.provenance == Mode::Require || demand.signed == Mode::Require {
Verdict::Block
} else {
Verdict::Warn
};
Outcome {
verdict,
detail,
code: Some("holger-provenance-0".into()),
verified: 0,
}
}
}
/// **G3a's `holger-provenance` as the pull-through's verifier.** The demand
/// becomes the effective policy (its identities and SLSA minimum, plus the
/// floor's keys and builders), and the material is verified OFFLINE against the
/// pinned Sigstore trust root. With no trust root a keyless signature is refused
/// by name (`holger-provenance-10`), never passed.
#[derive(Debug, Clone, Default)]
pub struct HolgerProvenance {
pub trust: Option<std::sync::Arc<holger_provenance::TrustRoot>>,
pub keys: Vec<holger_provenance::TrustedKey>,
pub builders: Vec<holger_provenance::BuilderRule>,
}
impl HolgerProvenance {
pub fn new(trust: Option<std::sync::Arc<holger_provenance::TrustRoot>>) -> Self {
Self {
trust,
..Default::default()
}
}
/// The effective policy a demand stands for.
pub fn policy(&self, demand: &Demand) -> holger_provenance::EffectivePolicy {
let mode = |m: Mode| match m {
Mode::Off => holger_provenance::Mode::Off,
Mode::Prefer => holger_provenance::Mode::Prefer,
Mode::Require => holger_provenance::Mode::Require,
};
holger_provenance::EffectivePolicy {
provenance: mode(demand.provenance),
signed: mode(demand.signed),
slsa_min: demand.slsa_min,
identities: demand
.identities
.iter()
.map(|i| holger_provenance::Identity {
issuer: i.issuer.clone(),
subject: i.subject.clone().unwrap_or_default(),
subject_regex: i.subject_regex.clone().unwrap_or_default(),
})
.collect(),
builders: self.builders.clone(),
keys: self.keys.clone(),
apk_keys: demand.apk_keys.clone(),
pgp_keys: demand.pgp_keys.clone(),
trust_root: None,
}
}
}
impl Verifier for HolgerProvenance {
fn verify(&self, subject: &[u8], materials: &[Material], demand: &Demand) -> Outcome {
let m: Vec<(String, Vec<u8>)> = materials
.iter()
.map(|m| (m.name.clone(), m.bytes.clone()))
.collect();
let r = holger_provenance::verify_artifact(
subject,
&m,
&self.policy(demand),
self.trust.as_deref(),
);
let verdict = match r.verdict {
holger_provenance::Verdict::Pass => Verdict::Pass,
holger_provenance::Verdict::Warn => Verdict::Warn,
holger_provenance::Verdict::Block => Verdict::Block,
};
Outcome {
verdict,
detail: r.detail.clone(),
code: r.code.clone(),
verified: r.candidates.iter().filter(|c| c.verified).count(),
}
}
}
/// The verifier this build carries: `holger-provenance`, with no trust root
/// until the floor's `trust_root` is loaded ([`super::PullIntake::replace_verifier`]).
pub fn default_verifier() -> std::sync::Arc<dyn Verifier> {
std::sync::Arc::new(HolgerProvenance::default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_placeholder_never_passes_a_requirement() {
let d = Demand {
provenance: Mode::Require,
signed: Mode::Require,
identities: vec![],
slsa_min: 0,
pgp_keys: vec![],
apk_keys: vec![],
};
let o = NotInBuild.verify(
b"x",
&[Material {
name: "cosign.json".into(),
bytes: b"{}".to_vec(),
}],
&d,
);
assert_eq!(o.verdict, Verdict::Block);
assert!(o.detail.contains("fail closed"), "{}", o.detail);
let p = Demand {
provenance: Mode::Prefer,
signed: Mode::Off,
identities: vec![],
slsa_min: 0,
pgp_keys: vec![],
apk_keys: vec![],
};
assert_eq!(NotInBuild.verify(b"x", &[], &p).verdict, Verdict::Warn);
}
}