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

nordisk/holger

server/lib/tests/pull_through_apk.rs

//! **APK pull-through** (lane T8, `server/lib/src/pull_intake/apk.rs`): a Wolfi
//! (or Chainguard, or Alpine) repository as a `kind: apk` remote source.
//!
//! One real Holger in front of a local FAKE upstream that serves REAL Wolfi bytes
//! (`provenance/tests/fixtures/wolfi/`: the `bootstrap/stage3` APKINDEX, signed
//! `.SIGN.RSA.wolfi-signing.rsa.pub`, and `libffi-3.4.2-r0.apk` from it).
//!
//! What the tests measure: `GET <source>/<arch>/-/pull/<name>` is the client's "not
//! yet" (`503` + `Retry-After`); the upstream index is held against the PINNED key
//! before a package is read from it, and every package against the signed index
//! (control hash, size, data hash) before it reaches the chain; the package lands
//! INGESTED as `pull-through:<source>`, its provenance verified again at SCANNED, and
//! waits at POLICY_CHECKED for a person; Holger's own `APKINDEX.tar.gz`, signed with
//! Holger's key, lists only AVAILABLE packages; after the approval the `.apk` comes
//! from the SEALED store.
//!
//! `real_client_apk_add_through_holger` is the measurement with the REAL client: the
//! same Holger in front of the REAL Wolfi repository (`https://packages.wolfi.dev/os`,
//! its key pinned from the fixture), and `apk add` in `cgr.dev/chainguard/wolfi-base`
//! whose only repository is Holger and whose only key is Holger's. It is `#[ignore]`d
//! for a plain `cargo test` (it needs podman, the image and the network) and reached by
//! the demo's row (`cargo xtask demo tests`). The client reaches Holger's loopback port
//! through its own pasta network (`-T <port>`), never `--network host`. No podman FAILS
//! by name; it never skips.

use std::collections::BTreeMap;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use holger_provenance::apk::{verify_index, ApkKey};
use holger_server_lib::promote::intake::{
    ArtifactState, EvidenceKind, EvidenceVerdict, IntakeChain, TransitionRequest,
};
use holger_server_lib::pull_intake::apk;
use holger_server_lib::{read_ron_config, wire_holger, Holger};
use traits::test_support::hold_port;

const FX: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../../provenance/tests/fixtures"
);
const WOLFI_KEY: &str = "wolfi-signing.rsa.pub";
const LIBFFI: &str = "libffi-3.4.2-r0.apk";

fn fixture(p: &str) -> Vec<u8> {
    let b = std::fs::read(Path::new(FX).join(p)).unwrap_or_else(|e| panic!("{p}: {e}"));
    assert!(
        !b.starts_with(b"version https://git-lfs"),
        "{p} is an LFS pointer: run `git lfs pull`"
    );
    b
}

fn token(who: &str) -> String {
    format!("{who}-token-of-sufficient-length-for-holger")
}

// ── the fake upstream ───────────────────────────────────────────────────────

struct Upstream {
    base: String,
    hits: Arc<Mutex<BTreeMap<String, usize>>>,
    /// Paths the fake answers late: it waits this long before it counts the hit and replies.
    slow: Arc<Mutex<BTreeMap<String, Duration>>>,
}

impl Upstream {
    fn hits(&self, path: &str) -> usize {
        self.hits.lock().unwrap().get(path).copied().unwrap_or(0)
    }

    /// Answer `path` only after `by`, the way a loaded box gets to it later: the request
    /// is counted when the wait is over.
    fn slow_down(&self, path: &str, by: Duration) {
        self.slow.lock().unwrap().insert(path.to_string(), by);
    }
}

struct Reply {
    status: u16,
    ctype: String,
    body: Vec<u8>,
}

fn reply(status: u16, ctype: &str, body: Vec<u8>) -> Reply {
    Reply {
        status,
        ctype: ctype.into(),
        body,
    }
}

fn flipped(mut b: Vec<u8>, from_end: usize) -> Vec<u8> {
    let n = b.len() - from_end;
    b[n] ^= 0x01;
    b
}

