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

nordisk/holger

server/lib/src/pull_intake/oci.rs

//! **An OCI registry as a remote source** (`kind: oci`): Chainguard's
//! `cgr.dev` first, and any registry that speaks the distribution API v2.
//!
//! An image is pulled WHOLE: the manifest (or image index) the client asked
//! for, every platform manifest the source keeps, each config and layer blob,
//! every digest checked, and with it the image's cosign signature and
//! attestations (the `sha256-<digest>.sig` and `.att` tags) and its OCI 1.1
//! referrers. The lot is ONE artifact on the intake chain: an OCI image layout
//! tar (`oci-layout`, `index.json`, `blobs/sha256/…`), plus
//! `holger/media-types.json` (each manifest's media type) and
//! `holger/provenance/<material>` (what the provenance verifier was handed), so
//! the sealed bytes can be verified again on the far side of an airgap.
//!
//! The registry door then serves the image from the SEALED layout, and only
//! when it is AVAILABLE.
//!
//! **The attached artifacts travel with it** (lane HOL-REFERRERS): the `.sig`,
//! `.att` and `.sbom` tag manifests of every image digest kept, and every OCI 1.1
//! referrer of each (a cosign bundle, a notation signature, an SBOM), each with its
//! config and layer blobs, are kept in the same layout (`holger/media-types.json`
//! lists them under `attached_tags` and `referrers`). They pass the chain as part
//! of the image, never on their own, and the door serves them so a client's
//! `cosign verify` or `notation verify` works through Holger, and through a
//! downstream the layout reaches by an export bundle.

use std::collections::BTreeMap;

use super::http::{Fetch, Reply};
use super::provenance::Material;

/// The manifest media types a pull asks for.
pub const MANIFEST_ACCEPT: &str = "application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, \
application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json";

const INDEX_TYPES: [&str; 2] = [
    "application/vnd.oci.image.index.v1+json",
    "application/vnd.docker.distribution.manifest.list.v2+json",
];

const MAX_MANIFEST: u64 = 4 * 1024 * 1024;

/// `sha256:<64 lower hex>`, the only digest this door takes.
pub fn is_digest(s: &str) -> bool {
    s.strip_prefix("sha256:").is_some_and(|h| {
        h.len() == 64
            && h.bytes()
                .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
    })
}

/// A tag (`latest`, `3.12-dev`) as the distribution spec allows it.
pub fn is_tag(s: &str) -> bool {
    !s.is_empty()
        && s.len() <= 128
        && s.bytes()
            .next()
            .is_some_and(|b| b.is_ascii_alphanumeric() || b == b'_')
        && s.bytes()
            .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b'-'))
}

/// An image name (`chainguard/static`): lower-case path components.
pub fn is_name(s: &str) -> bool {
    !s.is_empty()
        && s.len() <= 255
        && s.split('/').all(|c| {
            !c.is_empty()
                && c.bytes().all(|b| {
                    b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-')
                })
                && c != "."
                && c != ".."
        })
}

fn sha256_digest(b: &[u8]) -> String {
    format!(
        "sha256:{}",
        crate::attest::ArtifactDigest::of_sha256(b).hex()
    )
}

/// What a registry path below `/v2/<repository>/` asks for.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OciPath {
    Manifest {
        name: String,
        reference: String,
    },
    Blob {
        name: String,
        digest: String,
    },
    Tags {
        name: String,
    },
    /// OCI 1.1 `GET <name>/referrers/<digest>[?artifactType=…]`.
    Referrers {
        name: String,
        digest: String,
        artifact_type: Option<String>,
    },
}

