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