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

nordisk/gunnar

crates/gunnar-growbench/src/todo_sign.rs

//! **TD10 — a SIGNED medium** (`gunnar/.nornir/root-key-boot-proof-for-t3.md`, T10 `2b7d568`).
//!
//! ```text
//!   gunnar-growbench --scenarios TD10 --iso <medium composed with the real root key>
//!       --td10-pubkey <64 hex> [--td10-other-app <a second compose's app.znippy>] [--dwarves BIN]
//! ```
//!
//! Step 2 installs the medium on the OVMF rig and boots it: the console and the ESP's
//! `BOOTLOG.TXT` must say `require_signed=YES`, and the signature leg must be VERIFIED
//! (`signature-over-root=PASS`), not `NOT-ENROLLED`. Step 3 is the three tamper arms, each on the
//! same installed disk and each undone before the next:
//!
//! * **A** — one byte of `korp/app.sig` flipped: the machine must halt ON THE SIGNATURE LEG.
//! * **B** — `root.pub` and `app.sig` deleted and `korp.require_signed` stripped from the ESP's
//!   `korp/cmdline`: the policy is closed by default and baked into the signed first stage, so it
//!   must refuse.
//! * **C** ★ — a DIFFERENT payload written to P2, its length into `korp/app.len` and its blake3
//!   into `korp/app.blake3`, `root.pub`/`app.sig` untouched: integrity AGREES and the machine must
//!   STILL halt, on the signature. A boot here is a stop-everything result.
//!
//! Every arm is read off the serial AND off `BOOTLOG.TXT`; which one said it is recorded. The
//! seed never comes near this program: the medium carries only the public key and the signature.

use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;

use korp_installer::kvmproof;

use super::todo_drill::{flag, log_line, one_line_pub as one_line, unrun, verdict};
use super::{boot_failed, print_boot, Opts, Session, Verdict};
use crate::rig::{Rig, RigSpec};

pub const TD10_NAME: &str = "a SIGNED medium: boots with require_signed=YES and the signature leg VERIFIED; a flipped app.sig, a stripped anchor, and a foreign payload with an agreeing blake3 all HALT on the signature";

// ── the installed ESP, read and written as FAT ──────────────────────────────

fn esp_fs(boot: &Path) -> Result<(std::fs::File, u64, u64), String> {
    let (first, last) = kvmproof::gpt_entry(boot, 0);
    let f = std::fs::OpenOptions::new()
        .read(true)
        .write(true)
        .open(boot)
        .map_err(|e| format!("{}: {e}", boot.display()))?;
    Ok((f, first * 512, (last + 1 - first) * 512))
}

/// `Some(bytes)` of an ESP file, `None` when it is absent.
pub fn esp_read(boot: &Path, path: &str) -> Option<Vec<u8>> {
    let (first, last) = kvmproof::gpt_entry(boot, 0);
    kvmproof::read_fat_file(boot, first, last, path)
}

/// Write (`Some`) or delete (`None`) one file under `korp/` on the installed ESP.
pub fn esp_put(boot: &Path, name: &str, bytes: Option<&[u8]>) -> Result<(), String> {
    let (mut f, off, len) = esp_fs(boot)?;
    {
        let win = korp_installer::esp::PartitionWindow::new(&mut f, off, len);
        let fs = fatfs::FileSystem::new(win, fatfs::FsOptions::new())
            .map_err(|e| format!("ESP is not FAT: {e}"))?;
        {
            let dir = fs
                .root_dir()
                .open_dir("korp")
                .map_err(|e| format!("ESP korp/: {e}"))?;
            match bytes {
                Some(b) => {
                    let mut file = dir
                        .create_file(name)
                        .map_err(|e| format!("ESP korp/{name}: {e}"))?;
                    file.truncate().map_err(|e| e.to_string())?;
                    file.write_all(b).map_err(|e| e.to_string())?;
                    file.flush().map_err(|e| e.to_string())?;
                }
                None => {
                    if let Err(e) = dir.remove(name) {
                        return Err(format!("ESP korp/{name}: remove: {e}"));
                    }
                }
            }
        }
        fs.unmount().map_err(|e| format!("unmount: {e}"))?;
    }
    f.sync_all().map_err(|e| e.to_string())?;
    let back = esp_read(boot, &format!("korp/{name}"));
    match (bytes, back) {
        (Some(b), Some(r)) if r == b => Ok(()),
        (None, None) => Ok(()),
        (w, r) => Err(format!(
            "korp/{name} did not take: wrote {:?} B, read back {:?} B",
            w.map(|b| b.len()),
            r.map(|b| b.len())
        )),
    }
}

