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

nordisk/gunnar

installtest/tests/secure_boot.rs

//! **gunnar's Secure Boot arming** — the firmware runs GUNNAR's signed chain,
//! and refuses everything else.
//!
//! # What was missing, measured
//!
//! sign/enroll references, 2026-08-14: **holger 22** (armed, KVM-proven), korp 0,
//! **gunnar 8 — and all eight are machinery, none of them a product**. gunnar's
//! appliance boots printing `signature-over-root: NOT-ENROLLED — integrity only,
//! no authenticity` and its EFI payloads are unsigned. On hardware that ships
//! with Secure Boot on — the default for every laptop and most servers a defence
//! customer buys — an unsigned ESP is refused with *Access Denied* and there is
//! no boot and no diagnostic. Signing is therefore not polish: it is whether the
//! product installs at all.
//!
//! # The chain under test IS gunnar's chain
//!
//! `.nornir/build-variants.toml` states it as a fact about the artifact rather
//! than a preference: "UEFI ONLY … the sole boot path is korp-boot's EFI stage1
//! on the ESP". So the PEs an enforcing firmware verifies on a gunnar appliance
//! are exactly the two this test signs — the `korp-boot` first stage, and the
//! image it hands off to with the firmware's own `LoadImage`/`StartImage` (which
//! is why BOTH must be signed: signing only `BOOTX64.EFI` yields an appliance
//! that starts and then dies at the handoff, reading exactly like a broken
//! kernel).
//!
//! The chainloaded image here is korp-installer's `korp-stage2-marker` fixture
//! standing in for the kernel, and the disk is laid by the production
//! `run_install` path. Reused, never twinned: `korp_installer` owns the
//! pure-Rust key ceremony, the Authenticode signer and the varstore enroller,
//! and gunnar's job is to be ARMED with them, not to grow a second copy.
//!
//! # Three verdicts, and the third one is the new one
//!
//! 1. **Vetra DEV signed → RUNS.** A generated identity is enrolled as PK + KEK
//!    + db into a copy of the real factory (Setup-Mode) OVMF varstore, both PEs
//!    are signed with it, and an enforcing firmware boots the chain.
//! 2. **The SAME chain unsigned → REFUSED.** `Access Denied`, and no first-stage
//!    banner at all. This is the revert check baked in: if the enrollment were
//!    cosmetic or the firmware were not really enforcing, this case would boot
//!    too, and verdict 1 would mean nothing.
//! 3. **Signed by a DIFFERENT generated identity → REFUSED.** korp's own proof
//!    covers unsigned and signed-then-tampered; neither of those can tell a
//!    firmware that checks the SIGNER from one that merely checks that a
//!    signature is present and well formed over the bytes. A second, valid,
//!    self-signed identity that was never enrolled is the case that can — and it
//!    is the case a real attacker has, because minting a key is free.
//!
//! # No key is ever committed
//!
//! Every key in this file is generated inside the test and dies with the scratch
//! directory. There is no `.key`, `.pem` or `.der` in this repository and there
//! must never be one — the production identity's custody is a project decision
//! and no code changes when it is swapped in (`SigningIdentity` is the same type
//! either way).
//!
//! ```bash
//! cargo test --manifest-path installtest/Cargo.toml --test secure_boot \
//!     -- --ignored --nocapture
//! ```

use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};

use korp_installer::firststage::{build_first_stage, HANDOFF_MARKER, STAGE1_BANNER};
use korp_installer::secureboot::{enroll_platform_keys, is_signed, sign_pe, SigningIdentity};
use korp_installer::{run_install, InstallPlan, PayloadSource, Serial};

/// The chainloaded image's success line — proof the firmware verified the
/// SECOND PE too, not just the one it loaded from the ESP fallback path.
const STAGE2_ALIVE: &str = "KORP-STAGE2: ALIVE cmdline=";
/// What an enforcing firmware says when it refuses an image.
const ACCESS_DENIED: &str = "Access Denied";
/// The identity a site would hold. Generated here, never committed.
const VETRA_DEV_SUBJECT: &str = "Vetra DEV Secure Boot — gunnar appliance (NOT FOR PRODUCTION)";
/// A second, equally valid identity that was NEVER enrolled — the attacker's.
const STRANGER_SUBJECT: &str = "NOT Vetra — an identity the firmware never trusted";

