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

nordisk/holger

server/cli/tests/airgap_export_import.rs

//! **S5 — the airgap, end to end through the real binary.**
//!
//! An online holger walks a wheel through the intake chain to AVAILABLE with
//! signed scan evidence. `holger-server export` takes the approved set out as a
//! bundle candidate whose verdict is computed from that evidence; the candidate
//! is approved and release-signed; `holger-server bundle import` on an OFFLINE
//! secure holger verifies the bundle, every payload hash and every evidence
//! attestation, and lands the wheel AVAILABLE; the secure server then serves it
//! through its PEP 503 simple index with the right `#sha256=`.
//!
//! The four proofs:
//!
//! 1. an export refuses an artifact without evidence, by name, and writes nothing;
//! 2. a tampered bundle is refused at import (a flipped byte; forged evidence
//!    re-signed by a trusted release signer; a swapped payload);
//! 3. (the offline guard is `server/lib/tests/an_offline_holger_makes_no_network_call.rs`);
//! 4. the imported wheel is served with the right hash.

use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use traits::test_support::hold_port;

use server_lib::promote::intake::{ArtifactState, TransitionRequest};
use server_lib::ArtifactId;

const BIN: &str = env!("CARGO_BIN_EXE_holger");
const WHEEL_A: &str = "demo_pkg-1.0.0-py3-none-any.whl";
const WHEEL_B: &str = "unscanned_pkg-2.0.0-py3-none-any.whl";

fn crc32(data: &[u8]) -> u32 {
    let mut crc = 0xFFFF_FFFFu32;
    for b in data {
        crc ^= *b as u32;
        for _ in 0..8 {
            crc = if crc & 1 != 0 {
                (crc >> 1) ^ 0xEDB8_8320
            } else {
                crc >> 1
            };
        }
    }
    !crc
}

/// A small, well-formed wheel: a stored-only zip with its dist-info.
fn wheel(project: &str, version: &str) -> Vec<u8> {
    let module = project.replace('-', "_");
    let di = format!("{module}-{version}.dist-info");
    let members: Vec<(String, Vec<u8>)> = vec![
        (format!("{module}/__init__.py"), format!("__version__ = \"{version}\"\n").into_bytes()),
        (
            format!("{di}/METADATA"),
            format!("Metadata-Version: 2.1\nName: {project}\nVersion: {version}\nLicense-Expression: MIT\n").into_bytes(),
        ),
        (format!("{di}/WHEEL"), b"Wheel-Version: 1.0\nGenerator: holger-test\nRoot-Is-Purelib: true\nTag: py3-none-any\n".to_vec()),
        (format!("{di}/RECORD"), Vec::new()),
    ];
    let mut out = Vec::new();
    let mut central = Vec::new();
    for (name, body) in &members {
        let crc = crc32(body);
        let offset = out.len() as u32;
        out.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
        out.extend_from_slice(&[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        out.extend_from_slice(&crc.to_le_bytes());
        out.extend_from_slice(&(body.len() as u32).to_le_bytes());
        out.extend_from_slice(&(body.len() as u32).to_le_bytes());
        out.extend_from_slice(&(name.len() as u16).to_le_bytes());
        out.extend_from_slice(&0u16.to_le_bytes());
        out.extend_from_slice(name.as_bytes());
        out.extend_from_slice(body);
        central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
        central.extend_from_slice(&[20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        central.extend_from_slice(&crc.to_le_bytes());
        central.extend_from_slice(&(body.len() as u32).to_le_bytes());
        central.extend_from_slice(&(body.len() as u32).to_le_bytes());
        central.extend_from_slice(&(name.len() as u16).to_le_bytes());
        central.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        central.extend_from_slice(&offset.to_le_bytes());
        central.extend_from_slice(name.as_bytes());
    }
    let cd_offset = out.len() as u32;
    let n = members.len() as u16;
    out.extend_from_slice(&central);
    out.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
    out.extend_from_slice(&[0, 0, 0, 0]);
    out.extend_from_slice(&n.to_le_bytes());
    out.extend_from_slice(&n.to_le_bytes());
    out.extend_from_slice(&(central.len() as u32).to_le_bytes());
    out.extend_from_slice(&cd_offset.to_le_bytes());
    out.extend_from_slice(&0u16.to_le_bytes());
    out
}

fn wheel_id(name: &str) -> ArtifactId {
    ArtifactId {
        namespace: None,
        name: name.into(),
        version: String::new(),
    }
}

fn repos(d: &Path, extra: &str) -> 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("{}/{name}")),"#,
            d.display()
        )
    };
    format!(
        "{} {} {} {} {extra}",
        repo("incoming"),
        repo("quarantine"),
        repo("approved"),
        repo("production")
    )
}

fn ladder(scan: &str) -> String {
    format!(
        r#"promotion: Some((
        ladder_name: "intake",
        stages: [
            (name: "incoming", repo: "incoming"),
            (name: "quarantine", repo: "quarantine"),
            (name: "approved", repo: "approved"),
            (name: "production", repo: "production"),
        ],
        intake: Some((rules: (sandbox: off), 
            incoming: "incoming", quarantine: "quarantine",
            approved: "approved", production: "production",
            {scan}
        )),
    )),"#
    )
}