/// The byte range of GPT entry `index` (P2 = 1).
fn part_range(boot: &Path, index: usize) -> (u64, u64) {
    let (first, last) = kvmproof::gpt_entry(boot, index);
    (first * 512, (last + 1) * 512)
}

// ── what a boot said ────────────────────────────────────────────────────────

struct Said {
    serial: String,
    bootlog: String,
}

impl Said {
    fn has(&self, needle: &str) -> Option<&'static str> {
        if self.serial.contains(needle) {
            Some("serial")
        } else if self.bootlog.contains(needle) {
            Some("BOOTLOG.TXT")
        } else {
            None
        }
    }
    /// The lines that decide an arm, from either sink.
    fn lines(&self, keys: &[&str]) -> Vec<String> {
        let pick = |t: &str, src: &str| -> Vec<String> {
            t.lines()
                .filter(|l| keys.iter().any(|k| l.contains(k)))
                .map(|l| format!("[{src}] {}", l.trim()))
                .collect()
        };
        let mut v = pick(&self.serial, "serial");
        v.extend(pick(&self.bootlog, "BOOTLOG"));
        v.dedup();
        v.truncate(8);
        v
    }
}

/// The ESP's BOOTLOG.TXT grows across boots: keep only what THIS boot appended.
fn bootlog_since(boot: &Path, before: usize) -> String {
    let all = esp_read(boot, "BOOTLOG.TXT")
        .map(|b| String::from_utf8_lossy(&b).into_owned())
        .unwrap_or_default();
    if all.len() >= before {
        all[before..].to_string()
    } else {
        all // rotated to BOOTLOG.OLD: this boot's lines are all of it
    }
}

fn bootlog_len(boot: &Path) -> usize {
    esp_read(boot, "BOOTLOG.TXT").map(|b| b.len()).unwrap_or(0)
}

const SIG_KEYS: &[&str] = &[
    "require_signed",
    "signature-over-root",
    "root signature",
    "blake3",
    "self-test",
    "selftest",
    "REFUS",
    "refus",
    "halt",
    "HALT",
    "app.len",
];

// ── the cell ────────────────────────────────────────────────────────────────