/// Parse `<name…>/manifests/<ref>`, `<name…>/blobs/<digest>`, `<name…>/tags/list`,
/// `<name…>/referrers/<digest>`.
pub fn parse_path(rest: &str) -> Option<OciPath> {
    let (rest, query) = rest.split_once('?').unwrap_or((rest, ""));
    let rest = rest.trim_matches('/');
    if let Some(name) = rest.strip_suffix("/tags/list") {
        return is_name(name).then(|| OciPath::Tags { name: name.into() });
    }
    let (name, kind, last) = {
        let (head, last) = rest.rsplit_once('/')?;
        let (name, kind) = head.rsplit_once('/')?;
        (name, kind, last)
    };
    if !is_name(name) {
        return None;
    }
    match kind {
        "manifests" if is_digest(last) || is_tag(last) => Some(OciPath::Manifest {
            name: name.into(),
            reference: last.into(),
        }),
        "blobs" if is_digest(last) => Some(OciPath::Blob {
            name: name.into(),
            digest: last.into(),
        }),
        "referrers" if is_digest(last) => Some(OciPath::Referrers {
            name: name.into(),
            digest: last.into(),
            artifact_type: crate::referrers::artifact_type_query(query),
        }),
        _ => None,
    }
}

/// **A registry client**: the distribution API v2 with the bearer-token dance
/// (anonymous for `cgr.dev`'s public images, basic credentials for a private one).
pub struct Registry<'a> {
    pub http: &'a dyn Fetch,
    /// `https://cgr.dev`.
    pub base: String,
    /// `Basic <b64>` for the token realm and the registry, when configured.
    pub basic: Option<String>,
    token: std::cell::RefCell<Option<String>>,
}

impl<'a> Registry<'a> {
    pub fn new(http: &'a dyn Fetch, base: &str, basic: Option<String>) -> Self {
        Self {
            http,
            base: base.trim_end_matches('/').to_string(),
            basic,
            token: std::cell::RefCell::new(None),
        }
    }

    fn bearer_challenge(r: &Reply) -> Option<(String, Vec<(String, String)>)> {
        let h = r.header("www-authenticate")?;
        let rest = h
            .strip_prefix("Bearer ")
            .or_else(|| h.strip_prefix("bearer "))?;
        let mut realm = None;
        let mut params = Vec::new();
        for part in split_params(rest) {
            let (k, v) = part.split_once('=')?;
            let v = v.trim().trim_matches('"').to_string();
            match k.trim() {
                "realm" => realm = Some(v),
                k => params.push((k.to_string(), v)),
            }
        }
        Some((realm?, params))
    }

    /// GET a path below `/v2/`, answering a token challenge once.
    pub fn get(
        &self,
        path: &str,
        accept: Option<&str>,
        cap: u64,
        scope_name: &str,
    ) -> anyhow::Result<Reply> {
        let url = format!("{}/v2/{path}", self.base);
        let mut headers: Vec<(String, String)> = Vec::new();
        if let Some(a) = accept {
            headers.push(("Accept".into(), a.into()));
        }
        let send = |token: &Option<String>| -> anyhow::Result<Reply> {
            match (token, &self.basic) {
                (Some(t), _) => self.http.get(
                    &url,
                    &headers,
                    Some((&self.base, &format!("Bearer {t}"))),
                    cap,
                ),
                (None, Some(b)) => self.http.get(&url, &headers, Some((&self.base, b)), cap),
                (None, None) => self.http.get(&url, &headers, None, cap),
            }
        };
        let tok = self.token.borrow().clone();
        let r = send(&tok)?;
        if r.status != 401 {
            return Ok(r);
        }
        let (realm, mut params) = Self::bearer_challenge(&r).ok_or_else(|| {
            anyhow::anyhow!("{url}: HTTP 401 without a bearer challenge (credentials refused?)")
        })?;
        if !params.iter().any(|(k, _)| k == "scope") {
            params.push(("scope".into(), format!("repository:{scope_name}:pull")));
        }
        let q: Vec<String> = params
            .iter()
            .map(|(k, v)| format!("{k}={}", url_encode(v)))
            .collect();
        let token_url = format!(
            "{realm}{}{}",
            if realm.contains('?') { "&" } else { "?" },
            q.join("&")
        );
        let tr = self.http.get(
            &token_url,
            &[],
            self.basic.as_deref().map(|b| (realm.as_str(), b)),
            1024 * 1024,
        )?;
        if tr.status != 200 {
            anyhow::bail!("{url}: the token realm {realm} answered HTTP {}", tr.status);
        }
        let v: serde_json::Value = serde_json::from_slice(&tr.body)
            .map_err(|e| anyhow::anyhow!("token from {realm}: {e}"))?;
        let t = v
            .get("token")
            .or_else(|| v.get("access_token"))
            .and_then(|t| t.as_str())
            .ok_or_else(|| anyhow::anyhow!("the token realm {realm} handed no token"))?
            .to_string();
        *self.token.borrow_mut() = Some(t);
        let tok = self.token.borrow().clone();
        send(&tok)
    }

