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/referrers.rs

//! **OCI 1.1 referrers: Holger's own evidence, in the shapes a registry client
//! verifies** (lane HOL-REFERRERS, owner 2026-10-30: "the supply-chain checks must
//! not end at Holger").
//!
//! Holger signs a DSSE scan-evidence statement and a CycloneDX SBOM statement about
//! every artifact its intake chain scans ([`crate::scanner::attestation`]). For a
//! container image (the `.oci.tar` layout a pull-through keeps) those statements also
//! name the image's manifest digests as subjects ([`image_subjects`]), so the
//! [`AttestStore`] files them under the digest a client pulls by. This module shows
//! that store to a registry client, in the two shapes cosign reads:
//!
//! * **an OCI 1.1 referrer** per envelope: an artifact manifest whose `subject` is the
//!   image, `artifactType` `application/vnd.dev.sigstore.bundle.v0.3+json`, and one
//!   layer, a Sigstore bundle carrying the filed envelope's payload and signatures (cosign 2.4+
//!   with the new bundle format, `cosign tree`, `oras discover`);
//! * **a layer of the `sha256-<hex>.att` tag**, the scheme older cosign reads: the
//!   envelope itself, `application/vnd.dsse.envelope.v1+json`, beside whatever
//!   attestations the image's own `.att` already carries ([`att_manifest`]).
//!
//! Nothing is re-signed: the `.att` layer is byte-identical to the envelope an
//! airgapped box reads from the export bundle, the bundle carries its payload and
//! signatures unchanged, so `cosign verify-attestation --key <the public
//! key of attestation.signer_cert>` checks Holger's verdict through either shape. Every
//! object is deterministic (sorted JSON, no clock), so its digest is stable across
//! requests, restarts and boxes.

use std::collections::BTreeMap;

use serde_json::{json, Value};

use crate::attest::{ArtifactDigest, AttestStore, StoredAttestation};

/// The referrers API answers an image index.
pub const INDEX_MEDIA_TYPE: &str = "application/vnd.oci.image.index.v1+json";
/// The manifest every referrer here is.
pub const MANIFEST_MEDIA_TYPE: &str = "application/vnd.oci.image.manifest.v1+json";
/// OCI 1.1's empty config.
pub const EMPTY_MEDIA_TYPE: &str = "application/vnd.oci.empty.v1+json";
/// The empty config's bytes.
pub const EMPTY_BLOB: &[u8] = b"{}";
/// A DSSE envelope, as cosign files an attestation layer.
pub const DSSE_MEDIA_TYPE: &str = "application/vnd.dsse.envelope.v1+json";
/// A Sigstore bundle, cosign's new format.
pub const BUNDLE_MEDIA_TYPE: &str = "application/vnd.dev.sigstore.bundle.v0.3+json";
/// The config a cosign `.att` image carries.
pub const CONFIG_MEDIA_TYPE: &str = "application/vnd.oci.image.config.v1+json";
/// The annotation that marks a referrer as Holger's own evidence.
pub const HOLGER_EVIDENCE_ANNOTATION: &str = "rs.holger.evidence";
/// cosign's layer annotation for a signature (empty for an attestation).
pub const COSIGN_SIGNATURE_ANNOTATION: &str = "dev.cosignproject.cosign/signature";

/// `sha256:<hex>` of `bytes`.
pub fn sha256_digest(bytes: &[u8]) -> String {
    format!("sha256:{}", ArtifactDigest::of_sha256(bytes).hex())
}

fn descriptor(media_type: &str, bytes: &[u8]) -> Value {
    json!({ "mediaType": media_type, "digest": sha256_digest(bytes), "size": bytes.len() })
}

/// **The descriptor a referrers index lists for one manifest** (OCI 1.1): media
/// type, digest and size, its `artifactType` (the manifest's own, else its config's
/// media type) and its annotations.
pub fn descriptor_of(manifest: &[u8]) -> Value {
    let v: Value = serde_json::from_slice(manifest).unwrap_or(Value::Null);
    let mut d = descriptor(
        v.get("mediaType")
            .and_then(Value::as_str)
            .unwrap_or(MANIFEST_MEDIA_TYPE),
        manifest,
    );
    if let Some(t) = v
        .get("artifactType")
        .and_then(Value::as_str)
        .or_else(|| v.pointer("/config/mediaType").and_then(Value::as_str))
    {
        d["artifactType"] = Value::String(t.to_string());
    }
    if let Some(a) = v
        .get("annotations")
        .filter(|a| a.as_object().is_some_and(|o| !o.is_empty()))
    {
        d["annotations"] = a.clone();
    }
    d
}