pub fn td10(o: &Opts) -> Verdict {
    let work = o.work.join("TODO-TD10");
    let _ = std::fs::create_dir_all(&work);
    let Some(pubkey) = flag("--td10-pubkey") else {
        return unrun("TD10", TD10_NAME, "pass --td10-pubkey <the enrolled 64-hex public key> (dwarves root-key show); the medium must have been composed with $KORP_ROOT_SIGNING_KEY");
    };
    let medium_sha =
        crate::data::sha256_file(&o.iso).unwrap_or_else(|e| format!("unreadable: {e}"));
    let mut rows: Vec<String> = Vec::new();
    let mut red: Vec<String> = Vec::new();
    let r = (|| -> Result<(), String> {
        let spec = RigSpec {
            iso: o.iso.clone(),
            work: work.join("box"),
            boot_gib: 24,
            member_gib: 6,
            members: 4,
            mem_mb: o.mem_mb,
            smp: o.smp,
            settle_secs: 5,
            explicit_blockdevs: false,
        };
        let mut rig = Rig::create(spec)?;
        rig.install()?;
        // The install's own console: the medium's policy line and what it enrolled.
        let install_serial =
            std::fs::read_to_string(work.join("box/b1-install/serial.log")).unwrap_or_default();
        let enrolled = esp_read(&rig.boot, "korp/root.pub")
            .map(|b| b.iter().map(|x| format!("{x:02x}")).collect::<String>());
        let pub_ok = enrolled.as_deref().is_some_and(|h| {
            h.eq_ignore_ascii_case(pubkey.trim())
                || String::from_utf8_lossy(&hex_or_text(h)).contains(pubkey.trim())
        });
        rows.push(format!(
            "install: korp/root.pub on the ESP {} the key asked for ({}…); install console: {:?}",
            if pub_ok { "EQUALS" } else { "does NOT equal" },
            enrolled
                .as_deref()
                .map(|h| &h[..h.len().min(16)])
                .unwrap_or("ABSENT"),
            install_serial
                .lines()
                .filter(|l| l.contains("require_signed") || l.contains("root") && l.contains("key"))
                .take(3)
                .map(str::trim)
                .collect::<Vec<_>>()
        ));
        if !pub_ok {
            red.push(
                "step 1: the installed ESP carries no root.pub equal to the enrolled key".into(),
            );
        }
        let sig0 = esp_read(&rig.boot, "korp/app.sig")
            .ok_or("no korp/app.sig on the installed ESP — the medium is not signed")?;
        let pin0 = esp_read(&rig.boot, "korp/app.blake3").ok_or("no korp/app.blake3")?;
        let len0 = esp_read(&rig.boot, "korp/app.len");
        let cmd0 = esp_read(&rig.boot, "korp/cmdline").ok_or("no korp/cmdline")?;
        let pub0 = esp_read(&rig.boot, "korp/root.pub").ok_or("no korp/root.pub")?;

        // ── step 2: the enrolled boot ─────────────────────────────────────
        let before = bootlog_len(&rig.boot);
        // Ready = the rig saw the SSH banner AND the control plane accept (its own readiness rule);
        // with a CLI at hand the bootstrap line is read too.
        let cli_ok = o.cli.is_file();
        let b = rig.boot("signed", |p| {
            if cli_ok {
                Session::new(&work, &o.cli).and_then(|s| s.status_line(p))
            } else {
                Ok(format!(
                    "serving: SSH banner on :{} and :{} accepting",
                    p.ssh, p.grpc_private
                ))
            }
        });
        print_boot(&b);
        let said = Said {
            serial: b.serial.clone(),
            bootlog: bootlog_since(&rig.boot, before),
        };
        let yes = said.has("require_signed=YES");
        let pass = said.has("signature-over-root=PASS");
        let not_enrolled = said.has("signature-over-root=NOT-ENROLLED");
        let served = matches!(&b.driven, Some(Ok(_))) && boot_failed(&b).is_none();
        rows.push(format!("step 2 (enrolled boot): require_signed=YES {yes:?} · signature-over-root=PASS {pass:?} · NOT-ENROLLED {not_enrolled:?} · served {served} · {:?}", said.lines(SIG_KEYS)));
        if yes.is_none() || pass.is_none() || not_enrolled.is_some() || !served {
            red.push("step 2: the enrolled boot did not show require_signed=YES with the signature leg PASS and a serving box".into());
        }

        // ── the arms ──────────────────────────────────────────────────────
        let arm = |rig: &mut Rig, name: &str| -> (Said, bool) {
            let before = bootlog_len(&rig.boot);
            let b = rig.boot_until_exit(name, Duration::from_secs(240));
            let booted = b.banner.is_some();
            (
                Said {
                    serial: b.serial,
                    bootlog: bootlog_since(&rig.boot, before),
                },
                booted,
            )
        };
        let judge = |name: &str,
                     s: &Said,
                     booted: bool,
                     red: &mut Vec<String>,
                     rows: &mut Vec<String>| {
            let sig_leg = s
                .has("signature-over-root=FAIL")
                .or_else(|| s.has("root signature does not verify"));
            let blake_blamed = s.has("blake3 mismatch");
            rows.push(format!("arm {name}: booted {booted} · signature leg named {sig_leg:?} · blake3 blamed {blake_blamed:?} · {:?}", s.lines(SIG_KEYS)));
            if booted {
                red.push(format!("arm {name}: THE MACHINE BOOTED"));
            } else if name != "B" && sig_leg.is_none() {
                red.push(format!("arm {name}: halted, but not on the signature leg (blake3 blamed: {blake_blamed:?})"));
            }
        };

        // A: one byte of app.sig flipped.
        let mut sig = sig0.clone();
        let last = sig.len() - 1;
        sig[last] ^= 0x01;
        esp_put(&rig.boot, "app.sig", Some(&sig))?;
        let (s, booted) = arm(&mut rig, "arm-A");
        judge("A", &s, booted, &mut red, &mut rows);
        esp_put(&rig.boot, "app.sig", Some(&sig0))?;

        // B: the full ATTACK 1 downgrade.
        esp_put(&rig.boot, "root.pub", None)?;
        esp_put(&rig.boot, "app.sig", None)?;
        let stripped: String = String::from_utf8_lossy(&cmd0)
            .split_whitespace()
            .filter(|t| !t.starts_with("korp.require_signed"))
            .collect::<Vec<_>>()
            .join(" ");
        esp_put(
            &rig.boot,
            "cmdline",
            Some(format!("{stripped}\n").as_bytes()),
        )?;
        let (s, booted) = arm(&mut rig, "arm-B");
        let refused = !booted;
        judge("B", &s, booted, &mut red, &mut rows);
        if refused
            && s.lines(&[
                "refus",
                "REFUS",
                "require_signed=YES",
                "NOT-ENROLLED",
                "FAIL",
            ])
            .is_empty()
        {
            red.push("arm B: stopped, but said nothing by name on either sink".into());
        }
        esp_put(&rig.boot, "root.pub", Some(&pub0))?;
        esp_put(&rig.boot, "app.sig", Some(&sig0))?;
        esp_put(&rig.boot, "cmdline", Some(&cmd0))?;

        // C ★: a foreign payload whose blake3 the attacker wrote into the pin.
        match flag("--td10-other-app").map(PathBuf::from) {
            None => rows.push(
                "arm C: UNRUN — pass --td10-other-app <a second compose's app.znippy>".into(),
            ),
            Some(other) => {
                let bytes =
                    std::fs::read(&other).map_err(|e| format!("{}: {e}", other.display()))?;
                let hex = blake3::hash(&bytes).to_hex().to_string();
                if hex.as_bytes() == pin0.trim_ascii() {
                    return Err(format!(
                        "arm C: {} IS the signed payload (same blake3) — not a tamper",
                        other.display()
                    ));
                }
                // The free pre-check: the same verifier the appliance uses must REFUSE.
                let pre = dwarves_verify(&pubkey, &sig0, &hex);
                rows.push(format!(
                    "arm C pre-check (dwarves root-key verify over the foreign root {}…): {pre}",
                    &hex[..16]
                ));
                if !pre.starts_with("REFUSED") {
                    red.push(format!("arm C pre-check did not REFUSE: {pre}"));
                }
                let (start, end) = part_range(&rig.boot, 1);
                if bytes.len() as u64 > end - start {
                    return Err(format!(
                        "arm C: the foreign payload ({} B) does not fit P2 ({} B)",
                        bytes.len(),
                        end - start
                    ));
                }
                let saved = {
                    let mut f = std::fs::File::open(&rig.boot).map_err(|e| e.to_string())?;
                    let n = bytes.len().max(
                        len0.as_deref()
                            .and_then(|l| String::from_utf8_lossy(l).trim().parse::<usize>().ok())
                            .unwrap_or(0),
                    );
                    let mut v = vec![0u8; n];
                    f.seek(SeekFrom::Start(start)).map_err(|e| e.to_string())?;
                    f.read_exact(&mut v).map_err(|e| e.to_string())?;
                    v
                };
                let boot_img = rig.boot.clone();
                let write_p2 = |b: &[u8]| -> Result<(), String> {
                    let mut f = std::fs::OpenOptions::new()
                        .write(true)
                        .open(&boot_img)
                        .map_err(|e| e.to_string())?;
                    f.seek(SeekFrom::Start(start)).map_err(|e| e.to_string())?;
                    f.write_all(b).map_err(|e| e.to_string())?;
                    f.sync_all().map_err(|e| e.to_string())
                };
                write_p2(&bytes)?;
                esp_put(&rig.boot, "app.blake3", Some(hex.as_bytes()))?;
                if len0.is_some() {
                    esp_put(
                        &rig.boot,
                        "app.len",
                        Some(bytes.len().to_string().as_bytes()),
                    )?;
                }
                let (s, booted) = arm(&mut rig, "arm-C");
                let agrees = s.has("blake3 mismatch").is_none();
                rows.push(format!("arm C: foreign payload {} ({} B, blake3 {}…) on P2, pin and length rewritten to agree; integrity agreed: {agrees}", other.display(), bytes.len(), &hex[..16]));
                judge("C", &s, booted, &mut red, &mut rows);
                write_p2(&saved)?;
                esp_put(&rig.boot, "app.blake3", Some(&pin0))?;
                if let Some(l) = &len0 {
                    esp_put(&rig.boot, "app.len", Some(l))?;
                }
            }
        }
        if !o.keep {
            rig.destroy_disks();
        }
        Ok(())
    })();
    let head = format!("OVMF/q35 · medium sha256 {medium_sha}");
    let v = match r {
        Err(e) => verdict(
            "TD10",
            TD10_NAME,
            Err(format!("{head} · {e} · {}", rows.join(" | "))),
        ),
        Ok(()) if red.is_empty() => verdict(
            "TD10",
            TD10_NAME,
            Ok(format!("{head} · {}", rows.join(" | "))),
        ),
        Ok(()) => verdict(
            "TD10",
            TD10_NAME,
            Err(format!(
                "{head} · RED: {} · {}",
                red.join("; "),
                rows.join(" | ")
            )),
        ),
    };
    log_line(&o.work, "TD10", &v.line());
    v
}

