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:
- Steps 0–1 (compose) run on t14s or oden — the only boxes holding the seed. korp-installer's forge reads the seed at compose time; there is no detached-enrollment input on the compose side yet, so the compose cannot happen on a keyless box.
- Steps 2–3 (KVM boot + tamper arms) may run on loki: carry the composed
.isothere. The ISO carries only the PUBLIC key and the signature, never the seed, so moving it is safe. - The key already exists. Do NOT run
dwarves root-key generate; step 0 is now "confirm the fingerprint":dwarves root-key showmust printkorp-root:738b-5f93-9a47-cfae, public key03ade06722381a0631e839bdde685a537ec3e75029f52e363425e5ea97d89b1f.
Preconditions
- Run from
/home/rickard/git/gunnar, the real clone.cargo xtask iso-installerrefuses a git worktree by name (REFUSED [gunnar-worktree]) because the toolchain container cannot resolve a linked worktree's gitdir and the medium would carry no build reference. - Needs T10's branches merged:
dwarvesagent/root-signing-keyandgunnaragent/root-signing-key. dwarvesmust be built with--features appliance-isofor theroot-keyverb — the same feature the compose already uses.
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:
- the verb prints
iso-installer: rootkey …/korp-root.seed (a seed file named by $KORP_ROOT_SIGNING_KEY)— not the development seed, notNONE; - the composer does not print
NO ROOT KEY (KORP_ROOT_SIGNING_KEY unset) — composing an UNENROLLED medium; - the compose log names an enrolled public key that equals
dwarves root-key show's output. If those two differ, stop: the medium is anchored to a key nobody chose.
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:
- the policy line saying
require_signed=YES(the medium bakeskorp.require_signed=1when a key is enrolled), not therequire_signed=NO … DECLARED unenrolledline the estate has been printing all along; - the selftest's signature leg reporting the root signature verified against the enrolled pubkey — not
signature-over-root=NOT-ENROLLED; - the machine reaching its normal startup.
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.
- Power off the installed guest.
- 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).
- Compute its blake3 root and write it into
/boot/korp/app.blake3. - Leave
root.pubandapp.sigexactly as they are. - 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.