struct Online {
    dir: tempfile::TempDir,
    config: PathBuf,
    pki: bench_scenarios::secure_update::AttestPki,
}

/// The online side: wheel A walked to AVAILABLE through a chain that scans and
/// signs its evidence; wheel B walked to AVAILABLE through the same chain's
/// repositories by a chain with NO scanner, so it has no stored evidence.
fn online() -> Online {
    online_with(None)
}

/// A demo release key for the provenance tests (G3a): Ed25519, minted here.
struct Prov {
    key: rcgen::KeyPair,
}

impl Prov {
    fn new() -> Self {
        Self {
            key: rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519).unwrap(),
        }
    }
    /// The `scan.provenance` block that requires this key's signature.
    fn ron(&self, inbox: &Path) -> String {
        use base64::Engine as _;
        use rcgen::PublicKeyData as _;
        let spki =
            base64::engine::general_purpose::STANDARD.encode(self.key.subject_public_key_info());
        format!(
            r#"provenance: Some((rules: (keys: [(id: "demo-release", alg: ed25519, public_key: "{spki}")]),
                default: require(keys: ["demo-release"]), inbox: Some("{}"))),"#,
            inbox.display()
        )
    }
    /// A signed SLSA statement about `bytes`, as a DSSE envelope.
    fn attest(&self, name: &str, bytes: &[u8]) -> Vec<u8> {
        use base64::Engine as _;
        use rcgen::SigningKey as _;
        let b64 = |b: &[u8]| base64::engine::general_purpose::STANDARD.encode(b);
        let st = serde_json::to_vec(&serde_json::json!({
            "_type": "https://in-toto.io/Statement/v1",
            "subject": [{"name": name, "digest": {"sha256": update_bundle::digest::sha256_hex(bytes)}}],
            "predicateType": "https://slsa.dev/provenance/v1",
            "predicate": {"buildDefinition": {"buildType": "https://holger.rs/demo-build/v1"},
                          "runDetails": {"builder": {"id": "https://holger.rs/demo-builder"}}}
        }))
        .unwrap();
        let t = "application/vnd.in-toto+json";
        let mut pae = format!("DSSEv1 {} {t} {} ", t.len(), st.len()).into_bytes();
        pae.extend_from_slice(&st);
        let sig = self.key.sign(&pae).unwrap();
        serde_json::to_vec(&serde_json::json!({"payloadType": t, "payload": b64(&st), "signatures": [{"keyid": "demo-release", "sig": b64(&sig)}]})).unwrap()
    }
}

fn online_with(prov: Option<&Prov>) -> Online {
    let dir = tempfile::tempdir().unwrap();
    let d = dir.path().join("online");
    std::fs::create_dir_all(&d).unwrap();
    let pki = bench_scenarios::secure_update::mint_attestation_pki(&d.join("pki"), "online-scan")
        .unwrap();
    let attestation = format!(
        r#"attestation: Some((
        trust_roots: ["{}"],
        signer_key: Some("{}"),
        signer_cert: Some("{}"),
        signer_alg: Some("ecdsa-p256"),
    )),"#,
        pki.root_der.display(),
        pki.key_pkcs8.display(),
        pki.cert_der.display()
    );
    let body = |scan: &str| {
        format!(
            r#"(
    repositories: [ {repos} ],
    exposed_endpoints: [ (ron_name: "door", ron_url: "127.0.0.1:0", ron_http_url: None) ],
    storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{d}") ],
    audit: (dir: "{d}/audit", enabled: true),
    {attestation}
    {ladder}
)"#,
            repos = repos(&d, ""),
            d = d.display(),
            ladder = ladder(scan),
        )
    };
    let config = dir.path().join("online.ron");
    let inbox = d.join("holger-provenance");
    let prov_ron = prov.map(|p| p.ron(&inbox)).unwrap_or_default();
    std::fs::write(
        &config,
        body(&format!(
            "scan: Some((required: [vulnerability, sbom], {prov_ron})),"
        )),
    )
    .unwrap();
    let bare = dir.path().join("online-noscan.ron");
    std::fs::write(&bare, body("")).unwrap();

    for (cfg, name, project) in [
        (&config, WHEEL_A, "demo-pkg"),
        (&bare, WHEEL_B, "unscanned-pkg"),
    ] {
        let mut h = server_lib::read_ron_config(cfg).unwrap();
        h.instantiate_backends().unwrap();
        let audit = h.audit_log().unwrap();
        let chain = h.intake_chain(audit).unwrap().expect("an intake chain");
        let incoming = h.get_repo("incoming").unwrap().clone();
        let id = wheel_id(name);
        let version = if name == WHEEL_A { "1.0.0" } else { "2.0.0" };
        let bytes = wheel(project, version);
        if let (Some(p), true) = (prov, name == WHEEL_A) {
            let dir = inbox.join(update_bundle::digest::sha256_hex(&bytes));
            std::fs::create_dir_all(&dir).unwrap();
            std::fs::write(
                dir.join(format!("{name}.intoto.jsonl")),
                p.attest(name, &bytes),
            )
            .unwrap();
        }
        chain
            .ingest("ida", "test", "incoming", &id, &bytes, || {
                incoming.put(&id, &bytes)
            })
            .unwrap_or_else(|e| panic!("ingest {name}: {e}"));
        for (to, who) in [
            (ArtifactState::Scanned, "ida"),
            (ArtifactState::PolicyChecked, "ida"),
            (ArtifactState::Approved, "bob"),
            (ArtifactState::Sealed, "bob"),
            (ArtifactState::Promoted, "bob"),
            (ArtifactState::Available, "bob"),
        ] {
            chain
                .advance(TransitionRequest::new(id.clone(), to, who))
                .unwrap_or_else(|e| panic!("{name} → {to}: {e}"));
        }
        assert_eq!(chain.state(&id), ArtifactState::Available);
    }
    Online { dir, config, pki }
}

