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/tests/probe_corpus.rs

// SPDX-License-Identifier: AGPL-3.0-only
//! Writes the REAL verify-bundle guest's part of the `heimdall-wasm-host`
//! probe corpus (profiles/heimdall-wasm-host/verify-bundle/) and its runs in
//! profiles/heimdall-wasm-host/workload.toml (between the marker lines): the
//! single-instance guest and the split guest (verifier + decoder, N = 1 and
//! N = 8 decoder instances), signed with a throwaway CA, over a good stored
//! bundle, a good Holger-packed OpenZL bundle, a many-chunk OpenZL bundle, a
//! tampered bundle and two bombs. The rest of the corpus (WAT guests) is
//! crates/heimdall-cli/tests/profile_corpus.rs. Heimdall is a defensive
//! sandbox; the bundles attack only the guest's own checks.
//!
//! Run by hand when the guest or the corpus changes, then probe and regenerate:
//!   HEIMDALL_WRITE_VB_CORPUS=$PWD/../../profiles/heimdall-wasm-host \
//!     cargo test --release --test probe_corpus -- --ignored
//!   heimdall profile probe heimdall-wasm-host && heimdall profile gen heimdall-wasm-host
#![cfg(feature = "openzl")]

mod common;
use common::guests::{component, Build, Lab};
use common::zwriter::{openzl_frame, RawArchive};
use common::*;

use std::path::Path;

const PAYLOAD: &str = "payload/firmware.bin";
const BEGIN: &str = "# BEGIN verify-bundle runs: written by guests/verify-bundle/tests/probe_corpus.rs; do not edit by hand";
const END: &str = "# END verify-bundle runs";

/// The policy the CLI tests use for the guest (tests/cli_split.rs), with
/// `decoders` decoder instances.
fn policy_text(decoders: u32) -> String {
    format!(
        "# Heimdall is a defensive sandbox: probe corpus policy (verify-bundle guest).\n\
         version = 1\n[job]\nkind = \"wasm\"\n[limits]\n\
         fuel = 20000000000\nwall_time_ms = 120000\nmemory_bytes = 536870912\n\
         instances = 16\ntables = 16\nmemories = 4\ndecoders = {decoders}\n"
    )
}

/// The bundles, all of ONE fixture (so one policy parameter serves every
/// run): name, bytes, expected exit (0 accepted, 4 refused).
fn bundles(fx: &Fixture) -> Vec<(&'static str, Vec<u8>, i32)> {
    let mut tampered = fx.members.clone();
    tampered.get_mut(PAYLOAD).unwrap()[0] ^= 1;
    let bomb = openzl_frame(&vec![0u8; 256 << 20]);
    // A frame of 256 MiB of zeros behind the payload's small declared size:
    // refused when the decoder reads the frame header, nothing allocated.
    let lying = {
        let mut a = RawArchive::from_members_openzl(&fx.members, 1 << 20);
        let off = a.blobs.len() as u64;
        a.blobs.extend_from_slice(&bomb);
        let row = a.row_mut(PAYLOAD);
        row.blob_offset = off;
        row.blob_size = bomb.len() as u64;
        a.encode()
    };
    // The same frame declared honestly: refused by the expansion ratio
    // before anything is decoded.
    let honest = {
        let mut a = RawArchive::from_members_openzl(&fx.members, 1 << 20);
        let off = a.blobs.len() as u64;
        a.blobs.extend_from_slice(&bomb);
        let row = a.row_mut(PAYLOAD);
        row.blob_offset = off;
        row.blob_size = bomb.len() as u64;
        row.uncompressed_size = 256 << 20;
        a.encode()
    };
    vec![
        ("stored", fx.pack(), 0),
        ("openzl", pack_holger(&fx.members), 0),
        (
            "manychunk",
            RawArchive::from_members_openzl(&fx.members, 4096).encode(),
            0,
        ),
        ("tampered", pack_holger(&tampered), 4),
        ("bomb-lying", lying, 4),
        ("bomb-ratio", honest, 4),
    ]
}

fn signed(lab: &Lab, out: &Path, name: &str, bytes: &[u8]) {
    let m = lab.signed(name, bytes);
    for ext in ["", ".p7s", ".blake3"] {
        let from = format!("{}{ext}", m.display());
        std::fs::copy(&from, out.join(format!("{name}.wasm{ext}"))).unwrap();
    }
}

fn quote(s: &str) -> String {
    // A TOML basic string.
    let mut q = String::from("\"");
    for c in s.chars() {
        match c {
            '"' => q.push_str("\\\""),
            '\\' => q.push_str("\\\\"),
            '\n' => q.push_str("\\n"),
            '\t' => q.push_str("\\t"),
            c if (c as u32) < 0x20 => q.push_str(&format!("\\u{:04x}", c as u32)),
            c => q.push(c),
        }
    }
    q.push('"');
    q
}