/// **A referrers answer**: `descriptors` as an image index, filtered to one
/// `artifactType` when asked. The flag says whether a filter was applied (the
/// `OCI-Filters-Applied` header).
pub fn index(descriptors: Vec<Value>, artifact_type: Option<&str>) -> (Vec<u8>, bool) {
    let mut seen = std::collections::BTreeSet::new();
    let manifests: Vec<Value> = descriptors
        .into_iter()
        .filter(|d| {
            artifact_type.is_none_or(|t| d.get("artifactType").and_then(Value::as_str) == Some(t))
        })
        .filter(|d| {
            seen.insert(
                d.get("digest")
                    .and_then(Value::as_str)
                    .unwrap_or("")
                    .to_string(),
            )
        })
        .collect();
    let body = json!({ "schemaVersion": 2, "mediaType": INDEX_MEDIA_TYPE, "manifests": manifests })
        .to_string()
        .into_bytes();
    (body, artifact_type.is_some())
}

/// The `artifactType` a referrers query asks for (`?artifactType=…`), percent-decoded.
pub fn artifact_type_query(query: &str) -> Option<String> {
    query.split('&').find_map(|kv| {
        let (k, v) = kv.split_once('=')?;
        (k == "artifactType" && !v.is_empty()).then(|| percent_decode(v))
    })
}

fn percent_decode(s: &str) -> String {
    let b = s.as_bytes();
    let mut out = Vec::with_capacity(b.len());
    let mut i = 0;
    while i < b.len() {
        if b[i] == b'%' && i + 2 < b.len() {
            if let Ok(x) = u8::from_str_radix(&s[i + 1..i + 3], 16) {
                out.push(x);
                i += 3;
                continue;
            }
        }
        // A literal `+` stays a `+` (`application/spdx+json` sent unencoded).
        out.push(b[i]);
        i += 1;
    }
    String::from_utf8_lossy(&out).into_owned()
}

/// `sha256-<hex>.sig|.att|.sbom`: the tag scheme older cosign uses. `(subject
/// digest, suffix)`.
pub fn cosign_tag(reference: &str) -> Option<(String, &str)> {
    let rest = reference.strip_prefix("sha256-")?;
    let (hex, suffix) = rest.split_once('.')?;
    let ok = hex.len() == 64
        && hex
            .bytes()
            .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase());
    (ok && matches!(suffix, "sig" | "att" | "sbom")).then(|| (format!("sha256:{hex}"), suffix))
}

/// **The image digests a layout tar names**: the top manifest or index and each
/// platform manifest it keeps (`holger/media-types.json`'s `images`; a layout written
/// before the attached artifacts were kept lists only images under `manifests`).
/// Each becomes a subject of Holger's statements about the layout, named
/// `<image>@<digest>`. Empty for anything that is not such a layout.
pub fn image_subjects(file_name: &str, bytes: &[u8]) -> Vec<(String, ArtifactDigest)> {
    if !file_name.ends_with(".oci.tar") {
        return Vec::new();
    }
    let Some(meta) = layout_meta(bytes) else {
        return Vec::new();
    };
    let name = meta
        .get("name")
        .and_then(Value::as_str)
        .unwrap_or("image")
        .to_string();
    let digests: Vec<String> = match meta.get("images").and_then(Value::as_array) {
        Some(a) => a
            .iter()
            .filter_map(Value::as_str)
            .map(str::to_string)
            .collect(),
        None => meta
            .get("manifests")
            .and_then(Value::as_object)
            .map(|m| m.keys().cloned().collect())
            .unwrap_or_default(),
    };
    digests
        .into_iter()
        .filter_map(|d| {
            let hex = d.strip_prefix("sha256:")?;
            Some((format!("{name}@{d}"), ArtifactDigest::sha256(hex).ok()?))
        })
        .collect()
}