fn export(o: &Online, out: &Path, artifacts: &[&str]) -> Output {
    let mut c = Command::new(BIN);
    c.args(["export", "--config"])
        .arg(&o.config)
        .arg("--candidate")
        .arg(out);
    c.args([
        "--bundle-id",
        "airgap-1",
        "--sequence",
        "1",
        "--supplier",
        "online",
        "--product",
        "pypi",
        "--release-version",
        "1",
    ]);
    for a in artifacts {
        c.args(["--artifact", a]);
    }
    c.output().unwrap()
}

fn text(o: &Output) -> String {
    format!(
        "{}{}",
        String::from_utf8_lossy(&o.stdout),
        String::from_utf8_lossy(&o.stderr)
    )
}

struct Secure {
    dir: PathBuf,
    config: PathBuf,
    http: u16,
}

/// The secure side: offline, the four intake repositories, a bundle store, and
/// an attestation block that trusts the online side's root and enrols its signer.
fn secure(o: &Online) -> Secure {
    secure_with(o, "")
}

fn secure_with(o: &Online, scan: &str) -> Secure {
    let dir = o.dir.path().join("secure");
    std::fs::create_dir_all(&dir).unwrap();
    let http = hold_port();
    let grpc = hold_port();
    let bundles = format!(
        r#"(ron_name: "bundles", ron_repo_type: "raw", ron_upstreams: [], ron_in: None, ron_out: None, ron_data_dir: Some("{}/bundles")),"#,
        dir.display()
    );
    let ron = format!(
        r#"(
    network: Offline,
    repositories: [ {repos} ],
    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),
    attestation: Some((
        trust_roots: ["{root}"],
        enrolled_signers: ["{cert}"],
    )),
    {ladder}
)"#,
        repos = repos(&dir, &bundles),
        d = dir.display(),
        root = o.pki.root_der.display(),
        cert = o.pki.cert_der.display(),
        ladder = ladder(scan),
    );
    let config = o.dir.path().join("secure.ron");
    std::fs::write(&config, ron).unwrap();
    Secure { dir, config, http }
}

fn import(s: &Secure, bundle: &Path, policy: &Path) -> Output {
    allow_first_import(BIN, &s.config, bundle, Some(policy));
    Command::new(BIN)
        .args(["bundle", "import", "--config"])
        .arg(&s.config)
        .arg("--bundle")
        .arg(bundle)
        .arg("--policy")
        .arg(policy)
        .output()
        .unwrap()
}

/// Export wheel A, sign it, and return `(bundle, policy)`.
fn signed_bundle(o: &Online, name: &str) -> (PathBuf, PathBuf) {
    let cand = o.dir.path().join(format!("{name}.cand"));
    let out = export(o, &cand, &[WHEEL_A]);
    assert!(out.status.success(), "export: {}", text(&out));
    let members = update_bundle::prepare::read_members_dir(&cand).unwrap();
    sign(o, members, name)
}

fn sign(o: &Online, members: update_bundle::MemberMap, name: &str) -> (PathBuf, PathBuf) {
    let bundle = o.dir.path().join(format!("{name}.holger.znippy"));
    let policy = bench_scenarios::secure_update::sign_candidate_and_pack(members, &bundle).unwrap();
    let policy_path = o.dir.path().join(format!("{name}.policy.ron"));
    std::fs::write(&policy_path, policy.to_ron().unwrap()).unwrap();
    (bundle, policy_path)
}

fn http_get(port: u16, path: &str) -> (u16, Vec<u8>) {
    let mut s = std::net::TcpStream::connect(("127.0.0.1", port)).unwrap();
    write!(
        s,
        "GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"
    )
    .unwrap();
    let mut raw = Vec::new();
    s.read_to_end(&mut raw).unwrap();
    let split = raw
        .windows(4)
        .position(|w| w == b"\r\n\r\n")
        .expect("an HTTP head");
    let head = String::from_utf8_lossy(&raw[..split]).to_string();
    let status: u16 = head.split_whitespace().nth(1).unwrap().parse().unwrap();
    let mut body = raw[split + 4..].to_vec();
    if head
        .to_ascii_lowercase()
        .contains("transfer-encoding: chunked")
    {
        let mut out = Vec::new();
        let mut rest = &body[..];
        loop {
            let nl = rest.windows(2).position(|w| w == b"\r\n").unwrap();
            let n = usize::from_str_radix(std::str::from_utf8(&rest[..nl]).unwrap().trim(), 16)
                .unwrap();
            if n == 0 {
                break;
            }
            out.extend_from_slice(&rest[nl + 2..nl + 2 + n]);
            rest = &rest[nl + 2 + n + 2..];
        }
        body = out;
    }
    (status, body)
}

