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
}