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

nordisk/znippy

plugins/native/znippy-plugin-elf/src/provenance.rs

//! One signed statement per artifact: source commit → recipe → output digest.
//!
//! **Nothing here is a new signing scheme, a new envelope or a new predicate.**
//! All four already exist and are reused verbatim:
//!
//! | piece | where it already lives |
//! |---|---|
//! | the DSSE envelope, PAE, sign and verify | `znippy_common::sign::{DsseEnvelope, dsse_sign, dsse_verify}` |
//! | `payloadType` `application/vnd.in-toto+json` | `znippy_common::sign::DSSE_PAYLOAD_TYPE_INTOTO` |
//! | `predicateType` `https://slsa.dev/provenance/v1` | already on holger's accept allowlist (`AttestStore`), so no policy change |
//! | the store, trust roots and subject binding | `holger_server_lib::attest::AttestStore::{accept, emit_signed}` |
//!
//! What is genuinely new is the **binding**, and it is deliberately thin.
//!
//! # The statement REFERENCES the build-thing artifact, it does not restate it
//!
//! A build coordinate copied into a statement is a second record that can
//! disagree with the first. So the predicate carries *pointers by digest*
//! (`resolvedDependencies`) and only the minimum needed to find them
//! (`externalParameters`). A verifier fetches the referenced record and
//! **re-derives**; it never trusts a transcription.
//!
//! Two references are available today and both are stable, content-addressed
//! identities:
//!
//! * the recipe — `nornir_build_thing::contract::Coordinate`, `krate:version:hash`,
//!   where `hash` is SHA-256 over the recipe crate's `src/**/*.rs`. This one the
//!   binary carries **itself**, in its `.nordisk.buildthing` receipt, so an
//!   airgapped verifier needs nothing but the file.
//! * the build record — `nornir_build_thing::provenance::Attestation::digest`,
//!   SHA-256 over the canonical `BuildProvenance` bytes, which is the digest that
//!   is actually **signed** today (CMS or DSSE, via nornir's `ZnippySealer`).
//!
//! # Verification looks at the disk
//!
//! "The payload is signed" proves a statement exists. It proves nothing about the
//! file lying next to it. [`verify_against_disk`] recomputes the subject digest
//! from the bytes on disk and compares; a signed statement about a *different*
//! artifact is [`ProvenanceRefusal::SubjectIsNotThisFile`], which is a louder
//! failure than an unsigned one.

use std::path::Path;

use sha2::{Digest, Sha256};
use znippy_common::sign::{
    ArchiveSigner, CertStore, DSSE_PAYLOAD_TYPE_INTOTO, DsseEnvelope, dsse_sign, dsse_verify,
};

use crate::facts::{ElfFacts, Recipe};

/// The in-toto Statement type URI. Same constant as
/// `nornir_build_thing::provenance::IN_TOTO_STATEMENT_TYPE` and
/// `holger_server_lib::attest::IN_TOTO_STATEMENT_TYPE` — spelled here so this
/// crate does not take a dependency on either, never to differ from them.
pub const IN_TOTO_STATEMENT_TYPE: &str = "https://in-toto.io/Statement/v1";

/// The SLSA Provenance v1 predicate type. Already on holger's accept allowlist.
pub const SLSA_PREDICATE_TYPE: &str = "https://slsa.dev/provenance/v1";

/// What a caller knows that the binary cannot know about itself: which build
/// record vouches for it, and who the forge was.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BuildRecordRef {
    /// `BuildProvenance.id` — the record's own UUID.
    pub id: Option<String>,
    /// `Attestation::digest_hex()` — the SIGNED digest of the canonical record.
    pub digest_sha256: Option<String>,
    /// The repo the build was cut from, for the `git+nordisk/<repo>` material URI.
    pub repo: Option<String>,
    /// `runDetails.builder.id`.
    pub builder: Option<String>,
}

/// Why a statement was refused. Each is a sentence that names both sides.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProvenanceRefusal {
    /// The statement is signed, and it is about some OTHER file.
    SubjectIsNotThisFile { on_disk: String, in_statement: String },
    /// The statement names no subject digest at all.
    SubjectMissing,
    /// The payload is not an in-toto Statement / SLSA provenance.
    NotSlsaProvenance { payload_type: String, predicate_type: String },
    /// The signature did not verify or did not chain to a trusted root.
    NotSigned { why: String },
}

