Decision 0039
The root's key is sealed to the TPM behind a PIN, and the passphrase is the way back
Architecture decision record 0039: The root's key is sealed to the TPM behind a PIN, and the passphrase is the way back
Context
ADR-0012 decided what an encrypted root would be unlocked with: the TPM, bound to the Secure
Boot state and to a PIN, with the passphrase kept for the day the TPM cannot. ADR-0033 made the
installation the stem’s — a typed plan, laid out as fixed argument vectors — and the installed
machine has booted through its passphrase prompt ever since, because the sealing was not done:
the installer wrote tpm2-device=auto into the machine’s declaration and enrolled nothing, so
the initrd looked for a TPM token, found none, and asked for the passphrase. A promise in a
configuration file with nothing behind it. This record is the enrolment, and what proving it
against an emulated chip found.
Decision
The key is sealed at installation, while the volume is open, as the second of the stem’s guarded operations; the passphrase keeps its own slot and is asked only when the TPM cannot open the root.
Operation::EncryptionKeySlotis typed:enrol-tpmwith the passphrase and the PIN,add-recoveryandremove-tpmwith the passphrase. Every action opens the volume with the passphrase first, because a key slot is changed only by somebody who can already open the volume. There is no string naming an action, as there is no string naming a command.- The enrolment is
systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 --tpm2-with-pin=yeson the partition under the volume. PCR 7 is the Secure Boot state and nothing of the kernel or the bootloader; a key bound to those would be lost at the first update of either, which is the failure that teaches people to turn encryption off. The PIN is what makes a stolen laptop’s TPM worth nothing to its thief, and the TPM’s own dictionary-attack counter is what makes the PIN worth guessing at most a few times. - The passphrase and the PIN reach the tool through its environment, under the names it
documents (
PASSWORD,NEWPIN), and it erases each as it reads it. They appear in no argument, no report, no dry run and no{:?}: a step carries its secrets in a field of their own, typed as secrets, beside the plain environment it may also set. - The installer seals only to a TPM the facts can see (
system.tpm, from/sys/class/tpm). Asked for nothing, a machine without one gets the passphrase at every boot and is told so in the plan; asked for the TPM with--tpm, it is refused; and the keeper refuses on its own account whatever the client computed, because a keeper that quietly installed a passphrase machine where a PIN was promised would be a keeper that lies in the direction of less security. A PIN comes from a file, as the passphrase does, never from a command line. - The machine’s declaration is unchanged:
tpm2-device=autoin the root’scrypttaboptions. The LUKS2 token carries the rest — that a PIN is required, which PCRs — so the declaration says where to look and the header says what was sealed.
What the test is built to show
Five of the six points below have now been shown; the sixth, the recovery boot, is the one the run stopped at, and what stopped it is recorded after them.
- The installer must boot through the firmware the machine will. The key is bound to PCR 7 as the firmware measured it at enrolment, and the machine unseals it against PCR 7 as its firmware measures it at boot. A live image booted by the driver’s direct kernel load has no firmware in front of it and a PCR 7 of nothing; the installed machine boots through OVMF, which measures the Secure Boot variables into PCR 7 before it loads anything. Sealed under one and unsealed under the other, the policy never matches. The test’s installer now boots through the same firmware, with its kernel still handed in by the driver — the firmware boots what it is given — and with the ESP the virtual-machine module would mount for a firmware boot declared absent, because the live system’s own disk has none. A real installation has never had this problem, because the medium and the machine share a firmware; the test had to be made as true as the machine.
- One chip, two machines. The emulated TPM keeps its seeds and counters in a directory the driver names after each machine, so a live image and the machine it installs would have had two chips and the seal would have been to the wrong one. The test points both at one directory, and waits for the chip a stopped machine used to have let go of it before the next machine starts, since the driver stops it a moment after the machine.
- A wrong PIN is asked again; a wrong TPM asks for the passphrase. The initrd’s token
plugin answers a bad PIN with “ask again” and a policy or chip that does not match with a
failure, and
systemd-cryptsetupturns the failure into the traditional prompt. So the machine asks for its PIN, refuses a wrong one and asks again, and booted with a chip that never sealed anything — a cleared TPM, a replaced board — it asks for the PIN, fails at the TPM, and asks for the passphrase, which opens the root. The way back was kept for exactly this, and the test walks it. This is read from the initrd’s source; the run says whether it is so.
What the runs found
The installation ran as written. The live image, booted through the firmware the machine
boots through, sealed the key to the chip both machines share: three key slots in the header —
the passphrase’s, the TPM’s and the recovery key’s — with a systemd-tpm2 token bound to PCR 7
and requiring a PIN, and a systemd-recovery token beside it. The disk then booted as the
machine, twice, on the PIN typed at the initrd’s console, with the ephemeral root restored
between them and what persists surviving.
Two things the runs corrected. What the token binds to is not in cryptsetup luksDump, which
prints only the token’s name; the test exports the token and reads its JSON, which is what the
initrd reads. And the PIN is asked for in the words of whichever of systemd’s two paths is
asking — its own code says TPM2 PIN, the token plugin it ships for libcryptsetup says
LUKS2 token PIN — so the test accepts either.
The sixth point stopped the first complete run. With a chip that never sealed anything, the initrd asks for the PIN, fails to unseal (“State not recoverable”), and asks again before systemd gives up on the token and falls back to the passphrase. A test that answered once and then waited for the passphrase was waiting for a question nobody had asked. It now answers every question in turn with the one secret that works. Whether the recovery key then opens the root is the last thing about this decision that has not been shown.
Consequences
- The default installation boots on a PIN, and the passphrase chosen at installation is the recovery credential. A generated recovery key, which ADR-0012 also asked for, is a slot the operation can already add and nothing yet shows once; it comes with the console that would show it.
tacid facts system.tpmsays whether there is a chip and which version, and the summary a model reads names it, so an agent asked “will this machine boot without me?” can answer.- The installation test is a machine with a TPM from its first boot: the live image sees the chip, seals to it, and the machine boots on the PIN through three boots, one of them with a wrong PIN first and the last with a TPM that never sealed anything. The same test now lets the live image build the machine’s first system itself, which ADR-0034 had left to a real machine.
- Still to come: the recovery key shown once, enrolment on a machine that was installed
without it (
tacid encryption enrol), and the TPM’s slot removed when the owner asks.
Alternatives rejected
- Seal to no PCRs at all. A key that opens for any firmware state opens for a firmware that was told to boot something else first. PCR 7 costs nothing an owner does not choose to change, and Secure Boot changes are the one event a person makes on purpose.
- A PIN-less seal, unlocking silently. Evil maid by default: the TPM would open the root for anyone who booted the machine. ADR-0012 refused it and this record keeps the refusal.
- Secrets on standard input, as
cryptsetuptakes them.systemd-cryptenrollreads its passphrase and PIN from the environment or from credentials, not from standard input; the environment is what it documents, it erases what it reads, and the stem’s child is the only process that ever sees it. - Credentials through
systemd-run. A transient unit withSetCredential=puts the secret on a command line;LoadCredential=needs a file the stem would have to write and remove. The environment of a direct child is the smaller exposure.