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

nordisk/edda

.nornir/installer-threat-model-2026-08-16.md

korp-installer — threat model, measured on oden 2026-08-16

13 470 lines across 33 modules in edda/crates/korp-installer/src. Read against one question: what can an attacker who reaches this machine actually do?

Everything below is either read out of the source or observed in a KVM drive on this box. Where a claim is inference rather than measurement, it says so.


STATUS BOARD — updated 2026-08-16, later the same day

Rickard read this page and picked rows. This is where each one stands. A ticked line means it was seen red first, then green; anything still moving says so by name.

✔ CLOSED TODAY
  ATTACK 3   the identity key      the installer mints the WireGuard keypair, seals the
             is sealed             PRIVATE half to the TPM under sha256:0,7, stages 288 B
                                   wg/identity.sea on P3, prints the PUBLIC key on the
                                   console for the hub's [Peer] stanza. korp-start
                                   unseals. Pure Rust TPM 2.0 client, no tss2/FFI/shell.
                                   Real chip: 16/16 PCRs agree with sysfs, seal 192 ms,
                                   unseal 665 ms. Reds seen: TPM_RC_POLICY_FAIL on a moved
                                   PCR (and the SAME blob opens again after PCR_Reset, so
                                   the refusal was the boot state and not a dead blob),
                                   TPM_RC_INTEGRITY on a foreign chip or an edited blob,
                                   and a rotted file caught by its own blake3 before the
                                   chip is ever asked.
  ATTACK 1   signed cmdline        The one hole that was exploitable today. Both halves
             (both halves)         shut. (1) The policy default is CLOSED: a command line
                                   with no korp.require_signed= token REQUIRES the anchor,
                                   so deleting a token — or the whole file — refuses by
                                   name instead of downgrading. (2) The authoritative
                                   tokens (korp.require_*, korp.entry=, korp.args=,
                                   korp.env.*, korp.seal_pcrs=, korp.pin=, KORP_*) are
                                   BAKED INTO korp-boot before it is Authenticode-signed,
                                   and \korp\cmdline is merged UNDER them — it may add a
                                   key the baked line does not name (korp.znippy=,
                                   console=, initrd=: the plumbing an installed disk
                                   really owns) and may never supply a policy key, a
                                   KORP_ key or the provenance stamp. Reds seen: the
                                   shipped korp-start printing FAILED policy: on a
                                   stripped line; a hostile ESP's korp.require_signed=0 /
                                   KORP_CMDLINE= / korp.env.* absent from the LoadOptions
                                   the chainloaded PE echoed; and one byte of the baked
                                   korp.require_signed= flipped inside the SIGNED image →
                                   "Access Denied -- rejected probably by Secure Boot"
                                   under enforcing OVMF. edda 326617c · cbedd19 · ce3cd10.
                                   NOT covered: a machine whose korp-boot bakes nothing
                                   (an old or hand-laid ESP) — such a boot refuses any
                                   waiver by name and says "Recompose this medium".
  ATTACK 5   PID-1 hex panic       decode_hex32 checked a BYTE length and then sliced a
             (found + fixed)       str; "a" + "é"×31 + "a" is 64 bytes and panicked on the
                                   first pair — as PID 1, i.e. a kernel panic instead of
                                   the named refusal. Input is app.blake3 on the plaintext
                                   ESP. edda 1b95cd4, red-proven.