impl std::fmt::Display for ProvenanceRefusal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "REFUSED [elf-provenance] ")?;
        match self {
            ProvenanceRefusal::SubjectIsNotThisFile { on_disk, in_statement } => write!(
                f,
                "the file on disk hashes to {on_disk} and the signed statement is about \
                 {in_statement} — the signature is valid and it vouches for a DIFFERENT \
                 artifact; fetch the artifact this statement names, or the statement for \
                 the artifact you have"
            ),
            ProvenanceRefusal::SubjectMissing => write!(
                f,
                "the statement names no subject digest, so it binds to nothing — a \
                 provenance statement that does not say WHAT it is about cannot vouch \
                 for anything"
            ),
            ProvenanceRefusal::NotSlsaProvenance { payload_type, predicate_type } => write!(
                f,
                "the envelope carries payloadType {payload_type} / predicateType \
                 {predicate_type}, not an in-toto SLSA provenance — payload-type \
                 confusion is refused by name, never guessed at"
            ),
            ProvenanceRefusal::NotSigned { why } => {
                write!(f, "the envelope did not verify against the trusted roots: {why}")
            }
        }
    }
}

/// Lowercase-hex SHA-256 of `bytes`.
pub fn sha256_hex(bytes: &[u8]) -> String {
    let mut s = String::with_capacity(64);
    for b in Sha256::digest(bytes) {
        s.push(char::from_digit(u32::from(b >> 4), 16).unwrap_or('0'));
        s.push(char::from_digit(u32::from(b & 0xf), 16).unwrap_or('0'));
    }
    s
}

/// Build the in-toto Statement for one artifact.
///
/// `subject_name` is how the artifact is filed (its `relative_path` in the
/// archive); `data` is the artifact's bytes — the digest is taken from THEM, not
/// from anything a caller asserts.
///
/// The canonical bytes are compact and **key-sorted**, because `serde_json`'s
/// `Map` is a `BTreeMap`: producer and verifier therefore fold to the same bytes,
/// the same property `nornir_build_thing::provenance::slsa_statement_bytes`
/// relies on.
pub fn statement(
    subject_name: &str,
    data: &[u8],
    facts: &ElfFacts,
    record: &BuildRecordRef,
) -> serde_json::Value {
    use serde_json::json;
    let r: &Recipe = &facts.recipe;

    let mut digest = serde_json::Map::new();
    digest.insert("sha256".into(), json!(sha256_hex(data)));
    // The build-id is not a digest of the file, so it does NOT go in `digest`
    // (in-toto digests are content hashes). It rides as a byproduct-grade
    // annotation on the subject instead, because it is the key debuginfod and
    // the index both use.
    let mut subject = serde_json::Map::new();
    subject.insert("name".into(), json!(subject_name));
    subject.insert("digest".into(), serde_json::Value::Object(digest));
    if let Some(b) = &facts.build_id {
        subject.insert("annotations".into(), json!({ "buildId": b }));
    }

    // ── resolvedDependencies: POINTERS, never copies ──────────────────────────
    let mut resolved: Vec<serde_json::Value> = Vec::new();
    if let Some(coord) = &r.coordinate {
        // `krate:version:hash` → a URI that names the recipe and a digest that
        // pins its CODE. The binary carries this itself, so it works airgapped.
        let (uri, sha) = split_coordinate(coord);
        let mut e = serde_json::Map::new();
        e.insert("name".into(), json!("build-thing-recipe"));
        e.insert("uri".into(), json!(uri));
        if let Some(sha) = sha {
            e.insert("digest".into(), json!({ "sha256": sha }));
        }
        resolved.push(serde_json::Value::Object(e));
    }
    if let Some(d) = &record.digest_sha256 {
        let uri = match &record.id {
            Some(id) => format!("nornir.build-provenance/v1/{id}"),
            None => "nornir.build-provenance/v1".to_string(),
        };
        resolved.push(json!({
            "name": "build-thing-provenance",
            "uri": uri,
            "digest": { "sha256": d },
        }));
    }
    if let Some(commit) = &r.source_commit {
        let repo = record.repo.as_deref().unwrap_or("unknown");
        resolved.push(json!({
            "name": "source",
            "uri": format!("git+nordisk/{repo}"),
            "digest": { "gitCommit": commit },
        }));
    }
    if let Some(image) = &r.toolchain_image {
        let mut e = serde_json::Map::new();
        e.insert("name".into(), json!("toolchain-image"));
        e.insert("uri".into(), json!(image));
        if let Some(d) = &r.toolchain_image_sha256 {
            e.insert("digest".into(), json!({ "sha256": d }));
        }
        resolved.push(serde_json::Value::Object(e));
    }

    // ── externalParameters: the MINIMUM needed to find the record ─────────────
    let mut external = serde_json::Map::new();
    if let Some(v) = &r.variant {
        external.insert("variant".into(), json!(v));
    }
    if let Some(t) = &r.target {
        external.insert("target".into(), json!(t));
    }

    let build_type = format!(
        "https://nordisk.dev/nornir/build-thing/variant/{}",
        r.variant.as_deref().unwrap_or("unknown")
    );

    json!({
        "_type": IN_TOTO_STATEMENT_TYPE,
        "subject": [serde_json::Value::Object(subject)],
        "predicateType": SLSA_PREDICATE_TYPE,
        "predicate": {
            "buildDefinition": {
                "buildType": build_type,
                "externalParameters": serde_json::Value::Object(external),
                // What the ARTIFACT is, measured — never a claim about it.
                "internalParameters": {
                    "elfMachine": facts.machine,
                    "elfClass": facts.class,
                    "elfType": facts.etype,
                    "elfAbiLevel": facts.abi_level,
                },
                "resolvedDependencies": resolved,
            },
            "runDetails": {
                "builder": {
                    "id": format!(
                        "nornir-build-thing://{}",
                        record.builder.as_deref().unwrap_or("unknown")
                    ),
                    "builderDependencies": [],
                },
                "byproducts": byproducts(facts),
            },
        },
    })
}