fn layout_meta(bytes: &[u8]) -> Option<Value> {
    use std::io::Read as _;
    let mut ar = tar::Archive::new(bytes);
    for e in ar.entries().ok()? {
        let e = e.ok()?;
        if e.path().ok()?.to_string_lossy() == "holger/media-types.json" {
            let mut b = Vec::new();
            e.take(4 * 1024 * 1024).read_to_end(&mut b).ok()?;
            return serde_json::from_slice(&b).ok();
        }
    }
    None
}

/// The subject a referrer points at: the image manifest's own descriptor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subject {
    pub media_type: String,
    pub digest: String,
    pub size: u64,
}

impl Subject {
    /// The descriptor of `manifest`'s bytes.
    pub fn of(media_type: &str, manifest: &[u8]) -> Self {
        Self {
            media_type: media_type.to_string(),
            digest: sha256_digest(manifest),
            size: manifest.len() as u64,
        }
    }
    fn json(&self) -> Value {
        json!({ "mediaType": self.media_type, "digest": self.digest, "size": self.size })
    }
}

/// **Holger's evidence about one image digest, as OCI objects**: the referrers to
/// list, the `.att` layers, and every manifest and blob they need, by digest.
#[derive(Debug, Clone, Default)]
pub struct Evidence {
    /// Descriptors for the referrers index.
    pub referrers: Vec<Value>,
    /// The `.att` layers (cosign's tag scheme).
    pub att_layers: Vec<Value>,
    objects: BTreeMap<String, (String, Vec<u8>)>,
}

impl Evidence {
    /// Every attestation `store` holds about `subject`, in both shapes. No store or
    /// no attestation: nothing.
    pub fn of(store: Option<&AttestStore>, subject: &Subject) -> Self {
        let mut out = Self::default();
        let Some(store) = store else { return out };
        let Ok(d) = subject
            .digest
            .strip_prefix("sha256:")
            .ok_or(())
            .and_then(|h| ArtifactDigest::sha256(h).map_err(|_| ()))
        else {
            return out;
        };
        let mut found = store.list(&d).unwrap_or_default();
        found.sort_by(|a, b| a.envelope_id.cmp(&b.envelope_id));
        for a in &found {
            out.add(subject, a);
        }
        out
    }

    fn put(&mut self, media_type: &str, bytes: Vec<u8>) -> Value {
        let d = descriptor(media_type, &bytes);
        self.objects
            .insert(sha256_digest(&bytes), (media_type.to_string(), bytes));
        d
    }

    fn add(&mut self, subject: &Subject, a: &StoredAttestation) {
        let pt = a.statement.predicate_type.clone();
        let Ok(envelope) = serde_json::from_slice::<Value>(&a.envelope_json) else {
            return;
        };
        // The `.att` layer: the envelope's own bytes.
        let mut layer = self.put(DSSE_MEDIA_TYPE, a.envelope_json.clone());
        layer["annotations"] = json!({ COSIGN_SIGNATURE_ANNOTATION: "", "predicateType": pt });
        self.att_layers.push(layer);
        // The referrer: a Sigstore bundle around the same payload and signatures. The
        // key is named by hint (the signer certificate's fingerprint, the envelope's
        // keyid); the verifier brings the key. The signatures go WITHOUT their keyid,
        // as cosign writes a bundle: a DSSE keyid is an unsigned hint, and a verifier
        // that names its key otherwise (cosign 3 does) skips a signature whose keyid
        // differs (measured with cosign 3.0.2: "Found: 0, Expected 1"). No
        // transparency log: Holger's evidence is checked offline against the key the
        // operator hands out.
        let hint = envelope
            .pointer("/signatures/0/keyid")
            .and_then(Value::as_str)
            .unwrap_or("")
            .to_string();
        let mut bundled = envelope.clone();
        if let Some(sigs) = bundled.get_mut("signatures").and_then(Value::as_array_mut) {
            for sig in sigs {
                if let Some(o) = sig.as_object_mut() {
                    o.remove("keyid");
                }
            }
        }
        let bundle = json!({
            "mediaType": BUNDLE_MEDIA_TYPE,
            "verificationMaterial": { "publicKey": { "hint": hint }, "tlogEntries": [] },
            "dsseEnvelope": bundled,
        });
        let bundle = self.put(BUNDLE_MEDIA_TYPE, bundle.to_string().into_bytes());
        let config = self.put(EMPTY_MEDIA_TYPE, EMPTY_BLOB.to_vec());
        let manifest = json!({
            "schemaVersion": 2,
            "mediaType": MANIFEST_MEDIA_TYPE,
            "artifactType": BUNDLE_MEDIA_TYPE,
            "config": config,
            "layers": [bundle],
            "subject": subject.json(),
            "annotations": {
                "dev.sigstore.bundle.content": "dsse-envelope",
                "dev.sigstore.bundle.predicateType": pt,
                HOLGER_EVIDENCE_ANNOTATION: "holger",
            },
        })
        .to_string()
        .into_bytes();
        self.referrers.push(descriptor_of(&manifest));
        self.put(MANIFEST_MEDIA_TYPE, manifest);
    }