/// The installed cmdline the proof lays down. It is gunnar's declared installer
/// cell's, trimmed to what a marker stage 2 can echo back, and it carries
/// `GUNNAR_APPLIANCE=1` on purpose: the two things this repository armed in the
/// same pass are the identity ceremony and the signature over the chain that
/// starts it, and a boot that proves one should show the other travelling.
const GUNNAR_CMDLINE: &str =
    "console=ttyS0 korp.appliance=lean GUNNAR_DATA_ROOT=/data GUNNAR_APPLIANCE=1 korp.entry=gunnar";

// ── the heavy proof ─────────────────────────────────────────────────────────

#[test]
#[ignore = "heavy: generates two identities, enrolls one into a real OVMF varstore, and boots gunnar's chain three times under Secure-Boot-enforcing OVMF"]
fn gunnars_signed_chain_runs_and_the_firmware_refuses_both_unsigned_and_a_stranger() {
    let qemu = preflight_qemu();
    let secboot_code = preflight_secboot_firmware();
    let factory_vars = preflight_factory_varstore();

    let tmp = scratch("gunnar-sb");
    let root = tmp.path();

    // ── 1. two identities, both generated HERE, both valid, ONE enrolled ──
    let vetra = SigningIdentity::self_signed(VETRA_DEV_SUBJECT, 2048)
        .expect("generate the Vetra DEV signing identity");
    let stranger = SigningIdentity::self_signed(STRANGER_SUBJECT, 2048)
        .expect("generate the stranger identity");
    assert_ne!(
        vetra.cert_der(),
        stranger.cert_der(),
        "the two identities must genuinely differ, or verdict 3 tests nothing — a \
         keygen that returned a constant would make the stranger case pass while \
         the firmware happily trusted it"
    );

    // ── 2. enroll ONLY Vetra as PK + KEK + db in a copy of the factory store ──
    let factory = std::fs::read(&factory_vars).expect("read the factory varstore");
    let enrolled =
        enroll_platform_keys(&factory, vetra.cert_der()).expect("enroll the Vetra DEV key");
    assert_eq!(
        enrolled.len(),
        factory.len(),
        "the varstore must keep its exact flash size or the firmware will not map it"
    );

    // ── 3. gunnar's two PEs, built once and signed three ways ──
    let stage1 = std::fs::read(
        build_first_stage(&root.join("stage1-target")).unwrap_or_else(|e| panic!("{e:#}")),
    )
    .expect("read korp-boot, gunnar's declared first stage");
    let stage2 = std::fs::read(build_stage2(&root.join("stage2-target")))
        .expect("read the chainload target");
    assert!(
        !is_signed(&stage1) && !is_signed(&stage2),
        "the fixtures must start UNSIGNED, or the unsigned verdict below is a \
         tautology sitting on an already-signed image"
    );

    let signed_disk = root.join("gunnar-signed.raw");
    install_disk(
        &signed_disk,
        &sign_pe(&stage1, &vetra).expect("sign the first stage"),
        &sign_pe(&stage2, &vetra).expect("sign the chainload target"),
        root,
    );
    let unsigned_disk = root.join("gunnar-unsigned.raw");
    install_disk(&unsigned_disk, &stage1, &stage2, root);
    let stranger_disk = root.join("gunnar-stranger.raw");
    install_disk(
        &stranger_disk,
        &sign_pe(&stage1, &stranger).expect("sign with the stranger"),
        &sign_pe(&stage2, &stranger).expect("sign with the stranger"),
        root,
    );

    // ── VERDICT 1: the signed chain RUNS ──
    let signed_log = boot_secure(
        &qemu,
        &secboot_code,
        &fresh_vars(root, &enrolled, "signed"),
        &signed_disk,
        &root.join("signed-serial.log"),
        STAGE2_ALIVE,
        Duration::from_secs(180),
    );
    eprintln!("─── gunnar SIGNED (Vetra DEV enrolled, SB enforcing) ───\n{signed_log}\n───");
    assert!(
        signed_log.contains(STAGE1_BANNER),
        "the firmware VERIFIED and ran gunnar's signed first stage; log:\n{signed_log}"
    );
    assert!(
        signed_log.contains(HANDOFF_MARKER),
        "the signed first stage chainloaded on:\n{signed_log}"
    );
    assert!(
        signed_log.contains(STAGE2_ALIVE),
        "the firmware verified the CHAINLOADED image too — LoadImage enforces db, \
         so signing only BOOTX64.EFI dies here and reads like a broken kernel:\n{signed_log}"
    );
    assert!(
        signed_log.contains("GUNNAR_APPLIANCE=1"),
        "gunnar's own cmdline must survive the verified chain — the identity \
         ceremony's gate travels on the boot line the signature protects:\n{signed_log}"
    );
    assert!(
        !signed_log.contains(ACCESS_DENIED),
        "nothing in the signed chain was refused:\n{signed_log}"
    );

    // ── VERDICT 2: the SAME chain, unsigned, is REFUSED ──
    let unsigned_log = boot_secure(
        &qemu,
        &secboot_code,
        &fresh_vars(root, &enrolled, "unsigned"),
        &unsigned_disk,
        &root.join("unsigned-serial.log"),
        ACCESS_DENIED,
        Duration::from_secs(180),
    );
    eprintln!("─── gunnar UNSIGNED (same firmware, same key) ───\n{unsigned_log}\n───");
    assert!(
        !unsigned_log.contains(STAGE1_BANNER),
        "an UNSIGNED first stage must NEVER run under Secure Boot:\n{unsigned_log}"
    );
    assert!(
        !unsigned_log.contains(STAGE2_ALIVE),
        "and nothing behind it may run either:\n{unsigned_log}"
    );
    assert!(
        unsigned_log.contains(ACCESS_DENIED),
        "the firmware must SAY why it refused, or this test cannot tell a refusal \
         from a guest that simply never printed:\n{unsigned_log}"
    );

    // ── VERDICT 3: signed by a STRANGER, refused ──
    // The case korp's proof does not cover. Unsigned and tampered both fail a
    // firmware that merely checks "is there a well-formed signature over these
    // bytes"; only a valid signature by an UNENROLLED signer distinguishes that
    // from a firmware that checks WHO signed. Minting a key is free, so this is
    // the case a real attacker has.
    let stranger_log = boot_secure(
        &qemu,
        &secboot_code,
        &fresh_vars(root, &enrolled, "stranger"),
        &stranger_disk,
        &root.join("stranger-serial.log"),
        ACCESS_DENIED,
        Duration::from_secs(180),
    );
    eprintln!("─── gunnar signed by a STRANGER (never enrolled) ───\n{stranger_log}\n───");
    assert!(
        !stranger_log.contains(STAGE1_BANNER),
        "a VALIDLY SIGNED image whose signer was never enrolled must not run — if it \
         does, the firmware is checking that a signature EXISTS, not who made it, \
         and the enrollment buys nothing:\n{stranger_log}"
    );
    assert!(
        !stranger_log.contains(STAGE2_ALIVE),
        "and nothing behind it may run either:\n{stranger_log}"
    );
    assert!(
        stranger_log.contains(ACCESS_DENIED),
        "the firmware must refuse the stranger by name:\n{stranger_log}"
    );

    eprintln!(
        "GUNNAR ARMED: pure-Rust key ceremony → PK/KEK/db enrollment of a Vetra DEV \
         identity → pure-Rust Authenticode over BOTH PEs → Secure-Boot-ENFORCING OVMF \
         ran gunnar's chain, and refused the same chain unsigned AND the same chain \
         signed by an identity it had never been given"
    );
}