/// What the build left behind that a consumer can use without the artifact:
/// today, the embedded dependency tree's root.
fn byproducts(facts: &ElfFacts) -> Vec<serde_json::Value> {
    let mut v = Vec::new();
    if let Some(root) = &facts.auditable_root {
        v.push(serde_json::json!({ "name": "dep-v0-root", "annotations": { "root": root } }));
    }
    v
}

/// `krate:version:hash` → (`nornir+buildthing://krate@version`, `hash`).
fn split_coordinate(coord: &str) -> (String, Option<String>) {
    let parts: Vec<&str> = coord.split(':').collect();
    match parts.as_slice() {
        [k, v, h] => (format!("nornir+buildthing://{k}@{v}"), Some((*h).to_string())),
        [k, v] => (format!("nornir+buildthing://{k}@{v}"), None),
        _ => (format!("nornir+buildthing://{coord}"), None),
    }
}

/// The exact bytes a DSSE envelope signs: compact, key-sorted JSON.
pub fn statement_bytes(
    subject_name: &str,
    data: &[u8],
    facts: &ElfFacts,
    record: &BuildRecordRef,
) -> anyhow::Result<Vec<u8>> {
    Ok(serde_json::to_vec(&statement(subject_name, data, facts, record))?)
}

/// Sign the statement as a DSSE envelope — a one-line delegation to znippy's own
/// `dsse_sign`, which is also what holger's `AttestStore::emit_signed` calls.
/// There is exactly one signing path in the estate and this is it.
pub fn seal(
    subject_name: &str,
    data: &[u8],
    facts: &ElfFacts,
    record: &BuildRecordRef,
    signer: &dyn ArchiveSigner,
) -> anyhow::Result<DsseEnvelope> {
    let payload = statement_bytes(subject_name, data, facts, record)?;
    dsse_sign(DSSE_PAYLOAD_TYPE_INTOTO, &payload, &[signer])
}

/// What a verification found. Both halves are reported, always: a caller must be
/// able to tell "signed but about another file" from "not signed at all".
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Verified {
    /// The digest recomputed FROM THE BYTES ON DISK.
    pub on_disk_sha256: String,
    /// The subject digest the statement named.
    pub statement_sha256: Option<String>,
    /// The recipe the statement points at, for the `elf-recipe` comparison.
    pub statement_recipe: Recipe,
}