    /// An object by digest: `(media type, bytes)`.
    pub fn object(&self, digest: &str) -> Option<(&str, &[u8])> {
        self.objects
            .get(digest)
            .map(|(m, b)| (m.as_str(), b.as_slice()))
    }

    /// Take `other`'s objects into this one.
    pub fn absorb(&mut self, other: Evidence) {
        self.referrers.extend(other.referrers);
        self.att_layers.extend(other.att_layers);
        self.objects.extend(other.objects);
    }

    /// **The `sha256-<hex>.att` manifest**: the image's own `.att` (when it came
    /// with one) with Holger's layers after its own, as cosign itself appends one.
    /// Holger's layers alone when the image carried none; the image's own, byte for
    /// byte, when Holger holds nothing about it; `None` when neither exists. The new
    /// manifest and its config are kept in `self` for the blob and manifest doors.
    pub fn att_manifest(&mut self, own: Option<&[u8]>) -> Option<Vec<u8>> {
        if self.att_layers.is_empty() {
            return own.map(<[u8]>::to_vec);
        }
        let mut layers: Vec<Value> = own
            .and_then(|b| serde_json::from_slice::<Value>(b).ok())
            .and_then(|v| v.get("layers").and_then(Value::as_array).cloned())
            .unwrap_or_default();
        // A layer already there (an `.att` pushed back with Holger's layers in it) is not added twice.
        let present: std::collections::BTreeSet<String> = layers
            .iter()
            .filter_map(|l| l.get("digest").and_then(Value::as_str).map(str::to_string))
            .collect();
        layers.extend(
            self.att_layers
                .iter()
                .filter(|l| {
                    !l.get("digest")
                        .and_then(Value::as_str)
                        .is_some_and(|d| present.contains(d))
                })
                .cloned(),
        );
        let diff_ids: Vec<Value> = layers
            .iter()
            .filter_map(|l| l.get("digest").cloned())
            .collect();
        let history: Vec<Value> = layers
            .iter()
            .map(|_| json!({ "created": "0001-01-01T00:00:00Z" }))
            .collect();
        let config = json!({
            "architecture": "", "os": "", "config": {}, "created": "0001-01-01T00:00:00Z",
            "history": history, "rootfs": { "type": "layers", "diff_ids": diff_ids },
        })
        .to_string()
        .into_bytes();
        let config = self.put(CONFIG_MEDIA_TYPE, config);
        let manifest = json!({ "schemaVersion": 2, "mediaType": MANIFEST_MEDIA_TYPE, "config": config, "layers": layers })
            .to_string()
            .into_bytes();
        self.put(MANIFEST_MEDIA_TYPE, manifest.clone());
        Some(manifest)
    }
}

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

    const HEX: &str = "3d5115baa07d03ca51eb3e44ebb86b4e64e679bd88f38e156ef9da12c35d2143";

    #[test]
    fn the_legacy_cosign_tags_parse_and_nothing_else_does() {
        assert_eq!(
            cosign_tag(&format!("sha256-{HEX}.sig")),
            Some((format!("sha256:{HEX}"), "sig"))
        );
        assert_eq!(
            cosign_tag(&format!("sha256-{HEX}.att")).map(|t| t.1),
            Some("att")
        );
        assert_eq!(
            cosign_tag(&format!("sha256-{HEX}.sbom")).map(|t| t.1),
            Some("sbom")
        );
        assert_eq!(
            cosign_tag(&format!("sha256-{HEX}")),
            None,
            "the referrers tag schema is not a cosign tag"
        );
        assert_eq!(cosign_tag(&format!("sha256-{HEX}.exe")), None);
        assert_eq!(cosign_tag("sha256-abc.sig"), None);
        assert_eq!(cosign_tag("latest"), None);
    }

    #[test]
    fn an_index_filters_by_artifact_type_and_says_so() {
        let a = json!({ "digest": "sha256:a", "artifactType": "application/spdx+json" });
        let b = json!({ "digest": "sha256:b", "artifactType": BUNDLE_MEDIA_TYPE });
        let (body, filtered) = index(vec![a.clone(), b.clone(), a.clone()], None);
        let v: Value = serde_json::from_slice(&body).unwrap();
        assert!(!filtered);
        assert_eq!(v["mediaType"], INDEX_MEDIA_TYPE);
        assert_eq!(
            v["manifests"].as_array().unwrap().len(),
            2,
            "one descriptor per digest: {v}"
        );
        let (body, filtered) = index(vec![a, b], Some(BUNDLE_MEDIA_TYPE));
        let v: Value = serde_json::from_slice(&body).unwrap();
        assert!(filtered);
        assert_eq!(
            v["manifests"],
            json!([{ "digest": "sha256:b", "artifactType": BUNDLE_MEDIA_TYPE }])
        );
        assert_eq!(
            artifact_type_query("n=1&artifactType=application%2Fspdx%2Bjson").as_deref(),
            Some("application/spdx+json")
        );
        assert_eq!(artifact_type_query("artifactType="), None);
    }

    #[test]
    fn a_descriptor_takes_the_artifact_type_else_the_config_media_type() {
        let m = br#"{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","artifactType":"application/vnd.cncf.notary.signature","config":{"mediaType":"application/vnd.oci.empty.v1+json"},"annotations":{"a":"b"}}"#;
        let d = descriptor_of(m);
        assert_eq!(d["artifactType"], "application/vnd.cncf.notary.signature");
        assert_eq!(d["annotations"]["a"], "b");
        assert_eq!(d["digest"], sha256_digest(m));
        assert_eq!(d["size"], m.len());
        let old = br#"{"schemaVersion":2,"config":{"mediaType":"application/vnd.dev.cosign.artifact.sbom.v1+json"}}"#;
        assert_eq!(
            descriptor_of(old)["artifactType"],
            "application/vnd.dev.cosign.artifact.sbom.v1+json"
        );
    }

    #[test]
    fn with_no_evidence_the_images_own_att_is_served_byte_for_byte() {
        let mut e = Evidence::of(
            None,
            &Subject {
                media_type: MANIFEST_MEDIA_TYPE.into(),
                digest: format!("sha256:{HEX}"),
                size: 1,
            },
        );
        assert!(e.referrers.is_empty());
        assert_eq!(e.att_manifest(None), None);
        assert_eq!(e.att_manifest(Some(b"own")), Some(b"own".to_vec()));
    }
}

