tokenvault · LUKS2 · systemd-cryptsetup
TPM2 and FIDO2. Both, or the disk stays shut.
systemd-cryptenroll gives every token its own keyslot, so a TPM2 enrollment and a FIDO2 enrollment each open the disk alone. That's OR. tokenvault chains them into AND, using only stock systemd units in the initramfs.
The trick: a vault inside a vault
The root partition gets one extra keyslot. Its passphrase is random and lives in a small, two-layer LUKS2 vault on the ESP. To read it, you have to get through both layers in order:
ESP /tokenvault/vault.img
└─ outer LUKS2 AES-256-XTS
unlocked by the TPM2: PCR 7 + signed PCR 11 + TPM2 PIN
└─ inner LUKS2 Serpent-256-XTS
unlocked by the FIDO2 key: client PIN + touch (hmac-secret)
└─ payload: 64-character passphrase → keyslot of the root partition
The two layers use two cipher families on two devices. The TPM never sees the FIDO2 secret, and the key never sees the TPM's. Your install passphrase stays in keyslot 0 as the offline recovery path.
What you see at boot
- "Please enter TPM2 PIN". The TPM checks the PIN against a policy bound to PCR 7 (your Secure Boot state) and PCR 11 (a UKI signed by your PCR key, inside the initramfs phase).
- "Please enter security token PIN". The FIDO2 key verifies its own client PIN, with its own retry counter.
- Touch the key. User presence is required, so malware can't silently use a key that happens to be plugged in.
- Root unlocks. gpt-auto's unmodified
systemd-cryptsetup@roottakes the cached passphrase. It is revoked as soon as root is open, and expires after 120 s at most. - The seal. PCR 11 is extended past the initramfs phase. The running system can no longer make the TPM release anything.
If any step fails, nothing is cached and you get the normal passphrase prompt. The causes include a wrong PIN, a missing key, no touch, a foreign TPM, a changed Secure Boot state or an unsigned kernel.
Why a signed PCR 11 policy
Binding to literal PCR values breaks on every kernel update. Here, ukify signs the expected PCR 11 value of each UKI with an offline PCR key. The TPM accepts any UKI carrying a valid signature from that key, and nothing else. Updates keep unlocking with no re-enrollment, and a forged kernel never unlocks.
PCR 7 is bound literally, because it changes only when you change firmware keys, and you want re-enrollment then.
Hardened around the edges
- The PIN config is labeled with its own SELinux type,
tokenvault_conf_t, so confined services can't read it. The e2e suite proves NetworkManager is denied. - The ESP is found through
LoaderDevicePartUUID, with no kernel arguments, because the command line is sealed in the UKI. tokenvault verifychecks the vault, the root keyslot, the UKI's sections and PCR signature, and that the TPM now refuses.
Proven in 11 scenarios
KVM, OVMF Secure Boot in custom mode, swtpm and a virtual CTAP2 key on a Linux USB gadget, with the real initramfs from the signed image:
| Scenario | Expected |
|---|---|
| Correct TPM2 PIN, FIDO2 PIN and touch | Root unlocks, no passphrase |
| After boot, ask the TPM again | Refused (sealed) |
| Wrong TPM2 PIN | Passphrase fallback |
| Wrong FIDO2 PIN | Passphrase fallback |
| No touch | Passphrase fallback |
| No key plugged in | Passphrase fallback |
| Foreign TPM | Passphrase fallback |
| Confined service reads the PIN config | Denied by SELinux |
| Rotate the payload | Previous keyslot retired, the new one unlocks |
| Remove the enrollment | Only the recovery passphrase keyslot is left |