/// `root.pub` may be stored as raw 32 bytes or as hex text; either compares.
fn hex_or_text(h: &str) -> Vec<u8> {
    (0..h.len() / 2)
        .filter_map(|i| u8::from_str_radix(&h[2 * i..2 * i + 2], 16).ok())
        .collect()
}

/// `dwarves root-key verify --pubkey P --sig <hex> --root <hex>` — the appliance's own verifier.
fn dwarves_verify(pubkey: &str, sig: &[u8], root_hex: &str) -> String {
    let Some(bin) = flag("--dwarves") else {
        return "UNRUN — no --dwarves".into();
    };
    let sig_hex: String = sig.iter().map(|b| format!("{b:02x}")).collect();
    match Command::new(&bin)
        .args([
            "root-key", "verify", "--pubkey", pubkey, "--sig", &sig_hex, "--root", root_hex,
        ])
        .output()
    {
        Ok(o) => one_line(&format!(
            "{}{}",
            String::from_utf8_lossy(&o.stdout),
            String::from_utf8_lossy(&o.stderr)
        )),
        Err(e) => format!("{bin}: {e}"),
    }
}

// ── TD11: the medium record names the installer the composer ACTUALLY linked ──

pub const TD11_NAME: &str = "the medium record's korp_installer is the installer the compose built into the medium, not the version gunnar's lock pins";