#[test]
#[ignore = "writes the checked-in probe corpus; run by hand"]
fn writes_the_verify_bundle_part_of_the_heimdall_wasm_host_probe_corpus() {
    let Some(dir) = std::env::var_os("HEIMDALL_WRITE_VB_CORPUS") else {
        panic!("set HEIMDALL_WRITE_VB_CORPUS=<profiles/heimdall-wasm-host>");
    };
    let dir = Path::new(&dir);
    let workload = dir.join("workload.toml");
    let toml = std::fs::read_to_string(&workload).expect("workload.toml of heimdall-wasm-host");
    let out = dir.join("verify-bundle");
    let _ = std::fs::remove_dir_all(&out);
    std::fs::create_dir_all(&out).unwrap();

    let lab = Lab::new();
    std::fs::copy(lab.dir.path().join("root.der"), out.join("root.der")).unwrap();
    signed(&lab, &out, "job", component(Build::Job));
    signed(&lab, &out, "verifier", component(Build::Verifier));
    signed(&lab, &out, "decoder", component(Build::Decoder));
    for n in [1, 8] {
        std::fs::write(out.join(format!("policy-n{n}.toml")), policy_text(n)).unwrap();
    }

    // One fixture with a payload big enough for many 4 KiB OpenZL chunks.
    let fx = build(Spec {
        payloads: vec![(
            "firmware.bin".into(),
            (0..1_200_000u32)
                .map(|i| (i % 251) as u8 ^ (i >> 9) as u8)
                .collect(),
            "application/octet-stream".into(),
        )],
        ..Spec::default()
    });
    let mut runs = String::new();
    runs.push_str(BEGIN);
    runs.push('\n');
    let params = [
        ("now", t0().to_rfc3339()),
        ("policy", fx.policy.to_ron().unwrap()),
    ];
    for (name, bytes, exit) in bundles(&fx) {
        let input = out.join(format!("in-{name}"));
        std::fs::create_dir_all(&input).unwrap();
        std::fs::write(input.join("bundle.holger.znippy"), &bytes).unwrap();
        for (mode, module, decoders) in [
            ("single", "job", 1),
            ("split_n1", "verifier", 1),
            ("split_n8", "verifier", 8),
        ] {
            let mut args: Vec<String> = [
                "run",
                "--job",
                "wasm",
                "--module",
                &format!("{{dir}}/verify-bundle/{module}.wasm"),
                "--trust-root",
                "{dir}/verify-bundle/root.der",
                "--in",
                &format!("{{dir}}/verify-bundle/in-{name}"),
                "--out",
                "{tmp}/out",
                "--policy",
                &format!("{{dir}}/verify-bundle/policy-n{decoders}.toml"),
            ]
            .iter()
            .map(|s| s.to_string())
            .collect();
            if module == "verifier" {
                args.push("--decoder".into());
                args.push("{dir}/verify-bundle/decoder.wasm".into());
            }
            for (k, v) in &params {
                args.push("--param".into());
                args.push(format!("{k}={v}"));
            }
            let run_name = format!(
                "verify_bundle_{mode}_{}_is_{}",
                name.replace('-', "_"),
                if exit == 0 { "accepted" } else { "refused" }
            );
            runs.push_str("\n[[run]]\n");
            runs.push_str(&format!("name = {}\n", quote(&run_name)));
            if exit != 0 {
                runs.push_str("error_path = true\n");
            }
            let list: Vec<String> = args.iter().map(|a| quote(a)).collect();
            runs.push_str(&format!("args = [{}]\n", list.join(", ")));
            runs.push_str(&format!("expect_exit = {exit}\n"));
        }
    }
    runs.push_str(END);
    runs.push('\n');

    // Replace the marked block, or append it.
    let new = match (toml.find(BEGIN), toml.find(END)) {
        (Some(a), Some(b)) => {
            let end = b + END.len() + toml[b + END.len()..].find('\n').map_or(0, |i| i + 1);
            format!("{}{runs}{}", &toml[..a], &toml[end..])
        }
        _ => format!("{}\n{runs}", toml.trim_end_matches('\n').to_string() + "\n"),
    };
    std::fs::write(&workload, new).unwrap();
    std::fs::write(
        out.join("README.md"),
        "# heimdall-wasm-host probe corpus: the verify-bundle guest\n\n\
         Heimdall is a defensive sandbox. Generated by\n\
         `guests/verify-bundle/tests/probe_corpus.rs`: the real guest (single, and\n\
         split = verifier + decoder), signed with a throwaway test CA (`root.der`),\n\
         and Holger bundles made with Holger's own library; the runs are in\n\
         `../workload.toml` between the verify-bundle marker lines.\n",
    )
    .unwrap();
}