// ── fast guards (no QEMU; these run in an ordinary `cargo test`) ─────────────

/// **The signer actually signs, and signing does not disturb what the firmware
/// hashes.** Asserted on the BYTES through `is_signed`, never on `sign_pe`
/// returning `Ok`: a signer that returned its input unchanged passes the latter.
#[test]
fn gunnars_first_stage_signs_and_a_second_identity_produces_different_bytes() {
    // The scratch dir is BOUND, not used inline: a `TempDir` temporary is
    // dropped at the end of the statement that made it, taking the freshly
    // built `bootx64.efi` with it. Measured 2026-08-14 — the first run of this
    // test failed with `read the first stage: NotFound` on a build that had
    // just reported success.
    let tmp = scratch("gunnar-sign");
    let Ok(efi) = build_first_stage(&tmp.path().join("target")) else {
        eprintln!("SKIP: the first stage would not build (see the PREFLIGHT message)");
        return;
    };
    let stage1 = std::fs::read(&efi).expect("read the first stage");
    assert!(
        !is_signed(&stage1),
        "gunnar's first stage ships unsigned today"
    );

    let vetra = SigningIdentity::self_signed(VETRA_DEV_SUBJECT, 2048).expect("vetra");
    let stranger = SigningIdentity::self_signed(STRANGER_SUBJECT, 2048).expect("stranger");

    let by_vetra = sign_pe(&stage1, &vetra).expect("sign with vetra");
    let by_stranger = sign_pe(&stage1, &stranger).expect("sign with the stranger");

    assert!(
        is_signed(&by_vetra),
        "the Vetra-signed PE must carry a signature"
    );
    assert!(is_signed(&by_stranger), "so must the stranger-signed one");
    assert_ne!(
        by_vetra, by_stranger,
        "two identities must produce different signed images, or the boot proof's \
         third verdict is comparing an image with itself"
    );
    assert!(
        by_vetra.len() > stage1.len() && by_stranger.len() > stage1.len(),
        "a signature is bytes ADDED to the PE; an unchanged length means the signer \
         did nothing"
    );
}

