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(¢ral);
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();
}