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

nordisk/holger

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