    /// A manifest by tag or digest: its bytes, media type and digest (the
    /// sha256 of the bytes; a digest reference must equal it). `None` on 404.
    pub fn manifest(
        &self,
        name: &str,
        reference: &str,
    ) -> anyhow::Result<Option<(Vec<u8>, String, String)>> {
        let r = self.get(
            &format!("{name}/manifests/{reference}"),
            Some(MANIFEST_ACCEPT),
            MAX_MANIFEST,
            name,
        )?;
        match r.status {
            200 => {}
            404 => return Ok(None),
            s => anyhow::bail!("{}/{name}: manifest {reference}: HTTP {s}", self.base),
        }
        let digest = sha256_digest(&r.body);
        if is_digest(reference) && digest != reference {
            anyhow::bail!(
                "{}/{name}: manifest {reference} hashes to {digest}; not taken",
                self.base
            );
        }
        let mt = media_type_of(&r.body)
            .or_else(|| r.header("content-type").map(str::to_string))
            .unwrap_or_default();
        Ok(Some((r.body, mt, digest)))
    }

    /// A blob, held against its digest.
    pub fn blob(&self, name: &str, digest: &str, cap: u64) -> anyhow::Result<Vec<u8>> {
        let r = self.get(&format!("{name}/blobs/{digest}"), None, cap, name)?;
        if r.status != 200 {
            anyhow::bail!("{}/{name}: blob {digest}: HTTP {}", self.base, r.status);
        }
        let got = sha256_digest(&r.body);
        if got != digest {
            anyhow::bail!(
                "{}/{name}: blob {digest} hashes to {got}; not taken",
                self.base
            );
        }
        Ok(r.body)
    }
}

fn split_params(s: &str) -> Vec<String> {
    let mut out = Vec::new();
    let mut cur = String::new();
    let mut quoted = false;
    for c in s.chars() {
        match c {
            '"' => {
                quoted = !quoted;
                cur.push(c);
            }
            ',' if !quoted => {
                out.push(std::mem::take(&mut cur));
            }
            c => cur.push(c),
        }
    }
    if !cur.trim().is_empty() {
        out.push(cur);
    }
    out
}

fn url_encode(s: &str) -> String {
    let mut o = String::new();
    for b in s.bytes() {
        if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
            o.push(b as char);
        } else {
            o.push_str(&format!("%{b:02X}"));
        }
    }
    o
}

/// The `mediaType` a manifest declares about itself.
pub fn media_type_of(manifest: &[u8]) -> Option<String> {
    serde_json::from_slice::<serde_json::Value>(manifest)
        .ok()?
        .get("mediaType")?
        .as_str()
        .map(str::to_string)
}

/// **One image, pulled whole.**
#[derive(Debug, Clone, Default)]
pub struct Pulled {
    pub name: String,
    /// The tag the client asked for, when it asked by tag.
    pub tag: Option<String>,
    /// The digest of the manifest or index the reference resolves to.
    pub digest: String,
    /// Every manifest (the top one and each platform's), by digest: (media type, bytes).
    pub manifests: BTreeMap<String, (String, Vec<u8>)>,
    /// Every config and layer blob, by digest.
    pub blobs: BTreeMap<String, Vec<u8>>,
    /// The cosign attachments and referrers, for the provenance verifier.
    pub materials: Vec<Material>,
    /// The image digests: the top manifest or index, then each platform manifest kept.
    pub images: Vec<String>,
    /// `sha256-<hex>.sig|.att|.sbom` → the digest of that attached manifest (in `manifests`).
    pub attached_tags: BTreeMap<String, String>,
    /// Subject digest → the descriptors of its referrers (each manifest in `manifests`).
    pub referrers: BTreeMap<String, Vec<serde_json::Value>>,
}

