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")
})
}