---
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
`.iso` there. 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 show` must print
`korp-root:738b-5f93-9a47-cfae`, public key
`03ade06722381a0631e839bdde685a537ec3e75029f52e363425e5ea97d89b1f`.
## Preconditions
* Run from **`/home/rickard/git/gunnar`**, the real clone. `cargo xtask
iso-installer` **refuses 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: `dwarves` `agent/root-signing-key` and `gunnar`
`agent/root-signing-key`.
* `dwarves` must be built with `--features appliance-iso` for the `root-key` verb
— 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, **not** `NONE`;
* 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 bakes
`korp.require_signed=1` when a key is enrolled), **not** the
`require_signed=NO … DECLARED unenrolled` line 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.
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.