impl Pulled {
    pub fn top(&self) -> &[u8] {
        self.manifests
            .get(&self.digest)
            .map(|(_, b)| b.as_slice())
            .unwrap_or_default()
    }
    pub fn size(&self) -> u64 {
        self.manifests
            .values()
            .map(|(_, b)| b.len() as u64)
            .chain(self.blobs.values().map(|b| b.len() as u64))
            .sum()
    }
}

/// `linux/amd64` of a descriptor's platform, `unknown/unknown` for an attestation manifest.
fn platform_of(d: &serde_json::Value) -> String {
    let p = d.get("platform");
    let os = p
        .and_then(|p| p.get("os"))
        .and_then(|x| x.as_str())
        .unwrap_or("unknown");
    let arch = p
        .and_then(|p| p.get("architecture"))
        .and_then(|x| x.as_str())
        .unwrap_or("unknown");
    match p.and_then(|p| p.get("variant")).and_then(|x| x.as_str()) {
        Some(v) => format!("{os}/{arch}/{v}"),
        None => format!("{os}/{arch}"),
    }
}

fn wants_platform(platforms: &[String], p: &str) -> bool {
    platforms.is_empty()
        || platforms
            .iter()
            .any(|w| p == w || p.starts_with(&format!("{w}/")))
}

/// **Pull `name:reference`** with every platform `platforms` keeps (empty =
/// all), the blobs, and the signatures, attestations and referrers.
pub fn pull(
    reg: &Registry<'_>,
    name: &str,
    reference: &str,
    platforms: &[String],
    cap: u64,
) -> anyhow::Result<Option<Pulled>> {
    let Some((top, top_mt, digest)) = reg.manifest(name, reference)? else {
        return Ok(None);
    };
    let mut p = Pulled {
        name: name.to_string(),
        tag: (!is_digest(reference)).then(|| reference.to_string()),
        digest: digest.clone(),
        ..Default::default()
    };
    let mut images: Vec<(String, Vec<u8>)> = Vec::new();
    if INDEX_TYPES.contains(&top_mt.as_str()) {
        let v: serde_json::Value = serde_json::from_slice(&top)
            .map_err(|e| anyhow::anyhow!("{name}@{digest}: the index is not JSON: {e}"))?;
        let mut kept = 0;
        for d in v
            .get("manifests")
            .and_then(|m| m.as_array())
            .into_iter()
            .flatten()
        {
            let plat = platform_of(d);
            if plat == "unknown/unknown" || !wants_platform(platforms, &plat) {
                continue;
            }
            let Some(md) = d
                .get("digest")
                .and_then(|x| x.as_str())
                .filter(|x| is_digest(x))
            else {
                continue;
            };
            let (bytes, mt, got) = reg.manifest(name, md)?.ok_or_else(|| {
                anyhow::anyhow!(
                    "{name}: the index names {md} ({plat}), which the registry does not have"
                )
            })?;
            debug_assert_eq!(got, md);
            p.manifests.insert(md.to_string(), (mt, bytes.clone()));
            images.push((md.to_string(), bytes));
            kept += 1;
        }
        if kept == 0 {
            anyhow::bail!(
                "{name}@{digest}: the index carries none of the platforms this source keeps ({})",
                platforms.join(", ")
            );
        }
    } else {
        images.push((digest.clone(), top.clone()));
    }
    p.manifests.insert(digest.clone(), (top_mt, top));
    p.images = std::iter::once(digest.clone())
        .chain(
            images
                .iter()
                .map(|(d, _)| d.clone())
                .filter(|d| *d != digest),
        )
        .collect();
    for (md, bytes) in images {
        let v: serde_json::Value = serde_json::from_slice(&bytes)
            .map_err(|e| anyhow::anyhow!("{name}@{md}: not JSON: {e}"))?;
        let config = v.pointer("/config/digest").and_then(|x| x.as_str());
        let layers = v
            .get("layers")
            .and_then(|l| l.as_array())
            .into_iter()
            .flatten()
            .filter_map(|l| l.get("digest").and_then(|x| x.as_str()));
        for b in config.into_iter().chain(layers) {
            if !is_digest(b) {
                anyhow::bail!("{name}@{md}: '{b}' is not a sha256 digest");
            }
            if p.blobs.contains_key(b) {
                continue;
            }
            let blob = reg.blob(name, b, cap)?;
            p.blobs.insert(b.to_string(), blob);
            if p.size() > cap {
                anyhow::bail!(
                    "{name}@{digest}: the image is larger than this source's {cap}-byte cap"
                );
            }
        }
    }
    attached(reg, name, cap, &mut p)?;
    p.materials = materials_of(&reg.base, name, &p);
    Ok(Some(p))
}