/// `/<repo>/x86_64/…`: `wolfi` as published, `tampered` with a flipped byte in the
/// package's data, `forged` with a flipped byte in the signed index.
fn route(_base: &str, path: &str, _auth: Option<&str>) -> Reply {
    let index = fixture("wolfi/stage3/APKINDEX.tar.gz");
    let apk = fixture(&format!("wolfi/stage3/{LIBFFI}"));
    match path {
        "/wolfi/x86_64/APKINDEX.tar.gz"
        | "/tampered/x86_64/APKINDEX.tar.gz"
        | "/nokey/x86_64/APKINDEX.tar.gz" => reply(200, "application/gzip", index),
        "/forged/x86_64/APKINDEX.tar.gz" => reply(200, "application/gzip", flipped(index, 100)),
        p if p == format!("/wolfi/x86_64/{LIBFFI}")
            || p == format!("/forged/x86_64/{LIBFFI}")
            || p == format!("/nokey/x86_64/{LIBFFI}") =>
        {
            reply(200, "application/octet-stream", apk)
        }
        p if p == format!("/tampered/x86_64/{LIBFFI}") => {
            reply(200, "application/octet-stream", flipped(apk, 20))
        }
        _ => reply(404, "text/plain", b"not found".to_vec()),
    }
}

fn serve_one(
    mut s: TcpStream,
    base: &str,
    hits: &Mutex<BTreeMap<String, usize>>,
    slow: &Mutex<BTreeMap<String, Duration>>,
) {
    let _ = s.set_read_timeout(Some(Duration::from_secs(10)));
    let mut buf = Vec::new();
    let mut chunk = [0u8; 4096];
    while !buf.windows(4).any(|w| w == b"\r\n\r\n") {
        match s.read(&mut chunk) {
            Ok(0) | Err(_) => return,
            Ok(n) => buf.extend_from_slice(&chunk[..n]),
        }
    }
    let head = String::from_utf8_lossy(&buf).to_string();
    let mut lines = head.lines();
    let path = lines
        .next()
        .unwrap_or("")
        .split_whitespace()
        .nth(1)
        .unwrap_or("/")
        .to_string();
    let auth = lines
        .find_map(|l| {
            l.strip_prefix("authorization: ")
                .or_else(|| l.strip_prefix("Authorization: "))
        })
        .map(str::to_string);
    let bare = path.split('?').next().unwrap_or("").to_string();
    let late = slow.lock().unwrap().get(&bare).copied();
    if let Some(by) = late {
        std::thread::sleep(by);
    }
    *hits.lock().unwrap().entry(bare.clone()).or_default() += 1;
    let r = route(base, &bare, auth.as_deref());
    let out = format!(
        "HTTP/1.1 {} X\r\nContent-Length: {}\r\nContent-Type: {}\r\nConnection: close\r\n\r\n",
        r.status,
        r.body.len(),
        r.ctype
    );
    let _ = s.write_all(out.as_bytes());
    let _ = s.write_all(&r.body);
}

fn upstream() -> Upstream {
    let l = TcpListener::bind("127.0.0.1:0").unwrap();
    let base = format!("http://{}", l.local_addr().unwrap());
    let hits: Arc<Mutex<BTreeMap<String, usize>>> = Arc::default();
    let slow: Arc<Mutex<BTreeMap<String, Duration>>> = Arc::default();
    let (b2, h2, s2) = (base.clone(), Arc::clone(&hits), Arc::clone(&slow));
    std::thread::spawn(move || {
        for s in l.incoming().flatten() {
            let (b3, h3, s3) = (b2.clone(), Arc::clone(&h2), Arc::clone(&s2));
            std::thread::spawn(move || serve_one(s, &b3, &h3, &s3));
        }
    });
    Upstream { base, hits, slow }
}

// ── one Holger ──────────────────────────────────────────────────────────────

struct Running {
    holger: Holger,
    _rt: tokio::runtime::Runtime,
    _data: tempfile::TempDir,
    _cfg: tempfile::NamedTempFile,
    http: String,
}

impl Running {
    fn chain(&self) -> Arc<IntakeChain> {
        self.holger.intake().expect("the intake chain")
    }
}