fn lock_version(lock: &Path, krate: &str) -> Option<String> {
    let text = std::fs::read_to_string(lock).ok()?;
    let mut lines = text.lines();
    while let Some(l) = lines.next() {
        if l.trim() == format!("name = \"{krate}\"") {
            return lines.next().and_then(|v| {
                v.trim()
                    .strip_prefix("version = ")
                    .map(|q| q.trim_matches('"').to_string())
            });
        }
    }
    None
}

/// `--td11-tree DIR` is a compose tree (gunnar beside its dwarves and edda siblings);
/// `--td11-record FILE` the `.medium.json` that compose wrote. The composer is dwarves, and dwarves
/// takes korp-installer by PATH from `../edda` (dwarves/crates/dwarves/Cargo.toml, feature
/// `appliance-iso`), so the installer on the medium is the SIBLING's. The record is written by
/// gunnar `xtask/src/medium.rs` (`installer_pin::PLACES[0]`, gunnar's own Cargo.lock) — measured
/// 2026-09-22: record 0.1.15, sibling 0.1.14, and the twin fell into the 0.1.14 disk election.
pub fn td11(o: &Opts) -> Verdict {
    let (Some(tree), Some(record)) = (
        flag("--td11-tree").map(PathBuf::from),
        flag("--td11-record").map(PathBuf::from),
    ) else {
        return unrun("TD11", TD11_NAME, "pass --td11-tree <compose tree: gunnar + dwarves + edda siblings> and --td11-record <its .medium.json>");
    };
    let rec = std::fs::read_to_string(&record).unwrap_or_default();
    let said = rec.lines().find_map(|l| {
        l.trim()
            .strip_prefix("\"korp_installer\": ")
            .map(|v| v.trim_end_matches(',').trim_matches('"').to_string())
    });
    let pin = lock_version(&tree.join("gunnar/Cargo.lock"), "korp-installer");
    let built = std::fs::read_to_string(tree.join("edda/crates/korp-installer/Cargo.toml"))
        .ok()
        .and_then(|t| {
            t.lines().find_map(|l| {
                l.trim()
                    .strip_prefix("version = ")
                    .map(|v| v.trim_matches('"').to_string())
            })
        });
    let linked = lock_version(&tree.join("dwarves/Cargo.lock"), "korp-installer");
    let detail = format!("record says {said:?} · gunnar's pin {pin:?} · the edda sibling the composer builds {built:?} (dwarves' lock {linked:?}) · tree {} · record {}", tree.display(), record.display());
    let v = if said.is_some() && said == built {
        verdict("TD11", TD11_NAME, Ok(detail))
    } else {
        verdict("TD11", TD11_NAME, Err(format!("the record names an installer the medium does not carry — {detail}. The compose must refuse when the sibling's korp-installer differs from the pin, and the record must name the sibling's")))
    };
    log_line(&o.work, "TD11", &v.line());
    v
}