/// **The enrollment lands, and it lands the Vetra key rather than any key.**
#[test]
fn enrolling_vetra_puts_pk_kek_and_db_in_a_real_factory_varstore() {
    let Some(factory_vars) = factory_varstore() else {
        eprintln!("SKIP: no factory OVMF varstore on this box (apt install ovmf)");
        return;
    };
    let factory = std::fs::read(&factory_vars).expect("read the factory varstore");
    let vetra = SigningIdentity::self_signed(VETRA_DEV_SUBJECT, 2048).expect("vetra");
    let enrolled = enroll_platform_keys(&factory, vetra.cert_der()).expect("enroll");

    assert_eq!(
        enrolled.len(),
        factory.len(),
        "the store must keep its exact flash size"
    );
    assert_ne!(
        enrolled, factory,
        "an enrollment that changed nothing is a factory store wearing a customer's \
         name — and it would boot ANYTHING"
    );
    // The applied output: the certificate's own bytes must be findable in the
    // store. A length-and-inequality check alone would pass on a store that had
    // been scribbled on with noise.
    let needle = vetra.cert_der();
    assert!(
        enrolled.windows(needle.len()).any(|w| w == needle),
        "the enrolled store must actually CONTAIN the Vetra certificate"
    );
}

// ── helpers ──────────────────────────────────────────────────────────────────

