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

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.

Split guest: --no-default-features --features split builds the VERIFIER (world verifier, no C), --features decoder the DECODER (world decoder, imports nothing); the host runs many decoder instances in parallel and the verifier never shares memory with one. Same verdicts as the single-instance guest; see docs/GATLING-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

Release components: how Holger pins and loads them

A release ships three components (not core modules; the host refuses a core module), each with its .wasm.blake3 and its .wasm.p7s:

File Build Loaded with
heimdall-verify-bundle.wasm default features (world job, OpenZL inside) Host::load_pinned
heimdall-verify-bundle-verifier.wasm --no-default-features --features split (world verifier, no C) Host::load_split_pinned, after its decoder
heimdall-verify-bundle-decoder.wasm --features decoder (world decoder, imports nothing) Host::load_decoder_pinned

Built by cargo run --release --locked --example dist -- <out-dir> [<target-root>] in the toolchain image with wasi-libc (ci/ci.Containerfile; rustc 1.97.1). The build is reproducible: tools/guest_build.rs remaps every Rust source path (--remap-path-prefix): each dependency, wherever cargo found it (registry, git, a vendor directory, versioned or not), becomes /stage/<name>-<version> — the release build's own layout, so a plain dist on any machine builds the release's bytes — and the guest, CARGO_HOME, the sysroot and the target directory get fixed names; build.rs maps the C sources' paths away (-ffile-prefix-map). Two builds from different checkout paths, cargo homes (vendored or not) and target directories give byte-identical files (tests/release_dist.rs builds all three modules from two other checkouts, one vendored, on every CI run). The same recipe (tools/guest_build.rs) builds what every test in this directory loads, and dist writes exactly those bytes.

Signed the way Holger's own wasm plugins are signed: .wasm.p7s is a detached CMS over the module's BLAKE3-256 by the forge's plugin-signing certificate (CN=nordisk plugin signing, dwarves trust/plugin-signing.der, sha256 6b08efec…f159f1), issued by the korp root (korp-root-ca.der, sha256 6b19bf47…570b16), which is the root Holger already trusts for its plugins (ron_plugin_trust_roots, holger-recipe trust/korp-root-ca.pem). cargo run --release --locked --example sign -- <material-dir> <dist-dir> signs all three with the material dwarves sign-cert issue --purpose plugin keeps beside the root seed, verifies each signature against the root and loads all three, pinned, in the real host; any failure leaves no signature behind. The release's SLSA provenance (the owner's YubiKey key) names the three files by sha256 as its subjects.

Holger pins and loads them like this:

  1. Pin the three BLAKE3 digests from the release (the .wasm.blake3 files, cross-checked against the SLSA statement's sha256 subjects) in Holger's own configuration, not only beside the files: the sidecar travels with the module, the caller pin does not.
  2. Give the host the korp root as its only trust root, as DER (Holger already writes the .der of its staged korp-root-ca.pem).
  3. Load, single instance: heimdall_run::wasm::load_guest(&job, &[root_der], Some(job_b3), &limits) (or Host::load_pinned); split: heimdall_run::wasm::load_split_guest(&verifier, Some(verifier_b3), &decoder, Some(decoder_b3), &[root_der], &limits) (the decoder is loaded first). As a job: JobKind::Wasm { module: GuestRef { path, blake3 }, decoder: Some(GuestRef { .. }) } through WasmJobRunner, or heimdall run --job wasm --module <verifier.wasm> --pin-blake3 <hex> --decoder <decoder.wasm> --decoder-pin-blake3 <hex> --trust-root korp-root-ca.der ... (docs/CLI.md).
  4. Any refusal is a load error by name (heimdall-wasm-*, decoder refused: ... for the decoder); Holger refuses the bundle, it never falls back to parsing it itself.