#[test]
fn an_export_refuses_an_artifact_without_evidence_by_name() {
    let o = online();
    let cand = o.dir.path().join("both.cand");
    let out = export(&o, &cand, &[WHEEL_A, WHEEL_B]);
    let t = text(&out);
    assert!(
        !out.status.success(),
        "an export carrying an unscanned artifact must fail: {t}"
    );
    assert!(
        t.contains("[holger-airgap-2]"),
        "refused with the no-evidence code: {t}"
    );
    assert!(t.contains(WHEEL_B), "the refusal names the artifact: {t}");
    assert_eq!(t.matches("REFUSED [").count(), 1, "only B is refused: {t}");
    assert!(!cand.exists(), "a refused export writes nothing");

    // The whole set (no --artifact) holds B too, so it is refused the same way.
    let all = o.dir.path().join("all.cand");
    let out = export(&o, &all, &[]);
    assert!(
        !out.status.success() && text(&out).contains(WHEEL_B),
        "{}",
        text(&out)
    );
    assert!(!all.exists());

    // A alone exports, and its verdict is computed: pass.
    let ok = o.dir.path().join("a.cand");
    let out = export(&o, &ok, &[WHEEL_A]);
    assert!(out.status.success(), "{}", text(&out));
    let core: serde_json::Value =
        serde_json::from_slice(&std::fs::read(ok.join("core.json")).unwrap()).unwrap();
    assert_eq!(core["policy_result"]["verdict"], "pass");
    assert_eq!(
        core["source_provenance"]["source_kind"],
        server_lib::promote::airgap::SOURCE_KIND
    );
    let index: serde_json::Value = serde_json::from_slice(
        &std::fs::read(
            ok.join("evidence")
                .join(server_lib::promote::airgap::INDEX_PATH),
        )
        .unwrap(),
    )
    .unwrap();
    let art = &index["artifacts"][0];
    assert_eq!(art["name"], WHEEL_A);
    assert!(art["sbom"].is_string(), "the SBOM rides along: {art}");
    assert!(
        !art["attestations"].as_array().unwrap().is_empty(),
        "the attestations ride along: {art}"
    );
    assert_eq!(art["approvers"], serde_json::json!(["bob"]));
    assert!(
        art["not_run"]
            .as_array()
            .unwrap()
            .iter()
            .any(|k| k == "malware"),
        "an absent scanner is listed, never passed: {art}"
    );
    // The export is audited.
    let audit: String = std::fs::read_dir(o.dir.path().join("online/audit"))
        .unwrap()
        .flatten()
        .map(|e| std::fs::read_to_string(e.path()).unwrap_or_default())
        .collect();
    assert!(
        audit.contains("airgap export airgap-1"),
        "the export left an audit record"
    );
    assert!(
        audit.contains("exported to airgap bundle airgap-1"),
        "each exported artifact has its `exported` trail step"
    );
}

/// **The airgapped secure side is the open build** (CI-RED, HOL-AUDIT 2026-10-31): an estate
/// build (`estate`, which `metered` turns on) always dials its front, its twin and the ONE box,
/// so it refuses `network: Offline` by name (`holger-offline-2`) and can never be the secure
/// side of a stick. The four tests that import into that side run in the open build only;
/// this one holds the estate build to the refusal.
#[cfg(feature = "estate")]
#[test]
fn an_estate_build_refuses_to_be_the_airgapped_side_by_name() {
    let o = online();
    let s = secure(&o);
    let (bundle, policy) = signed_bundle(&o, "estate");
    let out = import(&s, &bundle, &policy);
    let t = text(&out);
    assert!(
        !out.status.success(),
        "an estate build must not import on the airgapped side: {t}"
    );
    assert!(
        t.contains("[holger-offline-2]") && t.contains("estate build"),
        "{t}"
    );
}