/// Keep one attached manifest and its config and layer blobs in `p`.
fn keep_attached(
    reg: &Registry<'_>,
    name: &str,
    cap: u64,
    p: &mut Pulled,
    digest: &str,
    media_type: String,
    bytes: Vec<u8>,
) -> anyhow::Result<()> {
    let v: serde_json::Value = serde_json::from_slice(&bytes)
        .map_err(|e| anyhow::anyhow!("{name}@{digest}: an attached manifest is not JSON: {e}"))?;
    let config = v.pointer("/config/digest").and_then(|x| x.as_str());
    let layers = v
        .get("layers")
        .and_then(|l| l.as_array())
        .into_iter()
        .flatten()
        .filter_map(|l| l.get("digest").and_then(|x| x.as_str()));
    for b in config.into_iter().chain(layers) {
        if !is_digest(b) || p.blobs.contains_key(b) {
            continue;
        }
        let blob = reg.blob(name, b, cap)?;
        p.blobs.insert(b.to_string(), blob);
    }
    p.manifests.insert(digest.to_string(), (media_type, bytes));
    if p.size() > cap {
        anyhow::bail!("{name}: the image with its attached artifacts is larger than this source's {cap}-byte cap");
    }
    Ok(())
}

/// **The attached artifacts of every image digest**: the cosign tag scheme
/// (`sha256-<hex>.sig`, `.att`, `.sbom`) and the OCI 1.1 referrers, by the API or,
/// where the source has none, by the referrers tag schema (`sha256-<hex>`, an index).
fn attached(reg: &Registry<'_>, name: &str, cap: u64, p: &mut Pulled) -> anyhow::Result<()> {
    for subject in p.images.clone() {
        let hex = subject.trim_start_matches("sha256:").to_string();
        for suffix in ["sig", "att", "sbom"] {
            let tag = format!("sha256-{hex}.{suffix}");
            let Some((m, mt, d)) = reg.manifest(name, &tag)? else {
                continue;
            };
            keep_attached(reg, name, cap, p, &d, mt, m)?;
            p.attached_tags.insert(tag, d);
        }
        let r = reg.get(
            &format!("{name}/referrers/{subject}"),
            Some(crate::referrers::INDEX_MEDIA_TYPE),
            MAX_MANIFEST,
            name,
        )?;
        let listed: Vec<serde_json::Value> = if r.status == 200 {
            serde_json::from_slice::<serde_json::Value>(&r.body)
                .ok()
                .and_then(|v| v.get("manifests").and_then(|m| m.as_array()).cloned())
                .unwrap_or_default()
        } else {
            match reg.manifest(name, &format!("sha256-{hex}"))? {
                Some((m, _, _)) => serde_json::from_slice::<serde_json::Value>(&m)
                    .ok()
                    .and_then(|v| v.get("manifests").and_then(|m| m.as_array()).cloned())
                    .unwrap_or_default(),
                None => Vec::new(),
            }
        };
        let mut kept = Vec::new();
        for desc in listed {
            let Some(d) = desc
                .get("digest")
                .and_then(|x| x.as_str())
                .filter(|x| is_digest(x))
                .map(str::to_string)
            else {
                continue;
            };
            let Some((m, mt, _)) = reg.manifest(name, &d)? else {
                continue;
            };
            keep_attached(reg, name, cap, p, &d, mt, m.clone())?;
            // The descriptor as Holger reads it from the manifest itself, so it is
            // true of the bytes kept whatever the source listed.
            kept.push(crate::referrers::descriptor_of(&m));
        }
        if !kept.is_empty() {
            p.referrers.insert(subject.clone(), kept);
        }
    }
    Ok(())
}

