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

nordisk/holger

server/lib/tests/the_provenance_gate_stands.rs

//! **W17 — a signature that means something.** The four proofs that the
//! promotion boundary now depends on an operator-declared trust root.
//!
//! What was MEASURED on `main` at `bb0302e`, before any line of this landed:
//!
//! * `AttestTrust::from_der_root_files` read each declared path and pushed the
//!   BYTES. A zero-byte file, a PEM somebody forgot to convert, a path that
//!   resolved to a directory listing — each became a "root" that could never
//!   match anything, the boot succeeded, and the estate verified against fewer
//!   roots than it had declared. The plugin host has refused exactly this since
//!   `holger-plugin-8` (*"refusing rather than verifying against fewer roots than
//!   the operator declared"*); the evidence plane had the rule in prose only.
//! * NO shipped row carried a gate named `provenance`. `catalog_from_config`
//!   registered `osv`, and `attested` only when `promotion.require_attestation`
//!   named a predicate type. A config writing `gates: ["provenance"]` was a BOOT
//!   FAILURE on all five server rows — the unit test in `promote/gate.rs`
//!   literally used `"provenance"` as its example of a name no build carries.
//!
//! So `attest.rs` verified against roots nobody checked were roots, and the
//! promotion path never asked it anything. Both halves are here.

use holger_server_lib::attest::{ArtifactDigest, AttestTrust, Statement};
use holger_server_lib::errcode;
use holger_server_lib::promote::{
    Gate, GateCatalog, PromotedArtifact, PromotionContext, ProvenanceGate, ProvenancePolicy,
};
use std::path::PathBuf;
use std::sync::Arc;
use traits::ArtifactId;

// ── fixtures ─────────────────────────────────────────────────────────────────

/// A REAL DER certificate, minted by `rcgen`, so the positive case is not
/// agreeing with the same structural check the negative cases exercise.
fn real_der_cert() -> Vec<u8> {
    let c = rcgen::generate_simple_self_signed(vec!["holger-test-root".to_string()])
        .expect("rcgen mints a self-signed certificate");
    c.cert.der().to_vec()
}

fn id() -> ArtifactId {
    ArtifactId {
        namespace: None,
        name: "serde".into(),
        version: "1.0.0".into(),
    }
}

/// An in-toto Statement v1 about `digest`, parsed from real JSON so the test
/// exercises the same door evidence arrives through.
fn slsa_statement_about(digest: &ArtifactDigest) -> Statement {
    let json = format!(
        r#"{{"_type":"https://in-toto.io/Statement/v1",
            "subject":[{{"name":"serde-1.0.0.crate","digest":{{"{}":"{}"}}}}],
            "predicateType":"https://slsa.dev/provenance/v1",
            "predicate":{{"buildDefinition":{{"buildType":"https://nordisk.dev/nornir/build-thing/variant/mvp-server"}}}}}}"#,
        digest.alg(),
        digest.hex()
    );
    Statement::parse(json.as_bytes()).expect("a well-formed in-toto Statement v1")
}

fn art<'a>(id: &'a ArtifactId, bytes: &'a [u8], att: &'a [Statement]) -> PromotedArtifact<'a> {
    PromotedArtifact {
        id,
        ecosystem: "crates.io",
        digest: ArtifactDigest::of_sha256(bytes),
        bytes,
        attestations: att,
    }
}