// the airgapped secure side is the open build (an_estate_build_refuses_to_be_the_airgapped_side_by_name)
#[cfg(not(feature = "estate"))]
#[test]
fn a_tampered_bundle_is_refused_at_import() {
    let o = online();
    let s = secure(&o);

    // (a) One flipped byte in the signed bundle's payload, signatures untouched.
    // (A byte flipped at a fixed offset of the container can land in bytes no
    // member reads, so the flip is made in a member's content.)
    let (bundle, policy) = signed_bundle(&o, "honest");
    let mut members = update_bundle::container::load_bundle(&bundle).unwrap();
    let payload = members
        .keys()
        .find(|k| k.starts_with("payload/"))
        .cloned()
        .unwrap();
    let mid = members[&payload].len() / 2;
    members.get_mut(&payload).unwrap()[mid] ^= 0x01;
    let flipped = o.dir.path().join("flipped.holger.znippy");
    update_bundle::container::pack_bundle(&members, &flipped).unwrap();
    let out = import(&s, &flipped, &policy);
    assert!(
        !out.status.success(),
        "a flipped byte must be refused: {}",
        text(&out)
    );

    // (b) Forged evidence, re-signed by a TRUSTED release signer: the release
    // signature verifies, the evidence attestation does not.
    let cand = o.dir.path().join("forged.cand");
    assert!(export(&o, &cand, &[WHEEL_A]).status.success());
    let mut members = update_bundle::prepare::read_members_dir(&cand).unwrap();
    let env_path = members
        .keys()
        .find(|k| k.contains("/attestations/"))
        .cloned()
        .unwrap();
    let env: serde_json::Value = serde_json::from_slice(&members[&env_path]).unwrap();
    let mut forged = env.clone();
    forged["signatures"][0]["sig"] = serde_json::Value::String("AAAA".repeat(16));
    let forged_bytes = serde_json::to_vec(&forged).unwrap();
    for (_, e) in members
        .iter_mut()
        .filter(|(k, _)| k.contains("/attestations/"))
    {
        *e = forged_bytes.clone();
    }
    rewrite_core(&mut members);
    let (forged_bundle, forged_policy) = sign(&o, members, "forged");
    let out = import(&s, &forged_bundle, &forged_policy);
    let t = text(&out);
    assert!(
        !out.status.success(),
        "forged evidence must be refused: {t}"
    );
    assert!(
        t.contains("[holger-airgap-9]"),
        "refused as an attestation that does not verify: {t}"
    );

    // (c) A swapped payload, re-signed: the bundle is consistent, the artifact is not
    // the one the evidence and the index describe.
    let cand = o.dir.path().join("swapped.cand");
    assert!(export(&o, &cand, &[WHEEL_A]).status.success());
    let mut members = update_bundle::prepare::read_members_dir(&cand).unwrap();
    let payload = members
        .keys()
        .find(|k| k.starts_with("payload/"))
        .cloned()
        .unwrap();
    members.insert(payload, wheel("demo-pkg", "6.6.6"));
    rewrite_core(&mut members);
    let (swapped, swapped_policy) = sign(&o, members, "swapped");
    let out = import(&s, &swapped, &swapped_policy);
    let t = text(&out);
    assert!(
        !out.status.success(),
        "a swapped payload must be refused: {t}"
    );
    assert!(
        t.contains("[holger-airgap-8]"),
        "refused as a payload mismatch: {t}"
    );

    // Nothing landed from any of them.
    assert!(
        !s.dir
            .join("holger-prod-store")
            .join("lineage")
            .join("intake")
            .exists()
            || std::fs::read_dir(s.dir.join("holger-prod-store/lineage/intake"))
                .unwrap()
                .next()
                .is_none()
    );
}

/// Recompute `core.json`'s member digests after a test changed members, and the
/// envelope's core digest, as a release engineer holding the keys could.
fn rewrite_core(members: &mut update_bundle::MemberMap) {
    use update_bundle::digest::{blake3_hex, sha256_hex};
    let mut core = update_bundle::core::BundleCore::from_json_bytes(&members["core.json"]).unwrap();
    for p in &mut core.payloads {
        let b = &members[&p.relative_path];
        p.sha256 = sha256_hex(b);
        p.blake3 = blake3_hex(b);
        p.size_bytes = b.len() as u64;
    }
    for e in &mut core.evidence {
        let b = &members[&e.relative_path];
        e.sha256 = sha256_hex(b);
        e.blake3 = blake3_hex(b);
        e.size_bytes = b.len() as u64;
    }
    let core_bytes = core.to_json_bytes().unwrap();
    let mut env: serde_json::Value = serde_json::from_slice(&members["manifest.json"]).unwrap();
    env["core_digest"] = serde_json::Value::String(sha256_hex(&core_bytes));
    members.insert(
        "manifest.json".into(),
        serde_json::to_vec_pretty(&env).unwrap(),
    );
    members.insert("core.json".into(), core_bytes);
}

#[test]
fn the_export_carries_only_what_decides() {
    // HOL-NCSC (NCSC P2): the candidate carries the approvers and the evidence, never the
    // importer, the operators' words, the timings or the online box's repository names.
    let o = online();
    let out = o.dir.path().join("candidate-decision");
    let e = export(&o, &out, &[WHEEL_A]);
    assert!(e.status.success(), "{}", text(&e));
    let members = update_bundle::prepare::read_members_dir(&out).unwrap();
    let mut said = String::new();
    for (path, bytes) in &members {
        if path.starts_with("payload/")
            || path.ends_with(".der")
            || path.contains("/attestations/")
            || path.ends_with("scan-evidence.json")
            || path.ends_with(".cdx.json")
            || path.contains("/provenance/")
        {
            continue;
        }
        said.push_str(&format!("== {path}\n{}\n", String::from_utf8_lossy(bytes)));
    }
    assert!(said.contains("\"bob\""), "the approver crosses: {said}");
    for word in [
        "\"ida\"",
        "exported_by",
        "exported_at",
        "\"repo\":\"incoming",
        "\"repo\":\"production",
        "\"origin\"",
    ] {
        assert!(!said.contains(word), "{word} crossed the boundary:\n{said}");
    }
    let core: serde_json::Value =
        serde_json::from_slice(members.get("core.json").expect("core.json")).unwrap();
    let sp = &core["source_provenance"];
    assert!(
        sp["fetched_at"].is_null() && sp["source_repository"].is_null(),
        "the online box's timing and repository stay online: {sp}"
    );
    let record: String = members
        .iter()
        .filter(|(p, _)| p.ends_with("intake.jsonl"))
        .map(|(_, b)| String::from_utf8_lossy(b).into_owned())
        .collect();
    assert!(!record.is_empty(), "the decision record crosses");
    for line in record.lines() {
        let v: serde_json::Value = serde_json::from_str(line).unwrap();
        assert_eq!(
            (v["at"].as_i64(), v["reason"].as_str(), v["policy"].as_str()),
            (Some(0), Some(""), Some("")),
            "{line}"
        );
    }
}

