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

nordisk/gunnar

.nornir/root-key-boot-proof-for-t3.md


name: root-key-boot-proof-for-t3 description: "The KVM proof scenario lane T10 hands to lane T3, who owns all container/ISO/KVM work on this box: compose a medium with a REAL ed25519 root key, boot it and see the signature VERIFY, then tamper with the payload after signing and see the boot REFUSED. Exact commands, exact expected serial lines, and what each arm proves." metadata: node_type: procedure

Hand-off to T3 — prove the enrolled medium, then prove the tampered one is refused

From lane T10, 2026-09-21. T10 built the key ceremony and measured every software leg it can measure without KVM. T3 owns KVM on this box, so the last two legs are T3's. This is the scenario, written to be run rather than interpreted.

What is already proven without KVM, so T3 need not re-prove it

claim how where
a mint produces a real 32-byte key, 0600, never the published dev seed unit dwarves::root_key::tests
a detached signature verifies through the appliance's own boot verifier (korp_installer::trust::Ed25519RootVerifier) unit + CLI same
a signature over one root does NOT verify over another — the tamper property unit + CLI same
a payload signed by an attacker's key does not verify against our anchor unit same
rotation keeps the superseded seed so field media stay buildable unit same
composing unenrolled is a named act with a reason, refused on metered unit gunnar xtask isoinstaller::tests

CLI evidence already taken (fake $HOME, throwaway key):

$ dwarves root-key verify --pubkey <ours> --sig <ours over root A> --root <root A>
VERIFIED — this signature covers this root under this key.

$ dwarves root-key verify --pubkey <ours> --sig <ours over root A> --root <root B>
REFUSED — the appliance would halt on this medium:
root signature does not verify against the enrolled pubkey (versioned record, version 1) — refusing to boot

What is NOT proven, and is what T3 is being asked for: that a real composed medium carries the anchor, that a real boot exercises the signature leg, and that a real tampered medium halts instead of booting.


⚠ Where this can run — custody decides it (owner ruling 2026-09-21)

The root seed lives on t14s and oden only (.nornir/root-key-custody.md). It must not be copied to loki or anywhere else to make this scenario convenient. So the scenario splits:

Preconditions

Step 0 — mint the key (once, on the build box)

dwarves root-key generate

Records root key, fingerprint and custody. Write the 64-hex public key and the fingerprint into the measurement; never the seed, and never cat the seed into a log. The seed lands at ~/.dwarves/root/korp-root.seed, 0600.

If a key already exists the verb refuses rather than overwriting — that is correct, use the existing one and record its fingerprint.

Step 1 — compose an ENROLLED medium

cd /home/rickard/git/gunnar
export KORP_ROOT_SIGNING_KEY=$HOME/.dwarves/root/korp-root.seed
cargo xtask iso-installer --variant gunnar-installer-open

PASS requires all of:

FAIL to record rather than work around: if the compose still prints the unenrolled banner while the variable is set, the forge is not reading it and nothing below is meaningful.

Step 2 — boot it and see the signature VERIFY

Boot the ISO under KVM with OVMF as T3 normally does, let it install, let it reboot onto the installed disk.

PASS requires, on the serial:

Capture the serial. The exact NOT-ENROLLED string this replaces is quoted in edda/.nornir/installer-threat-model-2026-08-16.md.

Step 3 — ★ THE TAMPER ARM. A payload swapped after signing must be REFUSED

This is ATTACK 1's remaining move, run against an enrolled machine. Three arms, cheapest first; arm C is the one that counts and the other two are there to tell a broken test apart from a working defence.

Arm A — flip one byte of app.sig on the ESP

Power off the installed guest, edit /boot/korp/app.sig (the ESP's \korp\app.sig) so one byte differs, boot.

PASS: the machine halts on the signature leg, by name. FAIL: it boots, or it halts complaining about blake3 (that would mean the wrong file was edited).

Arm B — delete root.pub and app.sig, and strip korp.require_signed from \korp\cmdline

The full ATTACK 1 downgrade.

PASS: the machine refuses by name rather than booting — the policy default is CLOSED (SignaturePolicy: absence ⇒ REQUIRED) and the authoritative token is baked into the signed first stage, so the plaintext ESP cannot loosen it. This arm re-confirms T2's August work is still standing under an enrolled medium; if it boots, the enrollment changed the policy path and that is a regression worth its own report.

Arm C — ★ write a DIFFERENT payload to P2 and its blake3 into app.blake3

The real attack: the attacker supplies their own program and makes the integrity check agree with it.

  1. Power off the installed guest.
  2. Write a different payload archive over P2 (anything that is a valid payload but not ours — the smallest honest choice is the payload from a second compose, so the archive is well-formed and only its identity differs).
  3. Compute its blake3 root and write it into /boot/korp/app.blake3.
  4. Leave root.pub and app.sig exactly as they are.
  5. Boot.

PASS — and this is the whole point of the lane: blake3 agrees (the attacker updated the pin) and the machine still halts, because app.sig is an ed25519 signature over the root the key blessed and not over the attacker's. The serial must name the signature leg as the failing one.

FAIL: it boots. That would mean the anchor is present but not enforced, and it is a stop-everything result.

Before Arm C, a free pre-check that costs no boot: take the tampered payload's blake3 root and run

dwarves root-key verify --pubkey <the enrolled key> --sig <app.sig hex> --root <tampered root>

It must print REFUSED. That call goes through the same verifier the appliance uses, so if it says VERIFIED, the tamper was not a real tamper and Arm C would prove nothing.

What to report back

For each of steps 1, 2 and 3A/3B/3C: PASS or FAIL, the serial lines that decided it, the composed medium's path, and the enrolled key's fingerprint (never the seed). A FAIL anywhere in step 3 is more valuable than three PASSes and should be reported before the run finishes.