/// Lay a gunnar appliance disk carrying `boot` at the ESP fallback path and
/// `kernel` at `\vmlinuz`, through the PRODUCTION install path — not a
/// hand-built FAT image, so what boots is what an install produces.
fn install_disk(path: &Path, boot: &[u8], kernel: &[u8], workdir: &Path) {
    let payload = workdir.join("app.znippy");
    if !payload.is_file() {
        std::fs::write(&payload, vec![0x5Au8; 1024 * 1024]).expect("write the app payload");
    }
    let mut plan = InstallPlan::lean(PayloadSource::embedded(&payload));
    plan.esp_mib = 48;
    plan.gpt_seed = 0x6110;
    plan.boot_payload = boot.to_vec();
    plan.vmlinuz = Some(kernel.to_vec());
    plan.start_app = b"KORP-START-PID1".to_vec();
    plan.cmdline = GUNNAR_CMDLINE.to_string();

    let size = 256 * 1024 * 1024u64;
    let mut disk = std::fs::OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .truncate(true)
        .open(path)
        .expect("create the target disk");
    disk.set_len(size).expect("size the target disk");
    let mut serial = Serial::in_memory();
    run_install(&mut disk, size, &plan, &mut serial).unwrap_or_else(|e| panic!("install: {e:#}"));
}

/// A FRESH copy of the enrolled varstore per boot.
///
/// Not a convenience: a varstore is written by the firmware it boots, so reusing
/// one across the three verdicts would let the first boot's state decide the
/// second's, and a refusal could be inherited rather than earned.
fn fresh_vars(root: &Path, enrolled: &[u8], tag: &str) -> PathBuf {
    let p = root.join(format!("OVMF_VARS.{tag}.fd"));
    std::fs::write(&p, enrolled).expect("write a fresh enrolled varstore");
    p
}

/// Boot `disk` under a **Secure-Boot-enforcing** OVMF: the `.secboot` firmware,
/// SMM on, and the pflash marked secure so the varstore is writable only from
/// SMM. That combination is what makes the firmware actually check signatures —
/// the plain OVMF build does not check at all, and a proof run against it would
/// be green for every one of the three disks.
#[allow(clippy::too_many_arguments)]
fn boot_secure(
    qemu: &Path,
    code: &Path,
    vars: &Path,
    disk: &Path,
    log: &Path,
    marker: &str,
    timeout: Duration,
) -> String {
    let mut cmd = Command::new(qemu);
    cmd.args([
        "-machine",
        "q35,accel=kvm:tcg,smm=on",
        "-m",
        "1024",
        "-display",
        "none",
        "-no-reboot",
    ])
    .args(["-global", "driver=cfi.pflash01,property=secure,value=on"])
    .args(["-global", "ICH9-LPC.disable_s3=1"])
    .arg("-drive")
    .arg(format!(
        "if=pflash,format=raw,readonly=on,file={}",
        code.display()
    ))
    .arg("-drive")
    .arg(format!("if=pflash,format=raw,file={}", vars.display()))
    .arg("-drive")
    .arg(format!("file={},format=raw,if=virtio", disk.display()))
    .arg("-serial")
    .arg(format!("file:{}", log.display()));
    let mut child = cmd.spawn().expect("spawn qemu");

    let deadline = Instant::now() + timeout;
    let mut captured = String::new();
    while Instant::now() < deadline {
        if let Ok(bytes) = std::fs::read(log) {
            captured = String::from_utf8_lossy(&bytes).replace('\u{0}', "");
            if captured.contains(marker) {
                break;
            }
        }
        if let Ok(Some(_)) = child.try_wait() {
            break;
        }
        std::thread::sleep(Duration::from_millis(300));
    }
    let _ = child.kill();
    let _ = child.wait();
    if let Ok(bytes) = std::fs::read(log) {
        captured = String::from_utf8_lossy(&bytes).replace('\u{0}', "");
    }
    captured
}