// the airgapped secure side is the open build (an_estate_build_refuses_to_be_the_airgapped_side_by_name)
#[cfg(not(feature = "estate"))]
#[test]
fn the_imported_wheel_is_served_with_the_right_hash() {
    let o = online();
    let s = secure(&o);
    let (bundle, policy) = signed_bundle(&o, "honest");
    let out = import(&s, &bundle, &policy);
    let t = text(&out);
    assert!(out.status.success(), "the honest bundle imports: {t}");
    assert!(t.contains(&format!("LANDED {WHEEL_A}@ AVAILABLE")), "{t}");

    // The re-import is idempotent.
    let again = import(&s, &bundle, &policy);
    assert!(again.status.success(), "{}", text(&again));

    // The audit trail of the secure side holds the landing, every state of it.
    let audit: String = std::fs::read_dir(s.dir.join("audit"))
        .unwrap()
        .flatten()
        .map(|e| std::fs::read_to_string(e.path()).unwrap_or_default())
        .collect();
    assert!(audit.contains("airgap:airgap-1"), "the landing is audited");
    assert!(audit.contains("PROMOTED → AVAILABLE"), "{audit}");

    // The secure server serves it: offline, from the sealed store.
    let mut child = Command::new(BIN)
        .args(["start", "--config"])
        .arg(&s.config)
        .spawn()
        .unwrap();
    let mut up = false;
    for _ in 0..300 {
        if std::net::TcpStream::connect(("127.0.0.1", s.http)).is_ok() {
            up = true;
            break;
        }
        std::thread::sleep(std::time::Duration::from_millis(50));
    }
    let result = std::panic::catch_unwind(|| {
        assert!(up, "the secure server came up");
        let want = update_bundle::digest::sha256_hex(&wheel("demo-pkg", "1.0.0"));
        let (st, root) = http_get(s.http, "/production/simple/");
        assert_eq!(st, 200, "{}", String::from_utf8_lossy(&root));
        assert!(String::from_utf8_lossy(&root).contains("href=\"demo-pkg/\""));
        let (st, page) = http_get(s.http, "/production/simple/demo-pkg/");
        let page = String::from_utf8_lossy(&page).to_string();
        assert_eq!(st, 200, "{page}");
        let frag = format!("#sha256={want}");
        assert!(
            page.contains(&frag),
            "the link carries the wheel's sha256: {page}"
        );
        let href = page
            .split("href=\"")
            .nth(1)
            .unwrap()
            .split('#')
            .next()
            .unwrap();
        assert_eq!(href, format!("../../{WHEEL_A}"));
        let (st, bytes) = http_get(s.http, &format!("/production/{WHEEL_A}"));
        assert_eq!(st, 200);
        assert_eq!(
            update_bundle::digest::sha256_hex(&bytes),
            want,
            "the served bytes are the wheel"
        );
        let (st, _) = http_get(s.http, "/production/simple/unscanned-pkg/");
        assert_eq!(st, 404, "what was never imported is not listed");
    });
    let _ = child.kill();
    let _ = child.wait();
    if let Err(p) = result {
        std::panic::resume_unwind(p);
    }
}

/// The secure side's audit lines, as JSON.
fn audit_lines(s: &Secure) -> Vec<serde_json::Value> {
    std::fs::read_dir(s.dir.join("audit"))
        .unwrap()
        .flatten()
        .flat_map(|e| {
            std::fs::read_to_string(e.path())
                .unwrap_or_default()
                .lines()
                .map(str::to_string)
                .collect::<Vec<_>>()
        })
        .filter_map(|l| serde_json::from_str(&l).ok())
        .collect()
}

fn trail_of(lines: &[serde_json::Value], sha: &str) -> Vec<String> {
    lines
        .iter()
        .filter(|l| l["sha256"] == sha)
        .filter_map(|l| l["trail"].as_str().map(str::to_string))
        .collect()
}