/// The half that needs a real, signed, filed envelope (the `sign` feature).
#[cfg(all(test, feature = "sign"))]
mod signed_tests {
    use super::*;
    use crate::attest::{
        statement_bytes, AttestPolicy, AttestTrust, CYCLONEDX_BOM, HOLGER_SCAN_EVIDENCE_V1,
    };
    use znippy_common::sign::{dev, dsse_sign, ArchiveSigner, DSSE_PAYLOAD_TYPE_INTOTO};

    /// An image manifest and a store holding Holger's two statements about the layout
    /// AND the image digest (what [`image_subjects`] adds).
    fn filed(dir: &std::path::Path) -> (AttestStore, Subject) {
        let (ca_key, ca_der) = dev::mint_ca("referrers-ca").unwrap();
        let signer = dev::new_p256_signer(&ca_key, &ca_der, "holger-scan").unwrap();
        let certs = vec![signer.certificate_der().to_vec()];
        let ca_path = dir.join("ca.der");
        std::fs::write(&ca_path, &ca_der).unwrap();
        let mut trust = AttestTrust::from_der_root_files(&[ca_path]).unwrap();
        trust.enrol_signer_der(&certs[0]);
        let store = AttestStore::new(dir.join("store"))
            .with_trust(trust)
            .with_policy(AttestPolicy::default());
        let manifest = br#"{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","layers":[]}"#;
        let subject = Subject::of(MANIFEST_MEDIA_TYPE, manifest);
        let layout = ArtifactDigest::of_sha256(b"the layout tar");
        let image = ArtifactDigest::sha256(subject.digest.trim_start_matches("sha256:")).unwrap();
        for pt in [HOLGER_SCAN_EVIDENCE_V1, CYCLONEDX_BOM] {
            let payload = statement_bytes(
                &[
                    ("demo/hello.oci.tar".to_string(), layout.clone()),
                    (format!("demo/hello@{}", subject.digest), image.clone()),
                ],
                pt,
                json!({ "note": pt }),
            );
            let env = dsse_sign(DSSE_PAYLOAD_TYPE_INTOTO, &payload, &[&signer])
                .unwrap()
                .to_json()
                .unwrap();
            store.accept(env.as_bytes(), &certs, Some(&layout)).unwrap();
        }
        (store, subject)
    }