/// Build the chainload target — korp-installer's own `korp-stage2-marker`
/// fixture, by manifest path. Reused rather than twinned: a second no_std marker
/// crate in this repository would be ten lines of `println!` and a whole second
/// UEFI build to keep working.
fn build_stage2(target_dir: &Path) -> PathBuf {
    let manifest = korp_installer_root().join("tests/fixtures/korp-stage2-marker/Cargo.toml");
    assert!(
        manifest.is_file(),
        "PREFLIGHT: the korp-stage2-marker fixture is missing at {} — the sibling \
         edda checkout is what this test's path dependency already resolves through",
        manifest.display()
    );
    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
    let status = Command::new(&cargo)
        .args([
            "build",
            "--release",
            "--target",
            "x86_64-unknown-uefi",
            "--manifest-path",
        ])
        .arg(&manifest)
        .arg("--target-dir")
        .arg(target_dir)
        .status()
        .expect("spawn cargo for the chainload target");
    assert!(
        status.success(),
        "PREFLIGHT: run `rustup target add x86_64-unknown-uefi`"
    );
    target_dir.join("x86_64-unknown-uefi/release/bootx64.efi")
}

/// Where korp-installer's checkout is, derived from THIS crate's manifest dir
/// the same way its path dependency is — one answer, not two.
fn korp_installer_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(Path::parent)
        .expect("installtest has two ancestors")
        .join("edda/crates/korp-installer")
}

fn scratch(tag: &str) -> tempfile::TempDir {
    // A real disk scratch dir. NEVER /tmp on this box — that is RAM, and three
    // 256 MiB disk images plus two UEFI target dirs do not belong in it.
    let base = std::env::var("GUNNAR_TEST_SCRATCH")
        .map(PathBuf::from)
        .unwrap_or_else(|_| {
            PathBuf::from(std::env::var("HOME").unwrap_or_default())
                .join("scratch/gunnar-secure-boot")
        });
    std::fs::create_dir_all(&base).ok();
    tempfile::Builder::new()
        .prefix(tag)
        .tempdir_in(&base)
        .expect("scratch dir")
}

fn which(bin: &str) -> Option<PathBuf> {
    let path = std::env::var_os("PATH")?;
    std::env::split_paths(&path)
        .map(|d| d.join(bin))
        .find(|c| c.is_file())
}

/// A **Secure-Boot-enforcing** OVMF code image. The plain build does not check
/// signatures at all, so resolving to it would make every verdict green.
fn secboot_firmware() -> Option<PathBuf> {
    [
        "/usr/share/OVMF/OVMF_CODE_4M.secboot.fd",
        "/usr/share/OVMF/OVMF_CODE.secboot.fd",
        "/usr/share/edk2/x64/OVMF_CODE.secboot.4m.fd",
    ]
    .iter()
    .map(PathBuf::from)
    .find(|p| p.is_file())
}

/// A **factory** variable store — one still in Setup Mode, with no platform key,
/// so a customer key can be enrolled into it. Deliberately NOT the `.ms.fd`
/// store, which already carries Microsoft's keys and is not ours to speak for.
fn factory_varstore() -> Option<PathBuf> {
    [
        "/usr/share/OVMF/OVMF_VARS_4M.fd",
        "/usr/share/OVMF/OVMF_VARS.fd",
        "/usr/share/edk2/x64/OVMF_VARS.4m.fd",
    ]
    .iter()
    .map(PathBuf::from)
    .find(|p| p.is_file())
}

// Preflight failures PANIC with the fix rather than skipping: a Secure Boot
// proof that silently reported success on a box with no firmware is the exact
// blind green this repository refuses.
fn preflight_qemu() -> PathBuf {
    which("qemu-system-x86_64").unwrap_or_else(|| {
        panic!("PREFLIGHT: MISSING qemu-system-x86_64 — sudo apt install qemu-system-x86")
    })
}
fn preflight_secboot_firmware() -> PathBuf {
    secboot_firmware().unwrap_or_else(|| {
        panic!(
            "PREFLIGHT: MISSING Secure-Boot OVMF (looked for \
             /usr/share/OVMF/OVMF_CODE_4M.secboot.fd) — sudo apt install ovmf"
        )
    })
}
fn preflight_factory_varstore() -> PathBuf {
    factory_varstore().unwrap_or_else(|| {
        panic!("PREFLIGHT: MISSING /usr/share/OVMF/OVMF_VARS_4M.fd — sudo apt install ovmf")
    })
}