/// G3a: the secure side verifies the carried SLSA attestation again, BEFORE it
/// writes anything about the import. A tampered attestation is refused by name
/// and its trail holds only the refusal; the honest bundle then lands once.
// the airgapped secure side is the open build (an_estate_build_refuses_to_be_the_airgapped_side_by_name)
#[cfg(not(feature = "estate"))]
#[test]
fn a_tampered_attestation_is_refused_before_the_import_writes_its_trail() {
    let prov = Prov::new();
    let o = online_with(Some(&prov));
    let inbox = o.dir.path().join("secure-prov");
    let s = secure_with(
        &o,
        &format!(
            "scan: Some((required: [provenance], {})),",
            prov.ron(&inbox)
        ),
    );
    let sha = update_bundle::digest::sha256_hex(&wheel("demo-pkg", "1.0.0"));

    // The attestation the export carries, changed after it was signed.
    let cand = o.dir.path().join("ptamper.cand");
    let out = export(&o, &cand, &[WHEEL_A]);
    assert!(out.status.success(), "export: {}", text(&out));
    let mut members = update_bundle::prepare::read_members_dir(&cand).unwrap();
    let att = members
        .keys()
        .find(|k| k.contains("/provenance/"))
        .cloned()
        .expect("the export carries the attestation");
    let mut env: serde_json::Value = serde_json::from_slice(&members[&att]).unwrap();
    use base64::Engine as _;
    let b64 = base64::engine::general_purpose::STANDARD;
    let st = String::from_utf8(b64.decode(env["payload"].as_str().unwrap()).unwrap())
        .unwrap()
        .replace("demo-builder", "evil-builder");
    env["payload"] = b64.encode(st).into();
    members.insert(att, serde_json::to_vec(&env).unwrap());
    rewrite_core(&mut members);
    let (bundle, policy) = sign(&o, members, "ptamper");
    let out = import(&s, &bundle, &policy);
    let t = text(&out);
    assert!(
        !out.status.success(),
        "a tampered attestation must be refused: {t}"
    );
    assert!(
        t.contains("[holger-provenance-3]"),
        "refused as a signature that does not verify: {t}"
    );
    let trail = trail_of(&audit_lines(&s), &sha);
    assert!(
        !trail
            .iter()
            .any(|k| k == "hash_verified" || k == "airgap_imported" || k == "imported"),
        "a refused import writes only its refusal: {trail:?}"
    );
    assert!(
        trail.iter().any(|k| k == "policy_denied"),
        "the refusal is on the trail: {trail:?}"
    );

    // The honest bundle lands, once, with the provenance verified again here.
    let (bundle, policy) = signed_bundle(&o, "honest");
    let out = import(&s, &bundle, &policy);
    assert!(
        out.status.success(),
        "the honest bundle imports: {}",
        text(&out)
    );
    let trail = trail_of(&audit_lines(&s), &sha);
    assert_eq!(
        trail.iter().filter(|k| *k == "airgap_imported").count(),
        1,
        "{trail:?}"
    );
    assert!(
        trail.iter().any(|k| k == "scanned"),
        "the second verification is on the trail: {trail:?}"
    );
}

// ── H4: the sync manifest rides in the bundle, the ack comes back on the stick ──

/// Put a `sync:` block into a written config.
#[cfg(feature = "sync")]
fn add_sync(config: &Path, block: &str) {
    let s = std::fs::read_to_string(config).unwrap();
    let i = s.rfind(')').unwrap();
    std::fs::write(
        config,
        format!("{}    sync: Some(({block})),\n{}", &s[..i], &s[i..]),
    )
    .unwrap();
}

#[cfg(feature = "sync")]
fn cli(args: &[&std::ffi::OsStr]) -> Output {
    Command::new(BIN).args(args).output().unwrap()
}

