Gunnar

nordisk/heimdall

guests/verify-bundle/README.md

verify-bundle — Heimdall's wasm guest for Holger's update bundles

Heimdall is a defensive sandbox. This guest reads a holger.update-bundle/v1 (.holger.znippy) that came from somewhere nobody trusts, so that the Holger server never parses those bytes itself. Its tests exercise its own containment only.

Bytes in, a verdict and named outputs out; no host access. In order:

  1. the znippy v0.7 container (footer, Arrow IPC index framing walked before arrow reads it, chunk geometry, per-chunk BLAKE3, member names through the path guard, member count, the bomb shape from the declared sizes) — before a blob byte is read;
  2. the control members only, under their own caps;
  3. the release signature (Ed25519 over X.509, pinned or rooted, as Holger signs), the approvals and the two-person rule;
  4. every other member, named by the signed manifest before it is read;
  5. Holger's full verification order (content list both ways, envelope, expiry, evidence, target, anti-rollback);
  6. every DSSE envelope: payload type, then every signature by a pinned signer, and only then the in-toto statement, which must name a payload of this bundle.

Verdict: Accepted (outputs: verify-report.json plus the payload and evidence members) or Refused { code, reason }. emit-file takes one plain file name, so a member path is emitted under an injective name (payload/fw.bin is payload+2ffw.bin; see output_name), and the report lists both. In the wasm export (src/wasm.rs, bound to crates/heimdall-wasm/wit/heimdall.wit) the WIT refusal is one string, so the reason starts with the code in brackets. The one job input is the .holger.znippy itself; everything else arrives as job parameters. Codes are Holger's frozen holger-update-<n> wherever Holger's own verifier refuses the same thing, else heimdall-bundle-<n> (1 parameters, 2 archive, 3 codec, 4 DSSE, 5 DSSE binding, 6 parser panic).

Parameters: a RON VerifyParams document or Heimdall's string pairs (now, policy, product, model, revision, accepted, dsse, emit); see src/params.rs.

OpenZL: znippy compresses members with OpenZL, a C library. With the default feature openzl the guest compiles OpenZL's decoder (vendored in openzl/, built for wasm32 by build.rs against wasi-libc headers; needs clang and the wasi-libc package, see ci/ci.Containerfile) into the module, which still imports nothing but the WIT. A frame is decoded only after its header declares exactly the size the index declares. --no-default-features refuses compressed members by name (heimdall-bundle-3). Details and the open risk: docs/OPENZL-WASM.md.

cargo test --release                       # unit, integration, differential, proptest, and
                                           # tests/wasm_e2e.rs: the component in the real host
cargo build --release --target wasm32-unknown-unknown
cargo +nightly fuzz run archive fuzz/seeds/archive   # also: bundle dsse_envelope params certificate openzl_frame