▶ IN FLIGHT — picked by Rickard, one agent per lane, disjoint files
  ATTACK 1   signed cmdline        CLOSED, both halves — edda 326617c · cbedd19 · ce3cd10 ·
             (closed; verified     7804d1d. require_signed is three-state and ABSENCE now
              end to end)          means REQUIRED; the authoritative tokens are compiled
                                   into korp-boot BEFORE Authenticode signs it; \korp\cmdline
                                   is merged UNDERNEATH and may never loosen policy; KORP_*
                                   off the ESP is refused (KORP_CMDLINE was a total bypass);
                                   and a waiver needs the korp.cmdline=baked stamp only the
                                   loader writes. Reds on applied output: a blanked token in
                                   the real FAT bytes → FAILED policy; a hostile ESP against
                                   a baked loader → the chainloaded PE echoes the baked
                                   values and none of the ESP's; one byte flipped inside the
                                   signed image → Access Denied under enforcing OVMF.
                                   VERIFIED BY THE MAIN SESSION 2026-08-16: gunnar drive
                                   215.63 s, 2 kernel boots, reboot handoff latched, and the
                                   serial shows `cmdline BAKED (signed, authoritative)`,
                                   `cmdline MERGED`, and the policy line naming its reason.
  ATTACK 4   rollback              CLOSED against replay — edda cc370aa · 421cb7b ·
             (closed against       94ca4bb. The signed message is a 62-byte record (domain
              replay; live         tag ‖ u64 version ‖ blake3 root), a 4th `rollback-floor`
              forgery is not)      leg refuses a lower version by name, and the floor record
                                   is WITNESSED by a TPM NV monotonic counter — so restoring
                                   an old floor beside an old signed image is caught, which
                                   is the attack this page describes. Proven on oden's chip.
                                   An attacker present NOW can still read the counter and
                                   author a fresh record: that needs TPM2_NV_Write against a
                                   policy-write index (tpm.rs has Define/Increment/Read/
                                   ReadPublic, no Write) or a PCR-sealed MAC. A machine with
                                   NO TPM keeps the bare ESP floor and SAYS so every boot.
                                   See ATTACK 4 below.
  ATTACK 2   attestation           CLOSED — edda af96c46 · e26790b · 21fe4df · c99567e ·
             (closed; the AK's     421cb7b. TPM2_Quote with an ECC-P256 restricted signing
              own provenance       AK, and a verifier that refuses a stale nonce, a foreign
              is the named limit)  key, a structure not composed by a TPM, a quote over the
                                   wrong PCRs and a wrong boot state — each by name. The
                                   appliance's WireGuard public key is hashed INTO the
                                   quote's extraData together with the hub's nonce, so one
                                   signature covers freshness, the key binding and the boot
                                   state; split them and a relayed quote proves some machine
                                   is healthy while the attacker supplies the key. Nothing
                                   the verifier compares against comes out of the blob.
                                   Measure half done too: PCR 11 carries the merged
                                   /proc/cmdline and the four ESP trust records, ABSENCE
                                   INCLUDED. Named limit: the AK is not EK-certified, so a
                                   hub's belief that it lives in a real chip is trust on
                                   first sight — see ATTACK 2 below.
             PCR-4 binding         CLOSED — edda e26790b. The first boot that opens the
             (closed)              install-time blob re-seals it to sha256:0,4,7 and
                                   atomically replaces it; never mints, so the registered
                                   public key does not change. MEASURED first, in KVM: PCR 4
                                   is identical across three boots of one loader and
                                   different for another, and PCR 0/7 do not move with the
                                   loader. Cost, named: a loader upgrade needs
                                   KORP_RESEAL_PCRS=off on the last boot of the old one.

⏸ PARKED — Rickard's call, 2026-08-16: "låt data vara"
  ATTACK 3b  disk encryption       everything other than the identity key stays plaintext
             for /data             at rest. Deliberate, not forgotten.

One thing this board must not be read as saying. A row marked CLOSED closed the mechanism; it does not mean every product is configured to use it.

The sharpest example is gunnar right now. ATTACK 1 is closed, and gunnar's medium nevertheless bakes korp.require_signed=0, so the appliance boots on blake3 integrity alone — verified in the serial 2026-08-16:

INSTALLER: policy: require_signed=NO — this appliance is DECLARED unenrolled on the
SIGNED command line (korp.require_signed=0, korp.cmdline=baked) and boots on blake3
integrity alone. Integrity says the payload is the one that was pinned; only the
ed25519 anchor says WHO pinned it

That is exactly what the fix was for: the waiver is now a declaration on the signed half, not the absence of a token an attacker can delete. But gunnar does not yet have an enrolled root key, so gunnar does not yet have authenticity. Enrolling one and baking korp.require_signed=1 is the remaining product-side step, and it is not a code change.


The chain as it actually runs

UEFI firmware
  └─ BOOTX64.EFI = korp-boot        SIGNED (Authenticode, isocompose.rs:170)
       ├─ reads \korp\cmdline       ← A PLAIN FILE ON THE ESP. NOT SIGNED.
       └─ LoadImage \vmlinuz        SIGNED (isocompose.rs:172)
            └─ korp-start (PID 1)
                 ├─ /boot/korp/app.blake3   pin       ESP, not signed
                 ├─ /boot/korp/app.sig      signature ESP, not signed
                 ├─ /boot/korp/root.pub     pubkey    ESP, not signed
                 ├─ selftest: 3 legs (archive blake3 · ed25519 over root · znippy chunks)
                 └─ memfd + fexecve         the payload never lands on disk

secureboot.rs contains zero references to the command line; sign_pe is called on exactly two things, and \korp\cmdline is not one of them.

What is genuinely good, and better than the docs claim

payload-at-rest   REAL     two independent blake3 checks per boot — the pin over the
                           whole archive (korp-start), and znippy's per-chunk verify
                           during extraction. Re-run every boot, so tamper is
                           continuously evident, not just at install.
never-on-disk     REAL     the uncompressed program is written into an anonymous
                           memfd and fexecve'd. No extracted binary to swap.