/// **The provenance material of a pulled image**: one
/// `holger.cosign-attachments/v1` document with every `.sig` and `.att` layer of
/// the top digest (its annotations and its bytes), the shape `holger-provenance`
/// reads, and each referrer's sigstore bundle, read from what [`attached`] kept.
pub fn materials_of(base: &str, name: &str, p: &Pulled) -> Vec<Material> {
    use base64::Engine as _;
    let digest = p.digest.as_str();
    let hex = digest.trim_start_matches("sha256:");
    let mut doc = serde_json::json!({
        "schema": "holger.cosign-attachments/v1",
        "image": format!("{}/{name}", base.trim_start_matches("https://").trim_start_matches("http://")),
        "digest": digest,
        "signatures": [],
        "attestations": [],
    });
    let manifest_of = |tag: &str| -> Option<serde_json::Value> {
        let d = p.attached_tags.get(tag)?;
        serde_json::from_slice(&p.manifests.get(d)?.1).ok()
    };
    let mut any = false;
    for (suffix, key) in [("sig", "signatures"), ("att", "attestations")] {
        let Some(v) = manifest_of(&format!("sha256-{hex}.{suffix}")) else {
            continue;
        };
        for l in v
            .get("layers")
            .and_then(|l| l.as_array())
            .into_iter()
            .flatten()
        {
            let Some(d) = l
                .get("digest")
                .and_then(|x| x.as_str())
                .filter(|d| is_digest(d))
            else {
                continue;
            };
            let Some(blob) = p.blobs.get(d) else { continue };
            let layer = serde_json::json!({
                "mediaType": l.get("mediaType").cloned().unwrap_or_default(),
                "digest": d,
                "annotations": l.get("annotations").cloned().unwrap_or(serde_json::json!({})),
                "blob": base64::engine::general_purpose::STANDARD.encode(blob),
            });
            if let Some(arr) = doc.get_mut(key).and_then(|a| a.as_array_mut()) {
                arr.push(layer);
                any = true;
            }
        }
    }
    let mut out = Vec::new();
    if any {
        out.push(Material {
            name: "cosign.json".into(),
            bytes: serde_json::to_vec_pretty(&doc).unwrap_or_default(),
        });
    }
    // OCI 1.1 referrers: each artifact manifest whose layer is a sigstore bundle.
    for d in p.referrers.get(digest).into_iter().flatten() {
        let Some(md) = d.get("digest").and_then(|x| x.as_str()) else {
            continue;
        };
        let Some((_, m)) = p.manifests.get(md) else {
            continue;
        };
        let mv: serde_json::Value = serde_json::from_slice(m).unwrap_or_default();
        for l in mv
            .get("layers")
            .and_then(|l| l.as_array())
            .into_iter()
            .flatten()
        {
            let mt = l.get("mediaType").and_then(|x| x.as_str()).unwrap_or("");
            let Some(ld) = l
                .get("digest")
                .and_then(|x| x.as_str())
                .filter(|d| is_digest(d))
            else {
                continue;
            };
            if mt.contains("sigstore.bundle") || mt.contains("in-toto") || mt.contains("dsse") {
                if let Some(blob) = p.blobs.get(ld) {
                    out.push(Material {
                        name: format!("referrer-{}.sigstore.json", &ld[7..19]),
                        bytes: blob.clone(),
                    });
                }
            }
        }
    }
    out
}

// ── the OCI image layout the chain carries ──────────────────────────────────