/// Verify an envelope **against the file on disk**.
///
/// Order, and it is the order holger's `accept` already enforces: parse, then
/// payload type, then **subject binding**, then cryptography. Nothing is
/// accepted because it was signed; it is accepted because it was signed AND it
/// is about this file.
pub fn verify_against_disk(
    artifact: &Path,
    envelope_json: &[u8],
    signer_certs_der: &[Vec<u8>],
    roots: &CertStore,
) -> Result<Verified, ProvenanceRefusal> {
    let data = std::fs::read(artifact).map_err(|e| ProvenanceRefusal::NotSigned {
        why: format!("cannot read {}: {e}", artifact.display()),
    })?;
    verify_bytes_against(&data, envelope_json, signer_certs_der, roots)
}

/// [`verify_against_disk`] for bytes already in hand (an archive member).
pub fn verify_bytes_against(
    data: &[u8],
    envelope_json: &[u8],
    signer_certs_der: &[Vec<u8>],
    roots: &CertStore,
) -> Result<Verified, ProvenanceRefusal> {
    let text = std::str::from_utf8(envelope_json).map_err(|e| ProvenanceRefusal::NotSigned {
        why: format!("envelope is not UTF-8: {e}"),
    })?;
    let env = DsseEnvelope::from_json(text)
        .map_err(|e| ProvenanceRefusal::NotSigned { why: e.to_string() })?;

    let payload: serde_json::Value = serde_json::from_slice(&env.payload).map_err(|e| {
        ProvenanceRefusal::NotSlsaProvenance {
            payload_type: env.payload_type.clone(),
            predicate_type: format!("<unparsable: {e}>"),
        }
    })?;
    let predicate_type =
        payload.get("predicateType").and_then(|v| v.as_str()).unwrap_or("").to_string();
    if env.payload_type != DSSE_PAYLOAD_TYPE_INTOTO
        || payload.get("_type").and_then(|v| v.as_str()) != Some(IN_TOTO_STATEMENT_TYPE)
        || predicate_type != SLSA_PREDICATE_TYPE
    {
        return Err(ProvenanceRefusal::NotSlsaProvenance {
            payload_type: env.payload_type.clone(),
            predicate_type,
        });
    }

    // ── the subject binding, BEFORE the cryptography ──────────────────────────
    let on_disk = sha256_hex(data);
    let statement_sha = payload
        .get("subject")
        .and_then(|s| s.as_array())
        .and_then(|a| a.first())
        .and_then(|s| s.get("digest"))
        .and_then(|d| d.get("sha256"))
        .and_then(|v| v.as_str())
        .map(str::to_owned);
    match &statement_sha {
        None => return Err(ProvenanceRefusal::SubjectMissing),
        Some(s) if *s != on_disk => {
            return Err(ProvenanceRefusal::SubjectIsNotThisFile {
                on_disk,
                in_statement: s.clone(),
            });
        }
        Some(_) => {}
    }

    dsse_verify(&env, signer_certs_der, roots)
        .map_err(|e| ProvenanceRefusal::NotSigned { why: e.to_string() })?;

    Ok(Verified {
        on_disk_sha256: on_disk,
        statement_sha256: statement_sha,
        statement_recipe: recipe_from_statement(&payload),
    })
}