#[cfg(feature = "sync")]
// the airgapped secure side is the open build (an_estate_build_refuses_to_be_the_airgapped_side_by_name)
#[cfg(not(feature = "estate"))]
#[test]
fn a_bundle_carries_the_sync_manifest_and_every_ack_goes_back_on_the_stick() {
    use server_lib::sync::protocol::SyncKey;
    use std::ffi::OsStr;
    let os = |s: &str| OsStr::new(s).to_os_string();
    let o = online();
    let s = secure(&o);
    let keys = o.dir.path().join("sync-keys");
    std::fs::create_dir_all(&keys).unwrap();
    let (up_key, down_key) = (keys.join("dmz.key"), keys.join("onprem.key"));
    let up_hex = SyncKey::open_or_mint(&up_key).unwrap().public_hex();
    let down_hex = SyncKey::open_or_mint(&down_key).unwrap().public_hex();
    add_sync(
        &o.config,
        &format!(
            r#"name: "dmz", key: Some("{}"), downstreams: [(name: "onprem", key: "{down_hex}")], ack_deadline_secs: 3600"#,
            up_key.display()
        ),
    );
    add_sync(
        &s.config,
        &format!(
            r#"name: "onprem", key: Some("{}"), upstreams: [(name: "dmz", key: "{up_hex}")]"#,
            down_key.display()
        ),
    );
    let stick = o.dir.path().join("stick");
    std::fs::create_dir_all(&stick).unwrap();
    let run = |args: Vec<std::ffi::OsString>| {
        let refs: Vec<&OsStr> = args.iter().map(|a| a.as_os_str()).collect();
        let out = cli(&refs);
        assert!(out.status.success(), "{args:?}: {}", text(&out));
        text(&out)
    };

    // 1. The export carries a signed manifest; the import applies it, and its ack goes on the stick.
    let (bundle, policy) = signed_bundle(&o, "synced");
    assert!(
        o.dir
            .path()
            .join("synced.cand/evidence/holger-sync.json")
            .exists(),
        "the export carries the sync manifest"
    );
    allow_first_import(BIN, &s.config, &bundle, Some(&policy));
    let t = run(vec![
        os("bundle"),
        os("import"),
        os("--config"),
        s.config.clone().into(),
        os("--bundle"),
        bundle.clone().into(),
        os("--policy"),
        policy.clone().into(),
        os("--ack-out"),
        stick.clone().into(),
    ]);
    assert!(t.contains("LANDED") && t.contains("ACK "), "{t}");
    let ack1 = stick.join("holger-sync-ack-onprem-1.json");
    assert!(ack1.exists(), "{t}");
    let t = run(vec![
        os("sync"),
        os("ack"),
        os("--config"),
        o.config.clone().into(),
        os("--ack"),
        ack1.into(),
    ]);
    assert!(t.contains("ACK 'onprem' generation 1"), "{t}");

    // 2. A is invalidated on the online side; the metadata travels on the stick alone.
    let t = run(vec![
        os("sync"),
        os("invalidate"),
        os("--config"),
        o.config.clone().into(),
        os("--coordinate"),
        os(WHEEL_A),
        os("--reason"),
        os("malware"),
        os("--detail"),
        os("found after its approval"),
    ]);
    assert!(
        t.contains("INVALIDATED inv-2-") && t.contains(&format!("HELD here {WHEEL_A}")),
        "{t}"
    );
    // The manifest's generation is the newest: the invalidation's (2), or a later one when
    // the AVAILABLE set grew since (here: B, which no bundle carried, joins generation 3).
    let t = run(vec![
        os("sync"),
        os("manifest"),
        os("--config"),
        o.config.clone().into(),
        os("--out"),
        stick.clone().into(),
    ]);
    let manifest = PathBuf::from(
        t.lines()
            .find_map(|l| l.strip_prefix("MANIFEST "))
            .unwrap_or_else(|| panic!("{t}"))
            .trim(),
    );
    assert!(manifest.starts_with(&stick) && manifest.exists(), "{t}");
    let generation: u64 = manifest
        .file_stem()
        .unwrap()
        .to_string_lossy()
        .rsplit('-')
        .next()
        .unwrap()
        .parse()
        .unwrap();
    assert!(generation >= 2, "{t}");
    let t = run(vec![
        os("sync"),
        os("apply"),
        os("--config"),
        s.config.clone().into(),
        os("--manifest"),
        manifest.into(),
        os("--ack-out"),
        stick.clone().into(),
    ]);
    assert!(t.contains(&format!("HELD {WHEEL_A}")), "{t}");
    {
        let mut h = server_lib::read_ron_config(&s.config).unwrap();
        h.instantiate_backends().unwrap();
        let audit = h.audit_log().unwrap();
        let chain = h.intake_chain(audit).unwrap().unwrap();
        assert_eq!(
            chain.state(&wheel_id(WHEEL_A)),
            ArtifactState::Quarantined,
            "the secure side holds it"
        );
        assert_eq!(
            chain.serve(&wheel_id(WHEEL_A)).unwrap(),
            None,
            "and serves nothing of it"
        );
    }

    // 3. The ack goes back; the online board shows the invalidation reached, no alarm.
    let ack = stick.join(format!("holger-sync-ack-onprem-{generation}.json"));
    let t = run(vec![
        os("sync"),
        os("ack"),
        os("--config"),
        o.config.clone().into(),
        os("--ack"),
        ack.into(),
    ]);
    assert!(
        t.contains(&format!(
            "generation {generation}: 1 invalidation(s) reached now"
        )),
        "{t}"
    );
    let t = run(vec![
        os("sync"),
        os("board"),
        os("--config"),
        o.config.clone().into(),
    ]);
    let b: serde_json::Value =
        serde_json::from_str(t.trim()).unwrap_or_else(|e| panic!("{e}: {t}"));
    assert_eq!(b["alarm"], false, "{b}");
    assert_eq!(b["downstreams"][0]["state"], "current", "{b}");
    assert_eq!(b["downstreams"][0]["invalidations_reached"], 1, "{b}");
}

/// The admin's first-import authorization on the secure side (lane HOL-CDS, NCSC gap 2), for
/// the vendor, product and sequence of `bundle` (verified under `policy`). Its refusal (a
/// floor that already stands) is the import's business, so it is not asserted here.
fn allow_first_import(bin: &str, config: &Path, bundle: &Path, policy: Option<&Path>) {
    let mut c = Command::new(bin);
    c.args(["bundle", "allow-first-import", "--config"])
        .arg(config)
        .arg("--from-bundle")
        .arg(bundle);
    if let Some(p) = policy {
        c.arg("--policy").arg(p);
    }
    let _ = c
        .env_remove("ESTATE_CLOCK")
        .env_remove("ESTATE_CLOCK_KEY")
        .output();
}