/// **The image as one artifact**: an OCI image layout tar, deterministic
/// (sorted entries, mtime 0), with `holger/media-types.json` and the
/// provenance material.
pub fn layout_tar(p: &Pulled) -> anyhow::Result<Vec<u8>> {
    let mut files: BTreeMap<String, Vec<u8>> = BTreeMap::new();
    files.insert(
        "oci-layout".into(),
        br#"{"imageLayoutVersion":"1.0.0"}"#.to_vec(),
    );
    let (top_mt, top) = p.manifests.get(&p.digest).cloned().unwrap_or_default();
    let mut desc =
        serde_json::json!({ "mediaType": top_mt, "digest": p.digest, "size": top.len() });
    if let Some(t) = &p.tag {
        desc["annotations"] = serde_json::json!({ "org.opencontainers.image.ref.name": t });
    }
    let index = serde_json::json!({ "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests": [desc] });
    files.insert("index.json".into(), serde_json::to_vec(&index)?);
    let mut types = BTreeMap::new();
    for (d, (mt, b)) in &p.manifests {
        files.insert(format!("blobs/sha256/{}", &d[7..]), b.clone());
        types.insert(d.clone(), mt.clone());
    }
    for (d, b) in &p.blobs {
        files.insert(format!("blobs/sha256/{}", &d[7..]), b.clone());
    }
    let images: Vec<String> = if p.images.is_empty() {
        vec![p.digest.clone()]
    } else {
        p.images.clone()
    };
    let meta = serde_json::json!({
        "name": p.name, "tag": p.tag, "digest": p.digest, "manifests": types,
        "images": images, "attached_tags": p.attached_tags, "referrers": p.referrers,
    });
    files.insert(
        "holger/media-types.json".into(),
        serde_json::to_vec_pretty(&meta)?,
    );
    for m in &p.materials {
        files.insert(format!("holger/provenance/{}", m.name), m.bytes.clone());
    }
    let mut out = tar::Builder::new(Vec::new());
    for (path, bytes) in &files {
        let mut h = tar::Header::new_ustar();
        h.set_size(bytes.len() as u64);
        h.set_mode(0o644);
        h.set_mtime(0);
        h.set_uid(0);
        h.set_gid(0);
        h.set_entry_type(tar::EntryType::Regular);
        out.append_data(&mut h, path, bytes.as_slice())?;
    }
    Ok(out.into_inner()?)
}

/// A sealed layout, read back for serving.
#[derive(Debug, Clone, Default)]
pub struct Layout {
    pub name: String,
    pub tag: Option<String>,
    pub digest: String,
    /// Manifest digest → media type.
    pub manifests: BTreeMap<String, String>,
    blobs: BTreeMap<String, Vec<u8>>,
    pub materials: Vec<Material>,
    /// The image digests (top first); a layout from before HOL-REFERRERS: its manifests.
    pub images: Vec<String>,
    /// `sha256-<hex>.sig|.att|.sbom` → the attached manifest's digest.
    pub attached_tags: BTreeMap<String, String>,
    /// Subject digest → its referrers' descriptors.
    pub referrers: BTreeMap<String, Vec<serde_json::Value>>,
}

impl Layout {
    /// Read a layout tar [`layout_tar`] wrote. Every blob is held against its name.
    pub fn read(bytes: &[u8]) -> anyhow::Result<Self> {
        use std::io::Read as _;
        let mut ar = tar::Archive::new(bytes);
        let mut out = Layout::default();
        let mut meta: Option<serde_json::Value> = None;
        for e in ar.entries()? {
            let mut e = e?;
            let path = e.path()?.to_string_lossy().to_string();
            let mut b = Vec::new();
            e.read_to_end(&mut b)?;
            if let Some(hex) = path.strip_prefix("blobs/sha256/") {
                let d = format!("sha256:{hex}");
                if sha256_digest(&b) != d {
                    anyhow::bail!("the sealed layout's blob {d} does not hash to its name");
                }
                out.blobs.insert(d, b);
            } else if path == "holger/media-types.json" {
                meta = Some(serde_json::from_slice(&b)?);
            } else if let Some(n) = path.strip_prefix("holger/provenance/") {
                out.materials.push(Material {
                    name: n.to_string(),
                    bytes: b,
                });
            }
        }
        let meta =
            meta.ok_or_else(|| anyhow::anyhow!("the layout carries no holger/media-types.json"))?;
        out.name = meta
            .get("name")
            .and_then(|x| x.as_str())
            .unwrap_or("")
            .to_string();
        out.tag = meta.get("tag").and_then(|x| x.as_str()).map(str::to_string);
        out.digest = meta
            .get("digest")
            .and_then(|x| x.as_str())
            .unwrap_or("")
            .to_string();
        if let Some(m) = meta.get("manifests").and_then(|m| m.as_object()) {
            for (d, t) in m {
                out.manifests
                    .insert(d.clone(), t.as_str().unwrap_or("").to_string());
            }
        }
        out.images = match meta.get("images").and_then(|i| i.as_array()) {
            Some(a) => a
                .iter()
                .filter_map(|x| x.as_str())
                .map(str::to_string)
                .collect(),
            None => out.manifests.keys().cloned().collect(),
        };
        if let Some(t) = meta.get("attached_tags").and_then(|t| t.as_object()) {
            for (tag, d) in t {
                if let Some(d) = d.as_str() {
                    out.attached_tags.insert(tag.clone(), d.to_string());
                }
            }
        }
        if let Some(r) = meta.get("referrers").and_then(|r| r.as_object()) {
            for (subject, list) in r {
                out.referrers.insert(
                    subject.clone(),
                    list.as_array().cloned().unwrap_or_default(),
                );
            }
        }
        Ok(out)
    }