    #[test]
    fn holgers_statements_are_referrers_of_the_image_digest_with_every_object_fetchable() {
        let d = tempfile::tempdir().unwrap();
        let (store, subject) = filed(d.path());
        let e = Evidence::of(Some(&store), &subject);
        assert_eq!(e.referrers.len(), 2, "one referrer per filed envelope");
        let types: std::collections::BTreeSet<&str> = e
            .referrers
            .iter()
            .filter_map(|r| {
                r.pointer("/annotations/dev.sigstore.bundle.predicateType")
                    .and_then(Value::as_str)
            })
            .collect();
        assert_eq!(
            types,
            [HOLGER_SCAN_EVIDENCE_V1, CYCLONEDX_BOM]
                .into_iter()
                .collect()
        );
        for r in &e.referrers {
            assert_eq!(r["artifactType"], BUNDLE_MEDIA_TYPE);
            assert_eq!(r["annotations"][HOLGER_EVIDENCE_ANNOTATION], "holger");
            let (mt, m) = e
                .object(r["digest"].as_str().unwrap())
                .expect("the referrer manifest is fetchable by its digest");
            assert_eq!(mt, MANIFEST_MEDIA_TYPE);
            let mv: Value = serde_json::from_slice(m).unwrap();
            assert_eq!(mv["subject"]["digest"], subject.digest.as_str());
            assert_eq!(mv["subject"]["size"], subject.size);
            assert_eq!(mv["config"]["digest"], sha256_digest(EMPTY_BLOB));
            assert!(e.object(&sha256_digest(EMPTY_BLOB)).is_some());
            let (bmt, b) = e
                .object(mv["layers"][0]["digest"].as_str().unwrap())
                .expect("the bundle blob");
            assert_eq!(bmt, BUNDLE_MEDIA_TYPE);
            let bundle: Value = serde_json::from_slice(b).unwrap();
            assert_eq!(bundle["mediaType"], BUNDLE_MEDIA_TYPE);
            // The envelope inside is a filed one: its payload and signatures unchanged,
            // the keyid moved to the bundle's hint.
            let filed: Vec<Value> = store
                .list(
                    &ArtifactDigest::sha256(subject.digest.trim_start_matches("sha256:")).unwrap(),
                )
                .unwrap()
                .iter()
                .map(|a| serde_json::from_slice(&a.envelope_json).unwrap())
                .collect();
            let inner = &bundle["dsseEnvelope"];
            let same = filed
                .iter()
                .find(|f| f["payload"] == inner["payload"])
                .expect("the bundle carries a filed payload");
            assert_eq!(same["payloadType"], inner["payloadType"]);
            assert_eq!(same["signatures"][0]["sig"], inner["signatures"][0]["sig"]);
            assert!(
                inner["signatures"][0].get("keyid").is_none(),
                "no keyid in the bundle: {inner}"
            );
            assert_eq!(
                bundle["verificationMaterial"]["publicKey"]["hint"],
                same["signatures"][0]["keyid"]
            );
        }
        // Deterministic: the same digests on every call.
        let again = Evidence::of(Some(&store), &subject);
        assert_eq!(again.referrers, e.referrers);
    }

