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:
- nothing is measured into PCRs, so the platform cannot prove its state to a hub — the appliance's WireGuard identity is asserted, never attested;
- no secret can be sealed to a boot state, so there is no way to make a key unavailable to a tampered boot;
- Secure Boot alone blocks some bad code but leaves you blind afterwards, which is the standard reason measured boot is paired with it.
✔ 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:
korp-installermints and seals (install.rs→wgseal::mint_and_seal). The private half goes into the chip and lands on P3 aswg/identity.sea, wrapped to this TPM under a PCR policy. The public half goes on the console in the canonicalwg-appliance identity:line the fleet's one watcher already greps, so the operator pastes it into the hub's[Peer]stanza and nothing secret leaves the machine. Steps 3+4 of the ceremony, whichnornir-workspace.tomlstill markedplanerat.korp-startunseals (datahome::boot_data_home→wgseal:: load_or_mint_sealed_identity). Idempotency is kept: a machine with an identity never re-mints.- Fail closed, by name. A machine carrying
wg/identity.seathat cannot open it REFUSES — no reachable TPM, a moved PCR, a corrupt blob, allFAILED unseal:with the chip's ownTPM_RCand the live PCR values on the line. It never mints a plaintext replacement, because that would turn the strongest state into the weakest one on exactly the boot where something is wrong. Awg/holding BOTH a seal and a plaintext key is refused as an ambiguity. - Declared, not downgraded. A machine with no TPM says so every boot, names what it could not seal to, and points at
korp.require_sealed=1— which turns the whole configuration into a refusal at install time and at boot time.
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
- https://uapi-group.org/specifications/specs/unified_kernel_image/
- https://wiki.archlinux.org/title/Unified_kernel_image
- https://www.talos.dev/v1.10/talos-guides/install/bare-metal-platforms/secureboot/
- https://0pointer.net/blog/brave-new-trusted-boot-world.html
- https://source.android.com/docs/security/features/verifiedboot/avb
- https://source.android.com/docs/security/features/verifiedboot/dm-verity