    /// A manifest's bytes and media type.
    pub fn manifest(&self, digest: &str) -> Option<(&str, &[u8])> {
        let mt = self.manifests.get(digest)?;
        Some((mt.as_str(), self.blobs.get(digest)?.as_slice()))
    }

    /// Any blob (manifest, config, layer).
    pub fn blob(&self, digest: &str) -> Option<&[u8]> {
        self.blobs.get(digest).map(|b| b.as_slice())
    }

    /// Every digest the layout holds.
    pub fn digests(&self) -> impl Iterator<Item = &String> {
        self.blobs.keys()
    }
}

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

    #[test]
    fn registry_paths_parse_and_traversal_does_not() {
        assert_eq!(
            parse_path("cgr/chainguard/static/manifests/latest"),
            Some(OciPath::Manifest {
                name: "cgr/chainguard/static".into(),
                reference: "latest".into()
            })
        );
        let d = format!("sha256:{}", "a".repeat(64));
        assert_eq!(
            parse_path(&format!("cgr/x/blobs/{d}")),
            Some(OciPath::Blob {
                name: "cgr/x".into(),
                digest: d
            })
        );
        assert_eq!(
            parse_path("cgr/x/tags/list"),
            Some(OciPath::Tags {
                name: "cgr/x".into()
            })
        );
        assert_eq!(parse_path("cgr/../x/manifests/latest"), None);
        assert_eq!(parse_path("cgr/x/blobs/sha256:zz"), None);
        assert_eq!(parse_path("cgr/X/manifests/latest"), None);
    }

    #[test]
    fn a_layout_reads_back_what_it_carries() {
        let m = br#"{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","layers":[]}"#.to_vec();
        let d = sha256_digest(&m);
        let layer = b"layer".to_vec();
        let ld = sha256_digest(&layer);
        let mut p = Pulled {
            name: "x/y".into(),
            tag: Some("latest".into()),
            digest: d.clone(),
            ..Default::default()
        };
        p.manifests.insert(
            d.clone(),
            (
                "application/vnd.oci.image.manifest.v1+json".into(),
                m.clone(),
            ),
        );
        p.blobs.insert(ld.clone(), layer.clone());
        p.materials.push(Material {
            name: "cosign.json".into(),
            bytes: b"{}".to_vec(),
        });
        let tar = layout_tar(&p).unwrap();
        assert_eq!(tar, layout_tar(&p).unwrap(), "deterministic");
        let l = Layout::read(&tar).unwrap();
        assert_eq!(l.digest, d);
        assert_eq!(l.manifest(&d).unwrap().1, m.as_slice());
        assert_eq!(l.blob(&ld).unwrap(), layer.as_slice());
        assert_eq!(l.materials.len(), 1);
        assert_eq!(l.tag.as_deref(), Some("latest"));
    }
}