small-order keys  CLOSED   Ed25519RootVerifier::from_pubkey_bytes rejects is_weak().
                           Without it an enrolled key of 32 zero bytes would verify 64
                           zero bytes over ANY payload — a valid-looking key that
                           trusts everything. That is a real ed25519 footgun and it is
                           already shut.
malformed ≠ absent CLOSED  a root.pub that is present but not 32 bytes HALTS. It does
                           not silently downgrade to no-anchor, which is the obvious
                           way to disable a check by corrupting it.
pure Rust         REAL     no mdadm, no sbsign, no openssl, no shell. GPT, FAT, md
                           RAID1 v1.0 superblocks, Authenticode and ISO-9660 are all
                           written by hand. A smaller trusted surface than any
                           installer that shells out.

ATTACK 1 — cmdline downgrade. The one that matters.

Threat: an attacker with write access to the ESP (evil maid, a stolen disk, a recovery shell, any pre-boot code) — exactly the threat Secure Boot exists to stop.

The move: delete the korp.require_signed=1 token from \korp\cmdline, delete root.pub and app.sig, write your own payload to P2, write its blake3 into app.blake3.

Why it works: policy is read from the same unsigned file the attack edits.

// korp-start.rs:94
let require_signed = std::env::var("KORP_REQUIRE_SIGNED")… || cl.require_signed;
// korp-start.rs:174
let checked = if require_signed { verdict.require_all_legs() } else { verdict.into_result() };

Remove the token and require_signed is false, so nothing is required. The signature leg reports NOT-ENROLLED — honestly, loudly, and then boots anyway, because integrity-only is a supported configuration and the machine no longer has anything saying it should not be. require_signature_leg catches a stripped record on an enrolled machine; it cannot catch a machine that has been told it was never enrolled.

The blake3 pin does not help: it is on the same partition, and the attacker rewrites it. The code says this itself in trust.rs:57 — "a blake3 pin on a plaintext ESP that anyone who rewrites the ESP can forge".

This is precisely why UKI exists. Embedding kernel + initrd + cmdline in one signed PE means the command line cannot be changed without breaking the signature; a cmdline kept as a separate ESP file is the textbook vector (init=/bin/sh, disable LSMs, change the root device). korp-boot reads \korp\cmdline as a file, so it has the pre-UKI shape with a post-UKI threat model.

Fix, in order of cost: bake the cmdline into the signed first-stage PE (or into a UKI) so korp.require_signed and korp.entry are covered by Authenticode; or, much cheaper as a stopgap, make require_signed default true and require an explicit signed opt-out, so removing a token fails closed instead of open.