/// Recover the recipe the statement POINTS AT, so the caller can compare it with
/// what the binary says about itself (`elf-recipe`).
pub fn recipe_from_statement(payload: &serde_json::Value) -> Recipe {
    let mut r = Recipe::default();
    let bd = payload.get("predicate").and_then(|p| p.get("buildDefinition"));
    if let Some(ext) = bd.and_then(|b| b.get("externalParameters")) {
        r.variant = ext.get("variant").and_then(|v| v.as_str()).map(str::to_owned);
        r.target = ext.get("target").and_then(|v| v.as_str()).map(str::to_owned);
    }
    if let Some(deps) = bd.and_then(|b| b.get("resolvedDependencies")).and_then(|d| d.as_array()) {
        for d in deps {
            let name = d.get("name").and_then(|v| v.as_str()).unwrap_or("");
            let uri = d.get("uri").and_then(|v| v.as_str()).unwrap_or("");
            let sha = d.get("digest").and_then(|x| x.get("sha256")).and_then(|v| v.as_str());
            match name {
                "build-thing-recipe" => {
                    // `nornir+buildthing://krate@version` + digest → krate:version:hash
                    let bare = uri.strip_prefix("nornir+buildthing://").unwrap_or(uri);
                    let (k, v) = bare.split_once('@').unwrap_or((bare, ""));
                    r.coordinate = Some(match sha {
                        Some(h) => format!("{k}:{v}:{h}"),
                        None => format!("{k}:{v}"),
                    });
                }
                "source" => {
                    r.source_commit = d
                        .get("digest")
                        .and_then(|x| x.get("gitCommit"))
                        .and_then(|v| v.as_str())
                        .map(str::to_owned);
                }
                "toolchain-image" => {
                    r.toolchain_image = Some(uri.to_string());
                    r.toolchain_image_sha256 = sha.map(str::to_owned);
                }
                _ => {}
            }
        }
    }
    r
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::facts;
    use crate::fixture::*;

    fn a_binary_with_a_receipt() -> Vec<u8> {
        let receipt = br#"{"receipt_version":1,
            "build_thing":{"krate":"gunnar-build-thing","version":"0.3.1","hash":"abc123"},
            "atom":"gunnar-installer-metered","target":"iso-installer",
            "settings":{"containers":"on"},
            "inputs":{"commit":"451288a0000000000000000000000000000000aa",
                      "toolchain_image":"nordisk-cargo-toolchain",
                      "toolchain_image_sha256":"3d5115baa07d03ca51eb3e44ebb86b4e64e679bd88f38e156ef9da12c35d2143"}}"#;
        Builder::default()
            .etype(ET_EXEC)
            .text(64)
            .note(".note.gnu.build-id", b"GNU", 3, &[0xaa, 0xbb, 0xcc])
            .section(Section::new(".nordisk.buildthing", SHT_PROGBITS, receipt.to_vec()))
            .build()
    }

    fn a_record() -> BuildRecordRef {
        BuildRecordRef {
            id: Some("11111111-2222-3333-4444-555555555555".into()),
            digest_sha256: Some("ff".repeat(32)),
            repo: Some("gunnar".into()),
            builder: Some("alice@t14s".into()),
        }
    }

    fn signer() -> (znippy_common::sign::Ed25519Signer, Vec<u8>) {
        let (ca_key, ca_der) = znippy_common::sign::dev::mint_ca("elf test CA").unwrap();
        let s =
            znippy_common::sign::dev::new_ed25519_signer(&ca_key, &ca_der, "elf test signer")
                .unwrap();
        (s, ca_der)
    }

    fn roots_of(ca_der: &[u8]) -> CertStore {
        CertStore::from_der_certs(&[ca_der.to_vec()]).unwrap()
    }

    #[test]
    fn the_subject_digest_is_the_hash_of_the_bytes_and_of_nothing_else() {
        let data = a_binary_with_a_receipt();
        let f = facts::read(&data).unwrap();
        let st = statement("bin/gunnar", &data, &f, &a_record());
        let got = st["subject"][0]["digest"]["sha256"].as_str().unwrap();
        assert_eq!(got, sha256_hex(&data));
        assert_eq!(st["subject"][0]["name"].as_str().unwrap(), "bin/gunnar");
        // the build-id rides as an annotation, NOT as a digest — it is not one
        assert_eq!(st["subject"][0]["annotations"]["buildId"].as_str().unwrap(), "aabbcc");
    }

    #[test]
    fn the_statement_points_at_the_build_thing_artifacts_instead_of_copying_them() {
        let data = a_binary_with_a_receipt();
        let f = facts::read(&data).unwrap();
        let st = statement("bin/gunnar", &data, &f, &a_record());
        let deps = st["predicate"]["buildDefinition"]["resolvedDependencies"].as_array().unwrap();

        let recipe = deps.iter().find(|d| d["name"] == "build-thing-recipe").expect("recipe ref");
        assert_eq!(recipe["uri"], "nornir+buildthing://gunnar-build-thing@0.3.1");
        assert_eq!(recipe["digest"]["sha256"], "abc123");

        let record = deps.iter().find(|d| d["name"] == "build-thing-provenance").expect("record");
        assert_eq!(record["uri"], "nornir.build-provenance/v1/11111111-2222-3333-4444-555555555555");
        assert_eq!(record["digest"]["sha256"], "ff".repeat(32));

        let src = deps.iter().find(|d| d["name"] == "source").expect("source ref");
        assert_eq!(src["digest"]["gitCommit"], "451288a0000000000000000000000000000000aa");

        let tc = deps.iter().find(|d| d["name"] == "toolchain-image").expect("toolchain ref");
        assert_eq!(tc["uri"], "nordisk-cargo-toolchain");
        assert_eq!(
            tc["digest"]["sha256"],
            "3d5115baa07d03ca51eb3e44ebb86b4e64e679bd88f38e156ef9da12c35d2143"
        );

        // externalParameters holds the MINIMUM to find the record, not a copy of it
        let ext = &st["predicate"]["buildDefinition"]["externalParameters"];
        assert_eq!(ext["variant"], "gunnar-installer-metered");
        assert_eq!(ext["target"], "iso-installer");
        assert!(ext.get("settings").is_none(), "the recipe's contents must NOT be restated");
        assert!(ext.get("features").is_none(), "the recipe's contents must NOT be restated");
    }

    #[test]
    fn the_canonical_bytes_are_key_sorted_and_stable() {
        let data = a_binary_with_a_receipt();
        let f = facts::read(&data).unwrap();
        let a = statement_bytes("bin/gunnar", &data, &f, &a_record()).unwrap();
        let b = statement_bytes("bin/gunnar", &data, &f, &a_record()).unwrap();
        assert_eq!(a, b, "producer and verifier must fold the same bytes");
        let text = String::from_utf8(a).unwrap();
        assert!(text.starts_with(r#"{"_type":"https://in-toto.io/Statement/v1","predicate":"#), "{text}");
        assert!(!text.contains("\n "), "canonical bytes are compact");
    }

    #[test]
    fn a_sealed_statement_verifies_against_the_file_it_is_about() {
        let data = a_binary_with_a_receipt();
        let f = facts::read(&data).unwrap();
        let (s, ca) = signer();
        let env = seal("bin/gunnar", &data, &f, &a_record(), &s).unwrap();
        let json = env.to_json().unwrap();
        let v = verify_bytes_against(&data, json.as_bytes(), &[s.certificate_der().to_vec()], &roots_of(&ca))
            .expect("must verify");
        assert_eq!(v.on_disk_sha256, sha256_hex(&data));
        assert_eq!(v.statement_sha256.as_deref(), Some(v.on_disk_sha256.as_str()));
    }

    #[test]
    fn a_perfectly_signed_statement_about_another_file_is_refused() {
        // THE point: "the payload is signed" proves a statement exists. It proves
        // nothing about the file next to it.
        let signed_about = a_binary_with_a_receipt();
        let on_disk = Builder::default().etype(ET_EXEC).text(128).build();
        assert_ne!(signed_about, on_disk);

        let f = facts::read(&signed_about).unwrap();
        let (s, ca) = signer();
        let env = seal("bin/gunnar", &signed_about, &f, &a_record(), &s).unwrap();
        let json = env.to_json().unwrap();

        let err = verify_bytes_against(
            &on_disk,
            json.as_bytes(),
            &[s.certificate_der().to_vec()],
            &roots_of(&ca),
        )
        .expect_err("a valid signature over ANOTHER artifact must be refused");
        match &err {
            ProvenanceRefusal::SubjectIsNotThisFile { on_disk: d, in_statement } => {
                assert_eq!(*d, sha256_hex(&on_disk));
                assert_eq!(*in_statement, sha256_hex(&signed_about));
            }
            other => panic!("wrong refusal: {other}"),
        }
        assert!(err.to_string().contains("vouches for a DIFFERENT artifact"), "{err}");
    }

    #[test]
    fn one_flipped_byte_on_disk_breaks_the_binding() {
        let data = a_binary_with_a_receipt();
        let f = facts::read(&data).unwrap();
        let (s, ca) = signer();
        let json = seal("bin/gunnar", &data, &f, &a_record(), &s).unwrap().to_json().unwrap();

        let mut tampered = data.clone();
        let last = tampered.len() - 1;
        tampered[last] ^= 0xff;
        assert!(matches!(
            verify_bytes_against(&tampered, json.as_bytes(), &[s.certificate_der().to_vec()], &roots_of(&ca)),
            Err(ProvenanceRefusal::SubjectIsNotThisFile { .. })
        ));
    }

    #[test]
    fn an_envelope_signed_by_an_untrusted_key_is_refused() {
        let data = a_binary_with_a_receipt();
        let f = facts::read(&data).unwrap();
        let (s, _ca) = signer();
        let (_other, other_ca) = signer(); // a DIFFERENT CA
        let json = seal("bin/gunnar", &data, &f, &a_record(), &s).unwrap().to_json().unwrap();
        let err = verify_bytes_against(
            &data,
            json.as_bytes(),
            &[s.certificate_der().to_vec()],
            &roots_of(&other_ca),
        )
        .expect_err("a signer that does not chain must be refused");
        assert!(matches!(err, ProvenanceRefusal::NotSigned { .. }), "{err}");
    }

    #[test]
    fn a_payload_that_is_not_slsa_provenance_is_refused_by_name() {
        let data = a_binary_with_a_receipt();
        let f = facts::read(&data).unwrap();
        let (s, ca) = signer();
        let mut st = statement("bin/gunnar", &data, &f, &a_record());
        st["predicateType"] = serde_json::json!("https://cyclonedx.org/bom");
        let payload = serde_json::to_vec(&st).unwrap();
        let env = dsse_sign(DSSE_PAYLOAD_TYPE_INTOTO, &payload, &[&s]).unwrap();
        let err = verify_bytes_against(
            &data,
            env.to_json().unwrap().as_bytes(),
            &[s.certificate_der().to_vec()],
            &roots_of(&ca),
        )
        .expect_err("payload-type confusion must be refused, never guessed at");
        assert!(matches!(err, ProvenanceRefusal::NotSlsaProvenance { .. }), "{err}");
    }

    #[test]
    fn a_statement_with_no_subject_digest_binds_to_nothing_and_is_refused() {
        let data = a_binary_with_a_receipt();
        let f = facts::read(&data).unwrap();
        let (s, ca) = signer();
        let mut st = statement("bin/gunnar", &data, &f, &a_record());
        st["subject"] = serde_json::json!([{ "name": "bin/gunnar" }]);
        let payload = serde_json::to_vec(&st).unwrap();
        let env = dsse_sign(DSSE_PAYLOAD_TYPE_INTOTO, &payload, &[&s]).unwrap();
        let err = verify_bytes_against(
            &data,
            env.to_json().unwrap().as_bytes(),
            &[s.certificate_der().to_vec()],
            &roots_of(&ca),
        )
        .expect_err("a statement that does not say what it is about cannot vouch");
        assert!(matches!(err, ProvenanceRefusal::SubjectMissing), "{err}");
    }

    #[test]
    fn the_pointers_round_trip_so_elf_recipe_can_compare_them() {
        let data = a_binary_with_a_receipt();
        let f = facts::read(&data).unwrap();
        let st = statement("bin/gunnar", &data, &f, &a_record());
        let recovered = recipe_from_statement(&st);
        // Exactly the fields the elf-recipe refusal compares.
        assert_eq!(recovered.coordinate, f.recipe.coordinate);
        assert_eq!(recovered.source_commit, f.recipe.source_commit);
        assert_eq!(recovered.toolchain_image_sha256, f.recipe.toolchain_image_sha256);
        assert_eq!(recovered.variant, f.recipe.variant);

        // And a statement that points at a DIFFERENT toolchain image is caught by
        // the refusal that already exists, not by a second rule here.
        let coord = crate::refusal::Coordinate {
            path: "bin/gunnar".into(),
            expect_toolchain_image_sha256: Some("f29e517c".into()),
            ..Default::default()
        };
        let host = crate::refusal::HostFacts {
            machine: f.machine.clone(),
            abi_level: None,
            interpreters: vec![],
        };
        assert!(crate::refusal::check(&f, &host, &coord).iter().any(|r| r.name() == "elf-recipe"));
    }

    #[test]
    fn verification_reads_the_file_on_disk_not_a_caller_supplied_digest() {
        let dir = std::env::temp_dir().join(format!("elf-prov-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("gunnar");
        let data = a_binary_with_a_receipt();
        std::fs::write(&path, &data).unwrap();

        let f = facts::read(&data).unwrap();
        let (s, ca) = signer();
        let json = seal("bin/gunnar", &data, &f, &a_record(), &s).unwrap().to_json().unwrap();
        let certs = [s.certificate_der().to_vec()];
        let roots = roots_of(&ca);

        assert!(verify_against_disk(&path, json.as_bytes(), &certs, &roots).is_ok());

        // Change the file on disk. The envelope is untouched and still valid.
        std::fs::write(&path, b"\x7fELF replaced").unwrap();
        assert!(matches!(
            verify_against_disk(&path, json.as_bytes(), &certs, &roots),
            Err(ProvenanceRefusal::SubjectIsNotThisFile { .. })
        ));
        let _ = std::fs::remove_dir_all(&dir);
    }
}