/// `rules.apk_keys`: Wolfi's key, pinned.
fn wolfi_pinned() -> String {
    format!(r#"(name: "{WOLFI_KEY}", pem_file: Some("{FX}/wolfi/wolfi-signing.rsa.pub")),"#)
}

fn start(sources: &str) -> Running {
    start_with(sources, &wolfi_pinned())
}

fn start_with(sources: &str, keys: &str) -> Running {
    let mut last = String::new();
    for _ in 0..8 {
        let (grpc, http) = (hold_port(), hold_port());
        match try_start(grpc, http, sources, keys) {
            Ok(r) => return r,
            Err(e) => last = e,
        }
    }
    panic!("server never started; last error: {last}");
}

/// The four intake repositories, the chain, and `pull_through:` with the apk source.
fn try_start(grpc: u16, http: u16, sources: &str, keys: &str) -> Result<Running, String> {
    let data = tempfile::tempdir().unwrap();
    let d = data.path().display().to_string();
    let repo = |name: &str| {
        format!(
            r#"(ron_name: "{name}", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
         ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")),
         ron_data_dir: Some("{d}/{name}")),"#
        )
    };
    let methods: Vec<String> = ["ida", "bob"]
        .iter()
        .map(|p| {
            format!(
                r#"StaticToken(subject: "{p}", token: Some("{}"))"#,
                token(p)
            )
        })
        .collect();
    let ron = format!(
        r#"(
    repositories: [ {incoming} {quarantine} {approved} {production} ],
    exposed_endpoints: [ (ron_name: "door", ron_url: "127.0.0.1:{grpc}", ron_http_url: Some("127.0.0.1:{http}")) ],
    storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{d}") ],
    audit: (dir: "{d}/audit", enabled: true),
    // Open reads by declaration: an estate build reads Gated by default.
    access: (reads: Open, front: Open),
    auth: Some((
        methods: [ {methods} ],
        duties: {{ "ida": [Importer], "bob": [SecurityApprover] }},
    )),
    promotion: Some((
        ladder_name: "intake",
        stages: [
            (name: "incoming", repo: "incoming"), (name: "quarantine", repo: "quarantine"),
            (name: "approved", repo: "approved"), (name: "production", repo: "production"),
        ],
        intake: Some((
            incoming: "incoming", quarantine: "quarantine", approved: "approved", production: "production",
            rules: (sandbox: off),
            approvals: 1,
            scan: None,
        )),
    )),
    pull_through: Some((
        sources: [ {sources} ],
        rules: (require_provenance: true, apk_keys: [ {keys} ]),
        retry_after_secs: Some(1),
    )),
)"#,
        methods = methods.join(", "),
        incoming = repo("incoming"),
        quarantine = repo("quarantine"),
        approved = repo("approved"),
        production = repo("production"),
    );
    let mut cfg = tempfile::NamedTempFile::new().unwrap();
    cfg.write_all(ron.as_bytes()).unwrap();
    let mut holger: Holger = read_ron_config(cfg.path()).map_err(|e| format!("config: {e}"))?;
    holger
        .instantiate_backends()
        .map_err(|e| format!("backends: {e}"))?;
    wire_holger(&mut holger).map_err(|e| format!("wire: {e:#}"))?;
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(4)
        .enable_all()
        .build()
        .unwrap();
    {
        let _in = rt.enter();
        holger.start().map_err(|e| format!("start: {e:#}"))?;
    }
    let r = Running {
        holger,
        _rt: rt,
        _data: data,
        _cfg: cfg,
        http: format!("127.0.0.1:{http}"),
    };
    for _ in 0..200 {
        if get(&r.http, "/healthz", &[]).status == 200 {
            return Ok(r);
        }
        std::thread::sleep(Duration::from_millis(20));
    }
    Err("never healthy".into())
}

struct Got {
    status: u16,
    headers: BTreeMap<String, String>,
    body: Vec<u8>,
}

impl Got {
    fn text(&self) -> String {
        String::from_utf8_lossy(&self.body).to_string()
    }
}

fn get(addr: &str, path: &str, headers: &[(&str, &str)]) -> Got {
    let Ok(mut s) = TcpStream::connect(addr) else {
        return Got {
            status: 0,
            headers: BTreeMap::new(),
            body: Vec::new(),
        };
    };
    let mut req = format!("GET {path} HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n");
    for (k, v) in headers {
        req.push_str(&format!("{k}: {v}\r\n"));
    }
    req.push_str("\r\n");
    s.write_all(req.as_bytes()).unwrap();
    let mut all = Vec::new();
    let _ = s.read_to_end(&mut all);
    let split = all
        .windows(4)
        .position(|w| w == b"\r\n\r\n")
        .map(|i| i + 4)
        .unwrap_or(all.len());
    let head = String::from_utf8_lossy(&all[..split]).to_string();
    let status = head
        .split_whitespace()
        .nth(1)
        .and_then(|s| s.parse().ok())
        .unwrap_or(0);
    let headers = head
        .lines()
        .skip(1)
        .filter_map(|l| {
            l.split_once(':')
                .map(|(k, v)| (k.trim().to_ascii_lowercase(), v.trim().to_string()))
        })
        .collect();
    Got {
        status,
        headers,
        body: all[split..].to_vec(),
    }
}

