TacidOS
DevelopersGitHub

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

Status
accepted
Date
2026-09-05

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::EncryptionKeySlot is typed: enrol-tpm with the passphrase and the PIN, add-recovery and remove-tpm with 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=yes on 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=auto in the root’s crypttab options. 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

The first full run of the installation test with the sealed key was under way when this record was written; what it found is recorded below it, in a section of its own, once it has.

  1. 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.
  2. 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.
  3. 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-cryptsetup turns 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.

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.tpm says 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 cryptsetup takes them. systemd-cryptenroll reads 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 with SetCredential= 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.