Also riding the same unsigned file, same reasoning: korp.entry=, korp.args= (the app's argv) and korp.env.<K>= (its envp). Those reach a verified binary, so the payload cannot be swapped — but its argv and environment can be chosen by whoever holds the ESP.

✔ CLOSED, same day — both halves

Step 1, the default (edda 326617c). korp.require_signed is no longer a bool that defaults false; it is a three-state (cmdline::SignaturePolicy): no token ⇒ required, =1 ⇒ required and said so, =0 ⇒ declared unenrolled. Only the exact =0/=false waives the anchor — =off, =no and any typo stay closed, so no misspelling is a downgrade. cmdline::decide is the single place the rule lives and it returns the sentence the console prints, so the installer PID-1 and korp-start cannot reach different verdicts from one input. InstallPlan::require_signed defaults true; isocompose bakes korp.require_signed=0 and warns loudly when no root key is configured, so "this fleet ships unsigned" is a decision the BUILDER made and the medium records.

RED, seen first: flipping the default turned 9 lib tests red with the same refusal a stripped machine now gets. tests/attack1_cmdline_policy.rs then runs the attack for real — compose an ESP through the production writer, blank the token out of the FAT bytes (same length, so no filesystem tooling is needed), read it back through fatfs, hand the recovered line to the shipped korp-start with a real .znippy whose integrity legs both PASS:

INSTALLER: policy: require_signed=YES (no korp.require_signed= token — the default is
  CLOSED, so a REMOVED policy token refuses instead of downgrading; …)
INSTALLER: selftest startup root=6a8c2bb11c2a archive-root-blake3=PASS
  signature-over-root=NOT-ENROLLED per-chunk-blake3=PASS
INSTALLER: FAILED policy: korp.require_signed is IN FORCE and the authenticity anchor
  did not hold … An appliance that genuinely carries no enrolled key is a legal
  configuration and must SAY so — declare korp.require_signed=0 on the medium that
  composed it

Step 2, the signature (edda cbedd19). Whoever can delete a token can add one, so the default alone was never enough. The authoritative token subset (cmdline::authoritative_tokens: korp.require_*, korp.entry=, korp.args=, korp.env.*, korp.seal_pcrs=, korp.pin=, KORP_*) is compiled INTO korp-boot by its build.rs from KORP_BAKED_CMDLINE, which isocompose::compose_and_master sets before it builds the loader and therefore before it Authenticode-signs it. Resolving the command line is now step 0 of the composition; it used to be step 4, after the loader was built and signed, which is the one order that cannot work.

\korp\cmdline survives, subordinate. korp-boot merges it UNDER the baked half and drops any token whose key is baked, any korp.require_*, any KORP_ (Linux turns an unrecognised KEY=value into init's ENVIRONMENT, and korp-start reads KORP_CMDLINE/KORP_APP_PIN/KORP_APP_SIG/ KORP_ROOT_PUBKEY from exactly there — without this an attacker skips the whole mechanism with one token), and korp.cmdline. Then it stamps korp.cmdline=baked, which the ESP can never write for itself, and korp-start honours korp.require_signed=0 only on a line carrying that stamp.

NOT baked, deliberately: console=, korp.appliance=, korp.znippy=, initrd= and the products' own boot params. Those are per-MACHINE plumbing an installed disk legitimately rewrites — its app is a RAW PARTITION the ISO knows nothing about — and a loader that insisted on its own copy would brick every install. They are also not what this attack is about: pointing the loader at another archive changes nothing while the pin and the anchor still have to hold.

RED, on applied output — the chainloaded PE's own echo of the LoadOptions it was handed, against an ESP carrying the full evil-maid set:

KORP-BOOT: cmdline (\korp\cmdline, UNSIGNED - subordinate) = "console=ttyS0
  korp.znippy=part:korp-rootfs korp.require_signed=0
  KORP_CMDLINE=korp.require_signed=0 korp.env.BAKEDNONCE=c9e23e2f29c1df1e initrd=\initrd"
KORP-BOOT: cmdline BAKED (signed, authoritative) = "korp.require_signed=1
  korp.entry=korp korp.env.BAKEDNONCE=18c8f57793c46d26"
KORP-STAGE2: ALIVE cmdline="console=ttyS0 korp.znippy=part:korp-rootfs initrd=\initrd
  korp.require_signed=1 korp.entry=korp korp.env.BAKEDNONCE=18c8f57793c46d26
  korp.cmdline=baked"

and the signature half, one byte of the baked korp.require_signed= flipped 1→0 inside the signed image (same length, so only content changed), under Secure-Boot-enforcing OVMF, against a control boot of the untouched image that runs and hands the baked policy on:

BdsDxe: failed to load Boot0002 "UEFI Misc Device" …: Access Denied
  -- rejected probably by Secure Boot

What is still open. A machine whose korp-boot bakes nothing — an old medium or a hand-laid ESP — has no authoritative half to merge under, so its whole command line is attacker-choosable. It cannot be downgraded (a waiver without the stamp is refused by name and told to recompose), but its korp.znippy=, console= and initrd= are still whoever's holds the disk. And on a BAKED machine the subordinate half remains exactly that: the loader can be pointed at a different archive, which the pin and the anchor then have to catch — as they always did.

ATTACK 2 — no measured boot, no sealing, no attestation

Grepped, not assumed (2026-08-16, \btpm\b|tpm2|pcr[0-9]|pcr_|measured.?boot|tss2|swtpm over *.rs/*.toml/*.md, target/ and vendor/ excluded):

korp        0      holger      0      gunnar      1   (an unrelated doc line)
tunnr       0      draupnir    0      Skidbladnir 0      dwarves 0
edda       18      ← every one of them accounted for below

edda's eighteen are: this file, nornir-vault's TpmProvider — its own doc says "design-only stub … TODO: pick a portable TPM 2.0 binding", and it is the thin-client vault (korp's own mTLS cert / Spark token), not the appliance —, one banner.rs comment using "measured-boot" as prose about console ordering, and two lines in a parked-gix/ scratch doc.

korp-installer itself: zero. So:

✔ HALF CLOSED, same day — korp_installer::tpm + korp_installer::wgseal

Sealing exists now, and it is measured. A pure-Rust TPM 2.0 client (crates/korp-installer/src/tpm.rs, hand-marshalled over /dev/tpmrm0 — no tss2, no shell, no FFI) carries StartAuthSession/PolicyPCR/ PolicyGetDigest/CreatePrimary/Create/Load/Unseal/FlushContext/ PCR_Read/PCR_Extend/PCR_Reset/GetRandom. Proven against oden's real chip and against two independent swtpm instances:

PCR_Read vs /sys/class/tpm/tpm0/pcr-sha256/*   16 of 16 agree byte-for-byte
seal (ECC-P256 SRK + PCR policy)               212 ms   blob 288 B on disk
unseal                                         648 ms   byte-identical

What is still MISSING from ATTACK 2 proper: nothing is measured into PCRs by korp's own chain (the firmware measures itself; korp-boot measures nothing), and there is still no attestation to a hub. Sealing is the confidentiality half; attestation is the other half and is untouched.

✔ CLOSED, same day — korp_installer::attest + korp_installer::measure

The hub can now check the claim it used to have to take. TPM2_Quote is implemented in the same hand-marshalled client (no tss2, no shell), signed by a deterministic ECC-P256 restricted primary on the owner hierarchy — the SRK template with sign where the SRK has decrypt. Restricted is the bit that matters: the chip refuses to sign external data beginning with TPM_GENERATED_VALUE, so a quote cannot be a blob somebody handed the chip.

The seam is one hash. extraData = blake3(domain ‖ hub nonce ‖ wg_public_b64), so a single signature covers three things at once:

  freshness        the hub's own 32-byte nonce is inside the signed bytes
  identity binding the appliance's WireGuard public key is inside the SAME bytes
  boot state       the PCR digest

Split them and each is defeatable: a quote carrying only a nonce proves some machine is healthy, and the attacker relays it beside their own key. That exact relay is a red in the suite, on real silicon, and it is refused as STALE or MISDIRECTED quote.

Nothing the verifier compares against comes out of the blob. The nonce is the one the hub issued, the WireGuard key is the one it has on file, the AK is the point pinned at enrolment, the expected boot state is its own record. A verifier that read the nonce out of the attestation would accept every replay and still be green.

And korp now measures its own chain. The firmware measures every image it loads into PCR 4 — measured, not assumed: three boots of one loader give an identical PCR 4 and a different loader gives a different one — so korp-boot, the kernel it chainloads, and (since cbedd19) the baked policy tokens inside the signed PE are already covered. What was not covered is what korp-start reads. PCR 11 now carries, once per boot:

  /proc/cmdline (merged)   the line the KERNEL received — its ESP third is unsigned
  korp/app.blake3          the payload pin
  korp/app.sig             the detached signature
  korp/root.pub            the enrolled anchor
  korp/rollback.floor      the version floor

Absence is measured, which is the entire point and is ATTACK 1's residue: delete root.pub and app.sig and the machine honestly reports NOT-ENROLLED and boots, because from the inside a machine that was never enrolled and one whose anchor was deleted this morning are identical. They are not identical to a hub holding the golden value — but only if a missing file hashes to something rather than being skipped.

The seal deliberately does not bind to PCR 11: the ESP third of the command line legitimately differs per machine, and binding the identity to the payload pin would cost a machine its key on every app update. The seal binds to what must be stable; the quote reports everything.

The named limit, and it is the real one. The AK is not certified against the TPM's endorsement key (no MakeCredential/ActivateCredential), so the hub's belief that the AK lives in a genuine TPM is trust on first sight: it learns the point in the same operator-carried ceremony that hands it the WireGuard public key. Every quote afterwards is a real statement — the same machine as at enrolment, in this boot state now — which is precisely what was missing. Detecting a forged FIRST enrolment needs the EK chain and is a separate change. An EK-hierarchy AK would also report the chip's true counters; ours are obfuscated by the spec (oden reports reset #3622688011), so they are comparable across quotes from one machine and meaningless in absolute terms.

ATTACK 3 — the minted WireGuard key is plaintext at rest, and it was meant to be TPM-sealed

This one is not a missing nice-to-have. It is stated design intent that was never built, and nothing in the tree says so.

The ceremony mints the appliance's WireGuard private key onto P3 and protects it with 0600 plus an O_CREAT|O_EXCL refusal on a group-readable file. That is all. Grepped wg-appliance-core for tpm|seal|encrypt|luks|aes|kek: the only hits are hubfixture.rs's sealed_packets, which counts WireGuard datagrams on the wire and has nothing to do with rest. ceremony.rs:55 states the shape plainly:

"The appliance's private key, base64 + newline, mode 0600. The file IS the …"

Base64. On an unencrypted partition. OS permissions are the whole protection, and they protect nothing against someone holding the disk: pull the drive, read the file, and you are that appliance to the hub — no forgery needed, it is the real key. The hub cannot tell the difference, because there is no attestation (ATTACK 2) for it to check against.

The step that made this reachable is in the design notes as a fix: /data had to become persistent so the key could survive reboot, because a RAM-backed /data could not keep one. That succeeded. It also turned a key that evaporated on power loss into a durable secret, and the confidentiality half was never added.

What the design called for: mint the key, then seal it to the platform TPM under a PCR policy, so it unseals only on a boot that measures the same. Then a stolen disk is inert and a tampered boot cannot reach the key.

What is on the box today: the key in the clear, and a TpmProvider in a different crate that returns an error saying it is design-only.

Cheapest honest step if a full seal is far off: say so in ceremony.rs. Right now the module documents the 0600 and the O_EXCL in loving detail, which reads as this key is looked after — and it is looked after against the wrong adversary.

✔ CLOSED, same day — the installer mints it and the TPM keeps it

Rickard's ruling: the installer generates the key, and it is stored on the disk, alongside the znippy archive. Built:

The bound PCRs are sha256:0,7 (korp.seal_pcrs= overrides), and that choice is measured, not reasoned — tests/tpm_kvm_pcrs.rs boots OVMF twice against one emulated chip:

             from a CD-ROM        from a hard disk
  PCR 0   012103E0F956C315…    012103E0F956C315…   SAME
  PCR 1   91FE52953A8C078E…    A339966DDE8F6FE2…   DIFFERS  ← the finding
  PCR 7   B5710BF57D25623E…    B5710BF57D25623E…   SAME

PCR 1 is the platform configuration PCR and the attached media is part of that configuration — so the binding that reads like the careful choice, "PCRs 0–7", would install every appliance cleanly and then break it on its first boot.

What is still plaintext, said plainly: hub.conf (correctly — public data) and the .znippy payload (correctly — it is an executable carrying no data, already covered by blake3 + ed25519).

✔ The PCR-4 gap, CLOSED — edda e26790b

The seal used to bind firmware + Secure Boot state and not which loader ran, so a substituted korp-boot still opened the appliance's key. The installer cannot close that — its own PCR 4 measures the installer's loader — so the first boot that successfully opens the blob re-seals it to sha256:0,4,7.

Measured before it was built, because widening a PCR set on reasoning is how you ship an appliance that opens its key exactly once (tests/tpm_kvm_pcrs.rs, four OVMF boots off disks this installer laid down):

  loader A, boot 1   PCR 4 = F2A242EDDC7012A9…
  loader A, boot 2   PCR 4 = F2A242EDDC7012A9…   STABLE
  loader A, boot 3   PCR 4 = F2A242EDDC7012A9…   STABLE
  loader B           PCR 4 = 9ED54B67D7E73E6A…   DIFFERS
  PCR 0 / PCR 7      unchanged by the loader swap

Both halves had to hold. Unstable across reboots and every appliance refuses its own key on the boot after the re-seal; identical across loaders and the binding costs the fragility and buys nothing. The third boot is there because a value that settled on boot 2 and moved on boot 3 would look correct and brick the machine one reboot later than anyone thinks to look.

The commit protocol is six steps with one rule — identity.sea is authoritative whenever it exists, identity.new is adopted only when it does not — so every crash window leaves at least one openable copy of the same key. Step 3 opens the staged blob again through the chip before the old one is deleted; without it a machine can delete the blob it could open and commit one it cannot, and find out on the next boot with nothing to fall back on. A re-seal never mints, so the public key the hub registered does not change.

Proven on oden's real chip, including the part that is the point: with the loader PCR moved, the re-sealed blob refuses with TPM_RC_POLICY_FAIL and the pre-re-seal blob still opens — the gap, demonstrated rather than argued.

The cost, named: a loader upgrade now needs a deliberate step. Relax the binding with KORP_RESEAL_PCRS=off on the last boot of the old loader (while the machine can still open its key), swap it, and the first boot of the new one re-binds. The refusal text says exactly this, because an operator meeting it for the first time is meeting it on a machine that will not boot.

ATTACK 4 — rollback ✔ CLOSED AGAINST REPLAY (edda cc370aa · 421cb7b · 94ca4bb)

live forgery of the floor record is NOT closed — see below

Ed25519RootVerifier::verify_root signed the blake3 root and nothing else — no version, no monotonic counter. A payload signed by the genuine key stayed valid forever, so an attacker who kept an old, signed, vulnerable image could install it and every leg went PASS, honestly, because every leg was true. The image really was whole and really was ours. It was simply old, and nothing in the chain could say so. This is the case Android Verified Boot spends a rollback index on.

What is signed now

rollback::signed_record — fixed width, fixed order, no separators to disagree about
  b"korp.root-of-trust.v1\0"   22   domain tag
  version.to_be_bytes()         8   u64 big-endian, >= 1
  root.as_bytes()              32   the blake3 root
                             ----
                               62

The tag is the mechanism, not decoration. Without it the two formats would be "32 bytes" and "40-ish bytes", and any code path willing to try both would let an attacker present one as the other — downgrade by format confusion, reopening the hole through the fix that closed it. Which format is in play is decided by the app.sig framing — 64 bytes = the legacy bare signature, KORPSIG1 ‖ u64 version ‖ sig = 80 bytes = a versioned record — before a key is touched, and a failed verify is never retried under the other reading.

The version is inside the signed material, so editing it in the blob breaks the signature. The number the floor compares is a number the signing key stated.

The floor — TWO homes, and a machine says which it has

korp/rollback.floor, read before the archive is touched, advanced only after every leg held and only to a version a verify actually recovered. /boot is mounted MS_RDONLY, so the write flips that mount rw and straight back; without that the floor would never leave 0 and the whole check would be a guard that can never fire.

It comes in two strengths, and korp-start prints which on every boot. A floor of 31 anyone can rewrite and a floor of 31 the chip stands behind are different machines, and no reader should have to guess which one they are looking at.

esp-file:/boot/korp/rollback.floor (UNWITNESSED)      no TPM
/boot/korp/rollback.floor witnessed by
                    tpm-nv:0x01800f20@/dev/tpmrm0     a real chip

Witnessed (rollback::TpmNvFloor, edda 421cb7b). A TPM NV counter cannot BE the floor: it only counts, NV_Increment moves it by one, and it starts wherever that chip's counters happen to start. Measured on oden's own chip — 7 for one freshly defined index and 14 for another an hour later. Read that as a version and the machine refuses every payload below 14 while looking exactly like working rollback protection.

So the counter is a ticket. The record carries both numbers — 31 4711 is "version 31, witnessed by counter 4711" — and a boot refuses unless they still agree:

accept(N):  write {N, V+1}   then   increment → V+1
boot:       ticket == V       consistent
            ticket == V + 1   the write landed, the increment did not (power loss)
                              — complete it and carry on
            ticket <  V       REFUSED: older than the chip
            ticket >  V + 1   REFUSED: never written here

Write-then-increment, and the healing window is one step ahead, which replay can never reach because replay only ever moves a ticket backwards.

That closes the attack this page is written about: an attacker who kept an old, properly signed, vulnerable image and restores it together with the floor that was current when it shipped. Rewriting the ESP no longer erases the floor, because the number that invalidates the record lives in the chip. Real-silicon refusal, /dev/tpmrm0:

rollback refused: the floor record at …/korp/rollback.floor claims version 900
witnessed by counter 16, and the NV counter at tpm-nv:0x01800f21@/dev/tpmrm0 is at
17 — a counter only ever goes UP, so this record is not the one this machine wrote.
An older record restored from a backup looks exactly like this, and it is the attack
the witness exists to catch

What is still open, and say it rather than let "TPM-backed" imply it: an attacker present now can read the current counter and author {version: 1, ticket: V}. Detecting that needs the floor VALUE inside the chip — TPM2_NV_Write against a policy-write index, and tpm.rs has DefineSpace / Increment / Read / ReadPublic and no Write — or a MAC over the record under a PCR-sealed key. Replay is closed; live forgery is not.

Unwitnessed (rollback::FloorFile). An appliance with no chip keeps the bare decimal on the plaintext ESP, and an attacker who can rewrite the ESP rewrites the floor. That is the same attacker ATTACK 1 describes. It still buys: an old genuinely-signed payload swapped onto P2 is refused at the next boot; a stale update channel is refused instead of accepted; a versioned machine never accepts an unversioned payload again. It does not survive a re-install, which rewrites the ESP and the floor with it.

A machine that has ever had a witnessed record refuses the unwitnessed path by name, so pulling the TPM — or booting a kernel without the driver — is not a way to disarm this. InstallPlan::rollback_floor is where an installer hands a durable floor to the install stage.

The declared compatibility policy

An unversioned signature counts as version 0. On a machine whose floor is still 0 it is accepted — nothing that boots today stops booting — and the rollback-floor leg reports NOT-ENROLLED, never PASS, because such a machine has no rollback protection and the serial line must not imply it does. Once any versioned payload has been accepted, an unversioned one is refused by name. The fleet ratchets forward and never back. Same-version is accepted: a repair, or a second boot of the running image, must not be a brick. The forge signs versioned by DEFAULT (KORP_PAYLOAD_VERSION, default 1); opting out is KORP_PAYLOAD_VERSION=legacy and has to be typed.

The refusal

selftest startup root=… archive-root-blake3=PASS signature-over-root=PASS
                        rollback-floor=FAIL per-chunk-blake3=NOT-RUN

rollback refused: this payload is version 9 and this machine has already accepted
version 31 — an OLDER image with a genuine signature is exactly what this floor
exists to stop

The signature leg still says PASS beside it, which is the point: the image is ours, and old.

ATTACK 5 — a hostile pin file panicked PID 1 ✔ FIXED TODAY

decode_hex32 checked hex.len() (bytes) and then sliced &hex[i*2..i*2+2] (a str slice). "a" + "é"×31 + "a" is exactly 64 bytes and panics on the first pair — in PID 1, where a panic is a kernel panic with no line naming the bad record, instead of the loud refusal the design promises. Input is /boot/korp/app.blake3 on the plaintext ESP.

Impact is denial of service only (a bad pin was always going to refuse), but it refused in the wrong way. Fixed in edda 1b95cd4, red-proven first (end byte index 2 is not a char boundary; it is inside 'é').

ATTACK 6 — media parsing, and why it is not the soft spot

iso9660.rs parses attacker-shaped structures off removable media. It bounds-checks: u32le refuses a field that runs past its record, directory records that overrun their extent are named and refused, and multi-extent files are refused rather than mis-joined. Rust's slice indexing removes the class that would otherwise dominate here. Nothing found worth fixing; the risk sits at ATTACK 1, not in the parsers.

Honest scorecard against 2026 practice

signed boot chain        DONE     Authenticode over korp-boot + kernel, customer PK/KEK/db,
                                  proven under enforcing OVMF (unsigned → Access Denied,
                                  foreign key → Access Denied)
payload integrity        DONE     blake3, twice, every boot
payload authenticity     OPTIONAL ed25519 over the blake3 root, real — but off unless
                                  a policy token on an unsigned file says otherwise
signed command line      DONE     the UKI property, by baking: the policy, korp.entry=,
                                  korp.args= and korp.env.* live inside the
                                  Authenticode-signed korp-boot, \korp\cmdline is
                                  merged under them and may never loosen. Red-proven
                                  both ways (a hostile ESP ignored; a one-byte edit of
                                  the baked line → Access Denied). ATTACK 1.
measured boot / PCRs     DONE     the FIRMWARE measures itself and every image it LOADS
                                  (PCR 4 = the loaded PE, measured across four KVM boots);
                                  korp extends PCR 11 with the merged /proc/cmdline and its
                                  four ESP trust records, absence included
TPM-sealed secrets       DONE     the appliance identity key, sealed under sha256:0,7 by
                                  the installer, unsealed by korp-start, fail-closed by
                                  name. Pure Rust, no tss2. Red-proven on real silicon:
                                  a moved PCR → TPM_RC_POLICY_FAIL, a foreign chip or an
                                  edited blob → TPM_RC_INTEGRITY
disk encryption          PARKED   everything OTHER than the identity key is plaintext at
                                  rest — correctly for hub.conf and the payload. Rickard's
                                  call 2026-08-16: leave /data alone for now
dm-verity / RO root      N/A      the payload is a blake3-verified archive exec'd from
                                  RAM, which covers the same ground differently
rollback protection      DONE     a u64 version is bound into the signed material
                         (with a  (62-byte domain-separated record) and a lower version is
                          named   refused by name at both install and boot. WITH A TPM the
                          limit)  floor record is witnessed by an NV monotonic counter, so
                                  restoring an old floor beside an old signed image is
                                  refused — proven on oden's chip. WITHOUT one it is a
                                  plaintext file and whoever rewrites the ESP rewrites it;
                                  korp-start prints which of the two a machine has, every
                                  boot. Live forgery by an attacker who reads the current
                                  counter is NOT closed: that needs TPM2_NV_Write on a
                                  policy-write index, or a PCR-sealed MAC over the record
A/B slots, atomic update MISSING  not picked
remote attestation       DONE     TPM2_Quote, ECC-P256 restricted AK, hub-supplied nonce
                                  AND the appliance's WireGuard public key hashed into one
                                  extraData. Verifier refuses stale/foreign/forged/wrong-PCR
                                  /wrong-state by name. Limit: the AK is not EK-certified,
                                  so first enrolment is trust on first sight  in flight — see the status board
per-product boot mark    DONE     an [[build.installer]] cell names its own boot-logo; the
                                  chain runs boot-logo → KORP_BOOT_LOGO → korp-boot-efi's
                                  build.rs → gop_splash, blitted before the first ESP read.
                                  Before today InstallerBuild had no such field and the
                                  production korp-boot had no splash code at all — the only
                                  thing in the tree that drew a logo was a KVM test
                                  fixture, so every product booted korp's ravens. Proven on
                                  bytes (one loader, two builds: 743 048 B vs 596 968 B,
                                  different md5) and on pixels (three distinct blake3 over
                                  real screenshots), on the install boot AND on the reboot

Is it "ultimate"? No, and it is not done. What exists is unusually strong for its size and unusually honest about what it has not measured. The gap between here and the state of the art is one shape, not ten features: bind the policy and the command line into something signed, then measure the boot into a TPM so the machine can prove what it ran. ATTACK 1 was the only one on this page exploitable today by someone who has already reached the disk, and it is the cheapest to close.

Sources for the comparison