fn wait_state(chain: &IntakeChain, id: &traits::ArtifactId, want: ArtifactState) {
    let t = Instant::now();
    while t.elapsed() < Duration::from_secs(60) {
        if chain.state(id) == want {
            return;
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    let jobs = chain.pull().map(|p| p.debug_records()).unwrap_or_default();
    panic!(
        "{} never reached {want}; it stands at {} ({:#?}); pull records: {jobs}",
        id.name,
        chain.state(id),
        chain.history(id)
    );
}

/// Wait (bounded, as [`wait_state`]) until the fake upstream has seen `path` at least `n` times.
fn wait_hits(up: &Upstream, path: &str, n: usize, what: &str) {
    let t = Instant::now();
    while t.elapsed() < Duration::from_secs(60) {
        if up.hits(path) >= n {
            return;
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    panic!(
        "{what}: the upstream saw {path} {} time(s) in 60 s, wanted {n}",
        up.hits(path)
    );
}

/// A GET once the pull job is done: while the job still runs, the answer is 503.
fn get_settled(addr: &str, path: &str) -> Got {
    let t = Instant::now();
    loop {
        let g = get(addr, path, &[]);
        if g.status != 503 || t.elapsed() > Duration::from_secs(60) {
            return g;
        }
        std::thread::sleep(Duration::from_millis(50));
    }
}

fn approve(chain: &IntakeChain, id: &traits::ArtifactId) {
    chain
        .advance(
            TransitionRequest::new(id.clone(), ArtifactState::Approved, "bob")
                .with_reason("reviewed"),
        )
        .unwrap_or_else(|e| panic!("bob approves {}: {e}", id.name));
}

/// The trust the tests pin: Wolfi's signing key.
fn wolfi_pem() -> (String, Vec<u8>) {
    (
        WOLFI_KEY.to_string(),
        fixture("wolfi/wolfi-signing.rsa.pub"),
    )
}

fn source(up: &Upstream, name: &str) -> String {
    format!(
        r#"(name: "{name}", kind: apk, url: "{base}/{name}", gates: (provenance: Some(require(apk_keys: ["{WOLFI_KEY}"])))),"#,
        base = up.base
    )
}

/// Holger's signing key for a source, as a client fetches it into `/etc/apk/keys`.
fn holger_key(r: &Running, source: &str) -> ApkKey {
    let name = get(
        &r.http,
        &format!("/production/{source}/-/apk-key-name"),
        &[],
    );
    assert_eq!(name.status, 200, "{}", name.text());
    let name = name.text().trim().to_string();
    assert!(
        name.starts_with("holger-") && name.ends_with(".rsa.pub"),
        "{name}"
    );
    let pem = get(&r.http, &format!("/production/{source}/{name}"), &[]);
    assert_eq!(pem.status, 200, "{}", pem.text());
    ApkKey::from_pem(&name, &pem.body).expect("an RSA public key in PEM")
}

/// Holger's index for `source`, verified with Holger's key: the (name, version)s it lists.
fn listed(r: &Running, source: &str) -> Vec<(String, String)> {
    let key = holger_key(r, source);
    let g = get(
        &r.http,
        &format!("/production/{source}/x86_64/APKINDEX.tar.gz"),
        &[],
    );
    assert_eq!(g.status, 200, "{}", g.text());
    let idx = verify_index(&g.body, &[key]).expect("Holger's index verifies with Holger's key");
    assert_eq!(idx.alg, "rsa-sha256");
    idx.entries
        .into_iter()
        .map(|e| (e.name, e.version))
        .collect()
}

#[test]
fn an_apk_is_pulled_verified_held_and_listed_in_holgers_signed_index_only_once_available() {
    let up = upstream();
    // The dependency-closure walk runs on its own, after libffi's own intake, and under load
    // it reaches the upstream later (ODEN-CI round 3: 1 in 10 red on oden). The fake answers
    // glibc late on purpose, so the test sees that ordering on every run, not by luck.
    up.slow_down(
        "/wolfi/x86_64/glibc-2.36-r0.apk",
        Duration::from_millis(1500),
    );
    let r = start(&source(&up, "wolfi"));
    let chain = r.chain();

    // 0. Holger's index lists nothing yet, and is signed by Holger.
    assert_eq!(listed(&r, "wolfi"), []);

    // 1. The client asks for libffi: not yet; a pull is on its way.
    let first = get(&r.http, "/production/wolfi/x86_64/-/pull/libffi", &[]);
    assert_eq!(first.status, 503, "{}", first.text());
    assert!(
        first.headers.contains_key("retry-after"),
        "{:?}",
        first.headers
    );

    // 2. libffi lands INGESTED as pull-through:wolfi and waits for a person, its
    //    provenance (the signed index) verified at SCANNED.
    let id = apk::id("wolfi", "x86_64", "libffi", "3.4.2-r0");
    assert_eq!(id.name, format!("wolfi/x86_64/{LIBFFI}"));
    wait_state(&chain, &id, ArtifactState::PolicyChecked);
    let h = chain.history(&id);
    assert!(h.iter().all(|e| e.actor == "pull-through:wolfi"), "{h:#?}");
    let prov = h[1]
        .evidence
        .iter()
        .find(|e| e.kind == EvidenceKind::Provenance)
        .cloned()
        .expect("a provenance slot");
    assert_eq!(
        (prov.source.as_str(), prov.verdict),
        ("holger-provenance", EvidenceVerdict::Pass),
        "{prov:?}"
    );
    assert_eq!(
        h[0].sha256,
        hex::encode(<sha2::Sha256 as sha2::Digest>::digest(fixture(&format!(
            "wolfi/stage3/{LIBFFI}"
        ))))
    );
    // Its dependency (`so:libc.so.6`, glibc in that index) was asked for too; the
    // fake does not serve it, so it is not in the chain and libffi is not held up by it.
    // The walk runs on its own and may reach the upstream after libffi is PolicyChecked:
    // wait for it (bounded), as for the state above.
    wait_hits(
        &up,
        "/wolfi/x86_64/glibc-2.36-r0.apk",
        1,
        "the closure was walked",
    );
    assert!(chain
        .status(&apk::id("wolfi", "x86_64", "glibc", "2.36-r0"))
        .is_none());

    // 3. While it waits: the pull says so, the .apk is not served, the index lists nothing.
    let held = get_settled(&r.http, "/production/wolfi/x86_64/-/pull/libffi");
    assert_eq!(held.status, 404, "{}", held.text());
    assert!(
        held.text().contains("waiting for approval"),
        "{}",
        held.text()
    );
    assert!(
        held.text()
            .contains(&format!("wolfi/x86_64/{LIBFFI}: POLICY_CHECKED")),
        "the closure is named: {}",
        held.text()
    );
    assert_eq!(
        get(&r.http, &format!("/production/wolfi/x86_64/{LIBFFI}"), &[]).status,
        404
    );
    assert_eq!(listed(&r, "wolfi"), []);

    // 4. Approved: the index lists libffi with the upstream's stanza, the .apk is the
    //    sealed bytes, and the pull answers 200.
    approve(&chain, &id);
    assert_eq!(
        listed(&r, "wolfi"),
        [("libffi".to_string(), "3.4.2-r0".to_string())]
    );
    let g = get(&r.http, &format!("/production/wolfi/x86_64/{LIBFFI}"), &[]);
    assert_eq!(g.status, 200, "{}", g.text());
    assert_eq!(
        g.headers.get("x-holger-intake-state").map(String::as_str),
        Some("AVAILABLE")
    );
    assert_eq!(g.body, fixture(&format!("wolfi/stage3/{LIBFFI}")));
    let ok = get(&r.http, "/production/wolfi/x86_64/-/pull/libffi", &[]);
    assert_eq!(ok.status, 200, "{}", ok.text());
    assert_eq!(
        up.hits(&format!("/wolfi/x86_64/{LIBFFI}")),
        1,
        "the upstream gave the package once"
    );
    // The listed stanza is the upstream's, so a client holds the bytes to the same hashes.
    let key = holger_key(&r, "wolfi");
    let idx = verify_index(
        &get(&r.http, "/production/wolfi/x86_64/APKINDEX.tar.gz", &[]).body,
        &[key],
    )
    .unwrap();
    let upstream = verify_index(
        &fixture("wolfi/stage3/APKINDEX.tar.gz"),
        &[ApkKey::from_pem(WOLFI_KEY, &wolfi_pem().1).unwrap()],
    )
    .unwrap();
    let theirs = upstream
        .entries
        .iter()
        .find(|e| e.name == "libffi")
        .unwrap();
    assert_eq!(idx.entries[0].stanza, theirs.stanza);
    holger_provenance::apk::verify_package(&g.body, &idx)
        .expect("the served .apk verifies against Holger's index");
}

#[test]
fn a_package_the_signed_index_does_not_name_byte_for_byte_is_never_ingested() {
    let up = upstream();
    let r = start(&source(&up, "tampered"));
    let chain = r.chain();
    assert_eq!(
        get(&r.http, "/production/tampered/x86_64/-/pull/libffi", &[]).status,
        503
    );
    let refused = get_settled(&r.http, "/production/tampered/x86_64/-/pull/libffi");
    assert_eq!(refused.status, 403, "{}", refused.text());
    assert!(
        refused.text().contains("data segment"),
        "{}",
        refused.text()
    );
    assert!(
        chain
            .status(&apk::id("tampered", "x86_64", "libffi", "3.4.2-r0"))
            .is_none(),
        "never reached the chain"
    );
    assert_eq!(listed(&r, "tampered"), []);
}

#[test]
fn an_index_whose_signature_does_not_verify_is_refused_before_a_package_is_fetched() {
    let up = upstream();
    let r = start(&source(&up, "forged"));
    assert_eq!(
        get(&r.http, "/production/forged/x86_64/-/pull/libffi", &[]).status,
        503
    );
    let refused = get_settled(&r.http, "/production/forged/x86_64/-/pull/libffi");
    assert_eq!(refused.status, 403, "{}", refused.text());
    assert!(refused.text().contains("signature"), "{}", refused.text());
    assert_eq!(
        up.hits(&format!("/forged/x86_64/{LIBFFI}")),
        0,
        "no package is read from an index that does not verify"
    );
}

#[test]
fn with_no_pinned_apk_key_every_pull_is_refused_by_name() {
    let up = upstream();
    let r = start_with(&source(&up, "nokey"), "");
    assert_eq!(
        get(&r.http, "/production/nokey/x86_64/-/pull/libffi", &[]).status,
        503
    );
    let refused = get_settled(&r.http, "/production/nokey/x86_64/-/pull/libffi");
    assert_eq!(refused.status, 403, "{}", refused.text());
    assert!(
        refused.text().contains("no apk key is pinned"),
        "{}",
        refused.text()
    );
    assert_eq!(up.hits(&format!("/nokey/x86_64/{LIBFFI}")), 0);
}

#[test]
fn an_exact_apk_file_is_pulled_alone_and_an_unknown_name_is_404() {
    let up = upstream();
    let r = start(&source(&up, "wolfi"));
    let chain = r.chain();
    let first = get(&r.http, &format!("/production/wolfi/x86_64/{LIBFFI}"), &[]);
    assert_eq!(first.status, 503, "{}", first.text());
    let id = apk::id("wolfi", "x86_64", "libffi", "3.4.2-r0");
    wait_state(&chain, &id, ArtifactState::PolicyChecked);
    assert_eq!(
        up.hits("/wolfi/x86_64/glibc-2.36-r0.apk"),
        0,
        "a file request pulls that file only"
    );
    assert_eq!(
        get(
            &r.http,
            "/production/wolfi/x86_64/-/pull/no-such-package",
            &[]
        )
        .status,
        503
    );
    let unknown = get_settled(&r.http, "/production/wolfi/x86_64/-/pull/no-such-package");
    assert_eq!(unknown.status, 404, "{}", unknown.text());
    assert!(
        unknown.text().contains("publishes no"),
        "{}",
        unknown.text()
    );
    assert_eq!(
        get(&r.http, "/production/wolfi/x86_64/not-a-file", &[]).status,
        404
    );
}

// ── the real client ─────────────────────────────────────────────────────────

const CLIENT_IMAGE: &str = "cgr.dev/chainguard/wolfi-base:latest";
const WOLFI_OS: &str = "https://packages.wolfi.dev/os";
/// A closure of a few packages over wolfi-base; its binary answers `--version`.
const REAL_PKG: &str = "jq";

fn podman() -> std::path::PathBuf {
    let path = std::env::var_os("PATH").unwrap_or_default();
    std::env::split_paths(&path).map(|d| d.join("podman")).find(|p| p.is_file()).unwrap_or_else(|| {
        panic!("podman not on PATH: this test drives the REAL apk in {CLIENT_IMAGE} and proves nothing without a container runtime; do not make it skip")
    })
}

/// The image is present, or is pulled once. A failed pull is a failure.
fn ensure_image(podman: &Path, image: &str) {
    let exists = std::process::Command::new(podman)
        .args(["image", "exists", image])
        .status()
        .expect("podman runs");
    if !exists.success() {
        let pull = std::process::Command::new(podman)
            .args(["pull", image])
            .output()
            .expect("podman runs");
        assert!(
            pull.status.success(),
            "image not pullable: {image}\n{}",
            String::from_utf8_lossy(&pull.stderr)
        );
    }
}

/// `apk` in the client image: Holger is its only repository, and (when `key` is given)
/// Holger's key its only key. Holger's loopback port is the client's own `127.0.0.1:<port>`
/// (pasta `-T`). Returns (success, stdout + stderr).
fn apk_client(
    podman: &Path,
    r: &Running,
    source: &str,
    key: Option<(&str, &Path)>,
    script: &str,
) -> (bool, String) {
    let port = r.http.rsplit(':').next().unwrap();
    let mut cmd = std::process::Command::new(podman);
    cmd.args(["run", "--rm", "--network", &format!("pasta:-T,{port}")]);
    if let Some((name, file)) = key {
        cmd.arg("-v")
            .arg(format!("{}:/etc/apk/keys/{name}:ro", file.display()));
    }
    let sh = format!(
        "rm -f /etc/apk/keys/wolfi-signing.rsa.pub /etc/apk/keys/chainguard-*; \
         echo http://127.0.0.1:{port}/production/{source} > /etc/apk/repositories; {script}"
    );
    let out = cmd
        .args([CLIENT_IMAGE, "sh", "-c", &sh])
        .output()
        .expect("podman run");
    let text = format!(
        "{}{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    eprintln!("--- apk ({script})\n{text}");
    (out.status.success(), text)
}

/// The closure lines of a pull answer (`  <coordinate>: <STATE>`).
fn closure_states(answer: &str) -> Vec<(String, String)> {
    answer
        .lines()
        .filter_map(|l| {
            l.strip_prefix("  ")?
                .split_once(": ")
                .map(|(n, s)| (n.to_string(), s.trim().to_string()))
        })
        .filter(|(n, _)| n.ends_with(".apk"))
        .collect()
}

#[test]
#[ignore = "a real apk add in Wolfi against Holger; needs podman, the image and packages.wolfi.dev (cargo xtask demo tests)"]
fn real_client_apk_add_through_holger() {
    let podman = podman();
    ensure_image(&podman, CLIENT_IMAGE);
    let src = format!(
        r#"(name: "wolfi", kind: apk, url: "{WOLFI_OS}", gates: (provenance: Some(require(apk_keys: ["{WOLFI_KEY}"])))),"#
    );
    let r = start(&src);
    let chain = r.chain();

    // Holger's key, as a client puts it into /etc/apk/keys.
    let key = holger_key(&r, "wolfi");
    let keys = tempfile::tempdir().unwrap();
    let key_file = keys.path().join(&key.name);
    std::fs::write(
        &key_file,
        get(&r.http, &format!("/production/wolfi/{}", key.name), &[]).body,
    )
    .unwrap();
    let keyed = Some((key.name.as_str(), key_file.as_path()));

    // 1. Nothing AVAILABLE: apk trusts Holger's signed index, and it names no such package.
    let (_, out) = apk_client(
        &podman,
        &r,
        "wolfi",
        keyed,
        &format!("apk update 2>&1; apk add {REAL_PKG} 2>&1"),
    );
    assert!(
        out.contains("holger wolfi x86_64: the AVAILABLE packages"),
        "Holger's index was not read:\n{out}"
    );
    assert!(
        !out.contains("BAD signature") && !out.contains("UNTRUSTED"),
        "the client refused Holger's index:\n{out}"
    );
    assert!(
        out.contains(&format!("{REAL_PKG} (no such package)")),
        "before the pull apk add finds no such package:\n{out}"
    );

    // 2. The pull: not yet (503 + Retry-After), then waiting for a person, the closure named.
    let pull = format!("/production/wolfi/x86_64/-/pull/{REAL_PKG}");
    let first = get(&r.http, &pull, &[]);
    assert_eq!(first.status, 503, "{}", first.text());
    assert!(
        first.headers.contains_key("retry-after"),
        "{:?}",
        first.headers
    );

    // 3. A person approves each package of the closure as it reaches POLICY_CHECKED,
    //    until the pull answers 200 and no package it brought in waits for a person.
    let t = Instant::now();
    let mut approved = Vec::new();
    let mut last = String::new();
    let done = loop {
        assert!(
            t.elapsed() < Duration::from_secs(600),
            "the pull of {REAL_PKG} never settled; approved {approved:?}; last answer: {last}"
        );
        let g = get_settled(&r.http, &pull);
        last = format!("{} {}", g.status, g.text());
        let states = closure_states(&g.text());
        // Approved is the person's word; the seal and the walk to AVAILABLE follow when
        // the index (or the file) is next asked for.
        let judged = |s: &str| matches!(s, "APPROVED" | "SEALED" | "PROMOTED" | "AVAILABLE");
        if g.status == 200 && !states.is_empty() && states.iter().all(|(_, s)| judged(s)) {
            break g.text();
        }
        assert!(
            g.status == 404 || g.status == 200 || g.status == 503,
            "the pull answered {}: {}",
            g.status,
            g.text()
        );
        if g.status == 404 {
            assert!(g.text().contains("waiting for approval"), "{}", g.text());
        }
        for (n, s) in states.iter().filter(|(_, s)| s == "POLICY_CHECKED") {
            assert!(n.starts_with("wolfi/x86_64/"), "{n} {s}");
            approve(
                &chain,
                &traits::ArtifactId {
                    namespace: None,
                    name: n.clone(),
                    version: String::new(),
                },
            );
            approved.push(n.clone());
        }
        std::thread::sleep(Duration::from_millis(200));
    };
    assert!(
        approved
            .iter()
            .any(|n| n.starts_with(&format!("wolfi/x86_64/{REAL_PKG}-"))),
        "{approved:?}"
    );
    for n in &approved {
        let h = chain.history(&traits::ArtifactId {
            namespace: None,
            name: n.clone(),
            version: String::new(),
        });
        assert_eq!(h[0].actor, "pull-through:wolfi", "{n}: {h:#?}");
        let prov = h
            .iter()
            .flat_map(|e| e.evidence.iter())
            .find(|e| e.kind == EvidenceKind::Provenance)
            .cloned()
            .expect("a provenance slot");
        assert_eq!(prov.verdict, EvidenceVerdict::Pass, "{n}: {prov:?}");
    }
    eprintln!("the pull: {done}");

    // 4. apk add installs it from Holger's AVAILABLE packages, and the binary runs.
    let (ok, out) = apk_client(
        &podman,
        &r,
        "wolfi",
        keyed,
        &format!("apk update && apk add {REAL_PKG} && {REAL_PKG} --version"),
    );
    assert!(
        ok && out.contains("OK: "),
        "apk add {REAL_PKG} from Holger:\n{out}"
    );
    assert!(
        out.lines().any(|l| l.starts_with(&format!("{REAL_PKG}-"))),
        "the binary answers --version:\n{out}"
    );
    // The index walked the whole approved closure on to AVAILABLE.
    let after = get(&r.http, &pull, &[]);
    assert_eq!(after.status, 200, "{}", after.text());
    let states = closure_states(&after.text());
    assert!(
        states.iter().all(|(_, s)| s == "AVAILABLE"),
        "{}",
        after.text()
    );

    // 5. Without Holger's key the client refuses Holger's index.
    let (_, out) = apk_client(&podman, &r, "wolfi", None, "apk update 2>&1");
    assert!(
        out.contains("UNTRUSTED"),
        "a client without Holger's key took the index:\n{out}"
    );
}