fn hop<'a>(set: &'a [PromotedArtifact<'a>]) -> PromotionContext<'a> {
    PromotionContext {
        principal: "releaser",
        from_repo: "rust-dev",
        to_repo: "rust-prod",
        hop: None,
        set,
    }
}

// ── 1. the trust root ────────────────────────────────────────────────────────

/// ★ A path the operator NAMED and this server cannot use is a refusal carrying
/// `holger-attest-10`, and it takes the WHOLE load down with it.
///
/// The last assertion is the one that matters. Loading the good roots and
/// dropping the bad one is the failure this test exists to forbid: the operator
/// declared N anchors, the process would run with N-1, and nothing anywhere
/// would print the difference. "Fewer roots than declared" is a smaller trust
/// set arrived at by accident, which is precisely an empty trust set's smaller
/// cousin.
#[test]
fn an_unreadable_declared_root_is_a_named_refusal_not_an_empty_trust_set() {
    let dir = tempfile::tempdir().expect("tempdir");
    let good = dir.path().join("root.der");
    std::fs::write(&good, real_der_cert()).expect("write the good root");

    // The positive case first, so every refusal below is a refusal OF something
    // that would otherwise have worked.
    let t = AttestTrust::from_der_root_files(std::slice::from_ref(&good))
        .expect("a real DER root loads");
    assert_eq!(
        t.roots().len(),
        1,
        "the declared root must actually be in the trust set"
    );

    let empty = dir.path().join("empty.der");
    std::fs::write(&empty, b"").expect("write an empty file");
    let pem = dir.path().join("root.pem");
    std::fs::write(
        &pem,
        b"-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n",
    )
    .expect("write a PEM file");
    let truncated = dir.path().join("cut.der");
    let mut cut = real_der_cert();
    cut.truncate(cut.len() / 2);
    std::fs::write(&truncated, &cut).expect("write a truncated DER");
    let absent = dir.path().join("not-here.der");

    for (what, path) in [
        ("an EMPTY file", empty.clone()),
        ("a PEM file", pem.clone()),
        ("a TRUNCATED certificate", truncated.clone()),
        ("an ABSENT path", absent.clone()),
    ] {
        let err = AttestTrust::from_der_root_files(&[path])
            .expect_err(&format!("{what} must not become a trust anchor"));
        assert_eq!(
            err.code().code,
            "holger-attest-10",
            "{what} must raise the FROZEN trust-root code, not {}: {err}",
            err.code().code
        );
        let tagged = err.tagged();
        assert_eq!(
            errcode::parse(&tagged).map(|c| c.code),
            Some("holger-attest-10"),
            "the refusal must be recoverable from the message it rides in: {tagged}"
        );
    }

    // ★ THE ONE THAT MATTERS: one bad root poisons the whole declaration.
    let err = AttestTrust::from_der_root_files(&[good.clone(), empty.clone()])
        .expect_err("a good root beside a bad one must refuse, not load 1 of 2");
    assert_eq!(err.code().code, "holger-attest-10");
    assert!(
        err.to_string().contains("empty.der"),
        "the refusal must NAME the path that failed: {err}"
    );

    // And the order is irrelevant — a bad root first must not be forgiven by a
    // good one after it.
    assert!(
        AttestTrust::from_der_root_files(&[empty, good]).is_err(),
        "the refusal must not depend on the order the operator wrote the paths in"
    );

    // A `PathBuf` that is a DIRECTORY reads as an I/O failure on every platform
    // holger ships to; it must still be the trust-root code, never a bare store
    // I/O error the operator will go hunting a full disk for.
    let as_dir: PathBuf = dir.path().to_path_buf();
    let err =
        AttestTrust::from_der_root_files(&[as_dir]).expect_err("a directory is not a certificate");
    assert_eq!(err.code().code, "holger-attest-10", "{err}");
}

// ── 2. the gate refuses by name ──────────────────────────────────────────────

/// ★ With a root DECLARED and evidence that does not chain to it, the promotion
/// is refused by name and the refusal names the bytes.
///
/// Everything in the evidence plane got there through `AttestStore::accept`,
/// which verifies the DSSE signature against the declared roots and the enrolled
/// signer before a byte is written. So "no statement filed about this digest" IS
/// "nothing chains these bytes to the declared root" — the gate enforces the
/// consequence, which is the half that had no caller.
#[test]
fn a_promotion_whose_evidence_does_not_chain_to_the_declared_root_is_refused_by_name() {
    let gate = ProvenanceGate::new(ProvenancePolicy::Require, 1);
    let a = id();

    // (a) No evidence at all.
    let none: [Statement; 0] = [];
    let set = [art(&a, b"the promoted bytes", &none)];
    let v = gate.check(&hop(&set)).expect("the gate decides");
    assert!(
        !v.is_allowed(),
        "an artifact with no provenance must not be promoted"
    );
    let reason = format!("{v:?}");
    assert_eq!(
        errcode::parse(&reason).map(|c| c.code),
        Some("holger-promote-10"),
        "the refusal must carry the frozen code: {reason}"
    );
    assert!(
        reason.contains(ArtifactDigest::of_sha256(b"the promoted bytes").hex()),
        "the refusal must name the BYTES, not just the coordinate: {reason}"
    );
    assert_eq!(
        v.subject(),
        Some("__serde__1.0.0"),
        "the refusal must name the subject: {v:?}"
    );

    // (b) Evidence about the RIGHT bytes: allowed.
    let mine = [slsa_statement_about(&ArtifactDigest::of_sha256(
        b"the promoted bytes",
    ))];
    let set = [art(&a, b"the promoted bytes", &mine)];
    assert!(
        gate.check(&hop(&set))
            .expect("the gate decides")
            .is_allowed(),
        "a verified provenance statement about these bytes must let the hop through"
    );

    // (c) ★ Evidence about SOMEBODY ELSE'S bytes: still refused. A genuine
    // signature over a different artifact is not evidence for this one — the
    // same attack `holger-attest-3` names, arriving one layer up.
    let theirs = [slsa_statement_about(&ArtifactDigest::of_sha256(
        b"a different build",
    ))];
    let set = [art(&a, b"the promoted bytes", &theirs)];
    let v = gate.check(&hop(&set)).expect("the gate decides");
    assert!(
        !v.is_allowed(),
        "a statement about other bytes must not promote these ones: {v:?}"
    );

    // (d) A set of two where only ONE is unattested: the whole hop is refused,
    // and the refusal names the offender rather than the repository.
    let b = ArtifactId {
        namespace: None,
        name: "rand".into(),
        version: "0.8.5".into(),
    };
    let ok = [slsa_statement_about(&ArtifactDigest::of_sha256(b"good"))];
    let set = [art(&a, b"good", &ok), art(&b, b"bad", &none)];
    let v = gate.check(&hop(&set)).expect("the gate decides");
    assert!(!v.is_allowed());
    assert_eq!(v.subject(), Some("__rand__0.8.5"), "{v:?}");

    // Through a resolved GateSet, which is how the server reaches it: the
    // Decision records that the gate RAN, so an allow is evidence of a check
    // rather than evidence that nothing was configured.
    let cat = GateCatalog::new().with(Arc::new(ProvenanceGate::new(ProvenancePolicy::Require, 1)));
    let gs = cat
        .resolve(&["provenance".to_string()])
        .expect("the name resolves on this build");
    let set = [art(&a, b"unsigned", &none)];
    let d = gs.evaluate(&hop(&set));
    assert!(!d.is_allowed());
    assert!(
        !d.undecided(),
        "this is a policy refusal, not a broken gate: {}",
        d.summary()
    );
    assert_eq!(d.gates_run, vec!["provenance"]);
}

// ── 3. an estate with no root ────────────────────────────────────────────────

/// ★ An estate that declares NO trust root must say so — and must never print
/// the sentence an estate that chose `Off` prints.
///
/// This is the `Warn` default's whole defence. `Warn` refuses nothing, so the
/// only thing separating it from `Off` is that it SAYS what `Require` would have
/// refused, by frozen code, on every hop. If the two were indistinguishable,
/// an estate that never got round to signing anything would read exactly like
/// one that considered the question and declined — the defect `promote/gate.rs`
/// was written against, wearing a new coat.
#[test]
fn an_estate_that_declares_no_root_says_so_and_does_not_verify_everything() {
    let a = id();
    let none: [Statement; 0] = [];
    let set = [art(&a, b"never signed", &none)];

    // WARN, no root: the hop proceeds and the finding is on the record.
    let warn = ProvenanceGate::new(ProvenancePolicy::Warn, 0);
    assert_eq!(warn.declared_roots(), 0);
    assert!(
        warn.check(&hop(&set))
            .expect("the gate decides")
            .is_allowed(),
        "Warn must not refuse; that is the whole difference from Require"
    );
    let findings = warn.findings(&hop(&set));
    assert!(
        !findings.is_empty(),
        "Warn that finds nothing to warn about has silently become Off"
    );
    assert_eq!(
        errcode::parse(&findings.join(" ")).map(|c| c.code),
        Some("holger-attest-11"),
        "the finding must carry the frozen no-root code: {findings:?}"
    );
    let warn_posture = warn.posture();
    assert!(
        warn_posture.contains("NO attestation trust root"),
        "the posture must state the absence in words: {warn_posture}"
    );
    assert!(
        warn_posture.contains("holger-attest-11"),
        "and carry the code: {warn_posture}"
    );

    // OFF, no root: also allows — and says something DIFFERENT.
    let off = ProvenanceGate::new(ProvenancePolicy::Off, 0);
    assert!(off
        .check(&hop(&set))
        .expect("the gate decides")
        .is_allowed());
    assert!(
        off.findings(&hop(&set)).is_empty(),
        "Off has nothing to find; it was told not to look"
    );
    let off_posture = off.posture();
    assert_ne!(
        warn_posture, off_posture,
        "an absent key must NEVER read as `provenance: Off`"
    );
    assert!(
        off_posture.contains("DECLARED"),
        "Off must read as a decision on the record: {off_posture}"
    );
    assert!(
        !off_posture.contains("holger-attest-11"),
        "Off is not a finding; it must not borrow the finding's code: {off_posture}"
    );

    // REQUIRE, no root: refuses EVERY hop, and blames the server rather than the
    // artifact. A `Require` that cannot verify is never a `Require` that passes.
    let req = ProvenanceGate::new(ProvenancePolicy::Require, 0);
    let v = req.check(&hop(&set)).expect("the gate decides");
    assert!(
        !v.is_allowed(),
        "Require with nothing to chain to must refuse"
    );
    assert_eq!(
        v.subject(),
        None,
        "with no root declared the fault is the server's, not any one artifact's: {v:?}"
    );
    let reason = format!("{v:?}");
    assert_eq!(
        errcode::parse(&reason).map(|c| c.code),
        Some("holger-attest-11"),
        "not holger-promote-10: the fix is a config key, not a re-signed build: {reason}"
    );

    // ★ And with no root declared, a FULLY ATTESTED artifact is not waved
    // through either. Nothing could have been verified, so nothing was.
    let att = [slsa_statement_about(&ArtifactDigest::of_sha256(
        b"never signed",
    ))];
    let set = [art(&a, b"never signed", &att)];
    assert!(
        !req.check(&hop(&set))
            .expect("the gate decides")
            .is_allowed(),
        "with no declared root there is nothing for a statement to chain TO, so a \
         statement must not be mistaken for evidence"
    );
    assert!(
        ProvenancePolicy::default() == ProvenancePolicy::Warn,
        "the defended default is Warn; see ProvenancePolicy's docs"
    );
}