    #[test]
    fn the_att_tag_carries_the_images_own_layers_then_holgers_envelopes_byte_for_byte() {
        let d = tempfile::tempdir().unwrap();
        let (store, subject) = filed(d.path());
        let own_layer = json!({ "mediaType": DSSE_MEDIA_TYPE, "digest": format!("sha256:{}", "a".repeat(64)), "size": 3, "annotations": { "predicateType": "https://slsa.dev/provenance/v1" } });
        let own = json!({ "schemaVersion": 2, "mediaType": MANIFEST_MEDIA_TYPE, "config": {}, "layers": [own_layer.clone()] }).to_string().into_bytes();
        let mut e = Evidence::of(Some(&store), &subject);
        let att = e.att_manifest(Some(&own)).unwrap();
        let v: Value = serde_json::from_slice(&att).unwrap();
        let layers = v["layers"].as_array().unwrap();
        assert_eq!(layers.len(), 3);
        assert_eq!(
            layers[0], own_layer,
            "the image's own attestation comes first, unchanged"
        );
        let filed = store
            .list(&ArtifactDigest::sha256(subject.digest.trim_start_matches("sha256:")).unwrap())
            .unwrap();
        for l in &layers[1..] {
            assert_eq!(l["mediaType"], DSSE_MEDIA_TYPE);
            assert_eq!(l["annotations"][COSIGN_SIGNATURE_ANNOTATION], "");
            let (_, b) = e
                .object(l["digest"].as_str().unwrap())
                .expect("the layer blob");
            assert!(
                filed.iter().any(|a| a.envelope_json == b),
                "a layer is a filed envelope, byte for byte"
            );
        }
        let (mt, m) = e
            .object(&sha256_digest(&att))
            .expect("the merged manifest is fetchable by digest");
        assert_eq!((mt, m), (MANIFEST_MEDIA_TYPE, att.as_slice()));
        assert!(
            e.object(v["config"]["digest"].as_str().unwrap()).is_some(),
            "and its config"
        );
    }

    /// HOL-OCI-EVIDENCE: a client that appends to the `.att` tag (`cosign attest` after the
    /// image is AVAILABLE in a scanned registry) pushes back the merged manifest, Holger's
    /// layers in it. Merged again, each of Holger's envelopes is listed once.
    #[test]
    fn an_att_pushed_back_with_holgers_layers_in_it_lists_them_once() {
        let d = tempfile::tempdir().unwrap();
        let (store, subject) = filed(d.path());
        let mut first = Evidence::of(Some(&store), &subject);
        let merged = first.att_manifest(None).unwrap();
        let n = serde_json::from_slice::<Value>(&merged).unwrap()["layers"]
            .as_array()
            .unwrap()
            .len();
        let mut again = Evidence::of(Some(&store), &subject);
        let twice = again.att_manifest(Some(&merged)).unwrap();
        let layers = serde_json::from_slice::<Value>(&twice).unwrap()["layers"]
            .as_array()
            .unwrap()
            .clone();
        assert_eq!(layers.len(), n, "no envelope is listed twice: {layers:?}");
    }

    #[test]
    fn a_layout_names_its_images_as_subjects() {
        let meta = json!({ "name": "demo/hello", "digest": "sha256:".to_string() + &"1".repeat(64), "images": ["sha256:".to_string() + &"1".repeat(64), "sha256:".to_string() + &"2".repeat(64)], "manifests": {} });
        let mut t = tar::Builder::new(Vec::new());
        let b = serde_json::to_vec(&meta).unwrap();
        let mut h = tar::Header::new_ustar();
        h.set_size(b.len() as u64);
        h.set_mode(0o644);
        t.append_data(&mut h, "holger/media-types.json", b.as_slice())
            .unwrap();
        let tar = t.into_inner().unwrap();
        let s = image_subjects("cgr/demo/hello/sha256-x.oci.tar", &tar);
        assert_eq!(s.len(), 2);
        assert_eq!(s[0].0, format!("demo/hello@sha256:{}", "1".repeat(64)));
        assert!(image_subjects("demo.whl", &tar).is_empty(), "only a layout");
    }
}