diff options
Diffstat (limited to 'docs/tpm-unlock.md')
| -rw-r--r-- | docs/tpm-unlock.md | 429 |
1 files changed, 128 insertions, 301 deletions
diff --git a/docs/tpm-unlock.md b/docs/tpm-unlock.md index 8815639..d60b5e7 100644 --- a/docs/tpm-unlock.md +++ b/docs/tpm-unlock.md @@ -1,351 +1,178 @@ -# TPM Unlock on Arch +# TPM Root Unlock on Halley2 -The root volume uses a systemd TPM enrollment with a `pcrlock` policy that -protects PCR 7. Keep a working disk passphrase and the policy recovery PIN in -a secure location accessible without unlocking this laptop. +Before this migration, the root token requires the approved PCR 7 pcrlock +policy. The new enrollment also requires an RSA signed PCR 11 policy at +`enter-initrd`. The `luks-root` policy reference is for root unlock; the +existing `initrd` reference remains for NvPCR setup. The root volume is +`/dev/nvme0n1p2`. Keep the tested disk passphrase and pcrlock recovery PIN +available away from this laptop. -The policy, enrollment, and generated component files are host state. Do not -copy them into this repo. Deploy this configuration only after policy creation, -enrollment, and an automatic unlock test have succeeded. +The signing key, pcrlock policy, and LUKS metadata are host state. Do not put +them in this repo. Each kernel update must build and Secure Boot sign a UKI +with both PCR 11 signatures. A signed policy approves older UKIs that use the +same signing key and `luks-root` reference; it does not prevent rollback to +an older approved version. -## Deploy +## Prepare -Run on the Arch host, outside aibox: +Run these steps on Halley2, outside aibox. Stop if a check fails. No step in +this section changes the LUKS header. ```sh +pacman -Q systemd systemd-ukify mkinitcpio +sudo cryptsetup open --test-passphrase --key-slot 0 /dev/nvme0n1p2 +sudo /usr/lib/systemd/systemd-pcrlock --strict=yes --pcr=7 --location=770 predict +df -h /boot just apply -sudo systemctl daemon-reload -sudo systemctl enable systemd-pcrlock-make-policy.service -sudo systemctl restart systemd-pcrlock-make-policy.service -sudo journalctl -b -u systemd-pcrlock-make-policy.service --no-pager +sudo mkinitcpio -P +sudo sbctl verify +just tpm-nvpcr-check ``` -The journal must show PCR 7 in the protection mask, or report that the policy -has not changed. Check for errors and unmet conditions. A successful command -exit alone does not prove that a conditional service ran. - -The service runs after root is unlocked. It updates the existing TPM policy -and boot credential from the approved component files. It selects only PCR 7 -and requires the policy files and the mounted EFI System Partition at `/boot`. -On a host without an existing policy, the service skips execution. - -Leave `systemd-pcrlock-secureboot-policy.service` and -`systemd-pcrlock-secureboot-authority.service` disabled. These services generate -component files from the current system. This setup retains the approved -components instead of automatically accepting changes to Secure Boot settings. - -## Updates +`systemd` and `systemd-ukify` must have the same version. The normal +`mkinitcpio -P` build replaces all four maintained UKIs. The checker verifies +their Secure Boot signatures, initrd services, and both PCR 11 signatures +against the measured contents of each image. It does not claim that the LUKS +token has changed. Before enrollment, check that the initrd has no separate +root key file or credential that could bypass the TPM test. -Normal kernel and initramfs updates do not require LUKS re-enrollment. Keep the -existing mkinitcpio and Secure Boot signing hooks. Policy maintenance does not -build or sign UKIs. - -After a systemd update, check the prediction before rebooting: +Back up the header and old TPM token in the encrypted root file system. These +files must stay root owned and must not go to the ESP or the repo. Check the +current LUKS metadata before using the token and slot numbers below. On this +host the existing TPM token is 0, its keyslot is 1, and the passphrase uses +slot 0. Stop if these facts have changed. ```sh -sudo /usr/lib/systemd/systemd-pcrlock --pcr=7 --location=770 predict +sudo cryptsetup luksDump --dump-json-metadata /dev/nvme0n1p2 | jq '{keyslots: (.keyslots|keys), tokens: (.tokens|with_entries(.value |= {type, keyslots, "tpm2-pcrs", tpm2_pcrlock, tpm2_pubkey_pcrs, tpm2_pubkey_ref}))}' +sudo install -d -m 0700 /var/lib/dotfiles/tpm-root +sudo cryptsetup luksHeaderBackup /dev/nvme0n1p2 \ + --header-backup-file /var/lib/dotfiles/tpm-root/before.header +sudo install -m 0600 /dev/null /var/lib/dotfiles/tpm-root/old-token.json +sudo cryptsetup token export --token-id 0 \ + --json-file /var/lib/dotfiles/tpm-root/old-token.json /dev/nvme0n1p2 ``` -If PCR 7 is included and the prediction completes without errors, refresh the -policy and check the journal: +## Enroll + +Enroll a new keyslot with two required TPM policy shards. Do not request a +direct PCR value or a TPM PIN. Enrollment on the running system cannot test +the `enter-initrd` signature because PCR 11 has changed since that phase. ```sh -sudo systemctl restart systemd-pcrlock-make-policy.service -sudo journalctl -b -u systemd-pcrlock-make-policy.service --no-pager +sudo systemd-cryptenroll /dev/nvme0n1p2 \ + --unlock-tpm2-device=auto --tpm2-device=auto --tpm2-pcrs= \ + --tpm2-pcrlock=/var/lib/systemd/pcrlock.json \ + --tpm2-public-key=/etc/systemd/tpm2-pcr-public-key.pem \ + --tpm2-public-key-pcrs=11 \ + --tpm2-public-key-policyref=luks-root --tpm2-with-pin=no +sudo cryptsetup luksDump --dump-json-metadata /dev/nvme0n1p2 | jq '{keyslots: (.keyslots|keys), tokens: (.tokens|with_entries(.value |= {type, keyslots, "tpm2-pcrs", "tpm2-pcr-bank", tpm2_pcrlock, tpm2_pubkey_pcrs, tpm2_pubkey_ref}))}' ``` -Prediction uses the current boot log. It cannot guarantee that changed firmware -or boot software will produce the same measurements on the next boot. Keep the -disk passphrase available. No unattended hook relaxes protection or replaces -LUKS enrollments. - -## NvPCR Measurements - -This setup initializes the four systemd NvPCRs. It records hardware identity, -the logged-in user, and the LUKS root unlock method and keyslot. It also records -the root volume key in ordinary PCR 15. The verity NvPCR has no event on this -host because it does not use dm-verity. Root unlock still uses only the existing -PCR 7 `pcrlock` policy. The NvPCR indices are separate from that policy's index. - -Ukify signs an initrd PCR 11 policy with reference `initrd` and embeds its public -key in each UKI. The `sbctl` mkinitcpio post hook signs the complete UKI for -Secure Boot. Keep the PCR private key outside the repo and EFI System Partition -(ESP). Do not regenerate it during updates. - -### First Activation - -Run these steps on Halley2. Keep the LUKS passphrase and pcrlock recovery PIN -available without unlocking this laptop. Do not reboot until the requested test -is agreed. Stop if any command fails. - -1. Install the declared Arch packages. A full Arch update may rebuild UKIs. If - it does, verify Secure Boot signing and automatic unlock on a separate, - approved reboot before taking the recovery copy below. - - ```sh - sudo pacman -Syu --needed systemd-ukify efibootmgr sbsigntools - pacman -Q systemd systemd-ukify mkinitcpio - sudo sbctl verify - sudo /usr/lib/systemd/systemd-pcrlock --strict=yes --pcr=7 --location=770 predict - df -h /boot - ``` - - `systemd` and `systemd-ukify` must have the same package version. Check that - `/boot` can hold one more complete hardened UKI and temporary replacement - files. Leave enough free space for normal kernel updates. - -2. Save the current boot images and affected `/etc` files in a root-only backup. - Do this once, before `just apply`. Do not overwrite this backup on a retry. - - ```sh - sudo bash -euo pipefail -c ' - backup=/var/lib/dotfiles/tpm-nvpcr/backup - test ! -e "$backup" - install -d -m 0700 "$backup" - cp -a /boot/EFI/Linux/arch-linux-hardened*.efi \ - /boot/EFI/Linux/arch-linux-lts*.efi "$backup/" - tar -C / -cpf "$backup/etc.tar" \ - etc/nvpcr/cryptsetup.nvpcr etc/nvpcr/hardware.nvpcr \ - etc/nvpcr/login.nvpcr etc/nvpcr/verity.nvpcr \ - etc/systemd/system/systemd-pcrlogin@.service \ - etc/systemd/system/systemd-pcrproduct.service \ - etc/mkinitcpio.conf.d/60-no-nvpcr.conf \ - etc/mkinitcpio.conf etc/kernel/cmdline \ - etc/kernel/cmdline-linux-hardened - ' - ``` - -3. Copy and check a working recovery UKI. Use `--create-only` so this entry does - not change the normal EFI boot order. Read its entry number from `efibootmgr -v`. - - ```sh - sudo cp -a /boot/EFI/Linux/arch-linux-hardened.efi \ - /boot/EFI/Linux/arch-linux-hardened-nvpcr-recovery.efi - sudo sbverify --cert /var/lib/sbctl/keys/db/db.pem \ - /boot/EFI/Linux/arch-linux-hardened-nvpcr-recovery.efi - sudo efibootmgr --create-only --disk /dev/nvme0n1 --part 1 \ - --label 'Arch NvPCR Recovery' \ - --loader '\EFI\Linux\arch-linux-hardened-nvpcr-recovery.efi' - sudo efibootmgr -v - ``` - - On an approved reboot, select this entry once with `efibootmgr --bootnext` - or the firmware menu. Confirm automatic unlock. Return to the normal entry. - The recovery copy is outside all mkinitcpio presets. - -4. Create a dedicated PCR signing key pair if neither file exists. Ukify refuses - to replace existing key files. Check permissions and that the keys match. - - ```sh - sudo ukify --pcr-private-key=/etc/systemd/tpm2-pcr-private-key.pem \ - --pcr-public-key=/etc/systemd/tpm2-pcr-public-key.pem genkey - sudo stat -c '%U:%G:%a %n' /etc/systemd/tpm2-pcr-private-key.pem - sudo openssl pkey -in /etc/systemd/tpm2-pcr-private-key.pem -pubout | - sudo cmp - /etc/systemd/tpm2-pcr-public-key.pem - ``` - - The private file must be `root:root:600`. If either file already exists, - verify the pair and do not run `genkey` again. Never copy the private key - into the repo, an initramfs, or the ESP. +Confirm that the new TPM token has a different token ID and keyslot, and +requires `tpm2_pcrlock`, `tpm2_pubkey_pcrs: [11]`, and +`tpm2_pubkey_ref: "luks-root"`. Keep slot 0. If there is no new token, stop. -5. Apply the repo files and remove only the prior masks. The deployment checks - the key pair, ukify version, and recovery image before it changes `/etc`. - Chezmoi does not delete files that were removed from its source directory; - `tpm-nvpcr-unmask` checks their exact old contents before removing them. +Remove only the old token metadata. Its keyslot remains for reversible +rollback until the boot tests pass. The new token is now the only automatic +unlock path. This command assumes the old token is still ID 0. - ```sh - just apply - just tpm-nvpcr-unmask - ``` - -6. Build and sign all four candidate UKIs on the root filesystem. These - commands do not replace the active boot images. - - ```sh - sudo install -d -m 0700 /var/lib/dotfiles/tpm-nvpcr/stage - sudo mkinitcpio -k /boot/vmlinuz-linux-hardened \ - -U /var/lib/dotfiles/tpm-nvpcr/stage/arch-linux-hardened.efi \ - --cmdline /etc/kernel/cmdline-linux-hardened \ - --ukiconfig /etc/kernel/uki.conf - sudo mkinitcpio -k /boot/vmlinuz-linux-hardened \ - -U /var/lib/dotfiles/tpm-nvpcr/stage/arch-linux-hardened-fallback.efi \ - --cmdline /etc/kernel/cmdline-linux-hardened \ - --ukiconfig /etc/kernel/uki.conf -S autodetect - sudo mkinitcpio -k /boot/vmlinuz-linux-lts \ - -U /var/lib/dotfiles/tpm-nvpcr/stage/arch-linux-lts.efi \ - --cmdline /etc/kernel/cmdline --ukiconfig /etc/kernel/uki.conf - sudo mkinitcpio -k /boot/vmlinuz-linux-lts \ - -U /var/lib/dotfiles/tpm-nvpcr/stage/arch-linux-lts-fallback.efi \ - --cmdline /etc/kernel/cmdline --ukiconfig /etc/kernel/uki.conf \ - -S autodetect - just tpm-nvpcr-check \ - /var/lib/dotfiles/tpm-nvpcr/stage/arch-linux-hardened.efi \ - /var/lib/dotfiles/tpm-nvpcr/stage/arch-linux-hardened-fallback.efi \ - /var/lib/dotfiles/tpm-nvpcr/stage/arch-linux-lts.efi \ - /var/lib/dotfiles/tpm-nvpcr/stage/arch-linux-lts-fallback.efi - ``` - - The check reads `.pcrpkey`, `.pcrsig`, and `.cmdline` using ukify. It uses - `lsinitcpio` to check the four required initrd units and verifies each - Secure Boot signature with the enrolled `sbctl` certificate. Stop on any - mismatch. - -7. Copy only the checked hardened default image to a separate ESP path and - create a one-time test entry. Leave the normal and recovery images intact. - Read the new entry number from `efibootmgr -v`; do not assume it is `0002`. - - ```sh - sudo install -m 0644 \ - /var/lib/dotfiles/tpm-nvpcr/stage/arch-linux-hardened.efi \ - /boot/EFI/Linux/arch-linux-hardened-nvpcr-test.efi - just tpm-nvpcr-check /boot/EFI/Linux/arch-linux-hardened-nvpcr-test.efi - sudo efibootmgr --create-only --disk /dev/nvme0n1 --part 1 \ - --label 'Arch NvPCR Test' \ - --loader '\EFI\Linux\arch-linux-hardened-nvpcr-test.efi' - sudo efibootmgr -v - ``` - - Only after these commands succeed, set `BootNext` to that entry and perform - one approved reboot. Keep the disk passphrase available. Confirm automatic - unlock, `BootCurrent` equal to the test entry, and the checks below. The - normal boot order remains unchanged. Do not publish the other images until - the test passes. If the test fails, boot the normal or recovery entry and - inspect the journal before trying again. - -8. After the one-time test passes, copy each checked image to the ESP under a - temporary name, then rename it - into place before copying the next one. This needs space for only one extra - image at a time. Do not reboot after a partial copy. Restore from the backup - if a copy or verification fails. - - ```sh - sudo bash -euo pipefail -c ' - for name in arch-linux-hardened.efi arch-linux-hardened-fallback.efi \ - arch-linux-lts.efi arch-linux-lts-fallback.efi; do - install -m 0644 "/var/lib/dotfiles/tpm-nvpcr/stage/$name" \ - "/boot/EFI/Linux/$name.next" - mv -f "/boot/EFI/Linux/$name.next" "/boot/EFI/Linux/$name" - done - ' - sudo sbctl verify - just tpm-nvpcr-check /boot/EFI/Linux/arch-linux-hardened.efi \ - /boot/EFI/Linux/arch-linux-hardened-fallback.efi \ - /boot/EFI/Linux/arch-linux-lts.efi \ - /boot/EFI/Linux/arch-linux-lts-fallback.efi - ``` +```sh +sudo cryptsetup token remove --token-id 0 /dev/nvme0n1p2 +just tpm-unlock-check --trial +``` -### Boot Checks +## Test -After the one-time test boot, confirm automatic unlock and run: +Boot the normal Hardened entry once. Confirm automatic unlock, then run: ```sh -just tpm-nvpcr-check /boot/EFI/Linux/arch-linux-hardened-nvpcr-test.efi -sudo env SYSTEMD_PAGER=cat /usr/lib/systemd/systemd-pcrlock \ - --strict=yes --pcr=7 --location=770 predict -sudo journalctl -b --no-pager \ - -u systemd-tpm2-setup-early.service \ - -u systemd-pcrnvdone.service \ - -u systemd-pcrproduct.service \ - -u systemd-pcrlogin@1000.service \ - -u systemd-cryptsetup@root.service \ - -u systemd-pcrlock-make-policy.service +efibootmgr | rg 'BootCurrent|BootOrder' +just tpm-nvpcr-check +just tpm-unlock-check --trial systemctl --failed --no-pager -systemd-analyze nvpcrs ``` -The early setup and NvPCR separator must succeed. The `hardware`, `login`, and -`cryptsetup` records must appear after their events; `verity` can stay at its -initial value. Check that PCR 7 is still the only disk-unlock policy PCR. After -publishing all four images, reboot into the normal hardened entry and run -`just tpm-nvpcr-check` with no image arguments. Boot-test hardened and LTS -default and fallback UKIs with explicit approval. -Then rebuild with `sudo mkinitcpio -P` and repeat a normal boot test. Keep the -recovery UKI until these checks pass. +The image check verifies all four maintained UKIs without booting each one. +The token check verifies header metadata. The main boot proves that the +combined token can unlock the root volume. -After the final boot test, verify the disk passphrase without activating a new -mapping. Keyslot 0 is the passphrase slot on Halley2; check the LUKS header if -this test fails. Do not type the passphrase on the command line. +For a refusal test, make a copy of the main UKI without the `luks-root` +approval. The helper keeps the `initrd` approval and the measured sections. +Re-sign the modified image for Secure Boot, then check the new signature. The +unsigned intermediate file remains on the encrypted root file system. ```sh -sudo cryptsetup open --test-passphrase --key-slot 0 /dev/nvme0n1p2 +sudo python3 -m scripts.tpm_negative_uki \ + /boot/EFI/Linux/arch-linux-hardened.efi \ + /var/lib/dotfiles/tpm-root/no-root-approval.unsigned.efi +sudo sbsign --key /var/lib/sbctl/keys/db/db.key \ + --cert /var/lib/sbctl/keys/db/db.pem \ + --output /boot/EFI/Linux/arch-linux-hardened-no-root-approval.efi \ + /var/lib/dotfiles/tpm-root/no-root-approval.unsigned.efi +sudo sbverify --cert /var/lib/sbctl/keys/db/db.pem \ + /boot/EFI/Linux/arch-linux-hardened-no-root-approval.efi +sudo efibootmgr --create-only --disk /dev/nvme0n1 --part 1 \ + --label 'Arch Root Policy Refusal Test' \ + --loader '\EFI\Linux\arch-linux-hardened-no-root-approval.efi' +sudo efibootmgr ``` -If the test succeeds, remove the temporary recovery and test boot entries and -UKIs. Check that `BootCurrent` is 0000 and that entries 0001 and 0002 still -refer to these exact files before running the removal commands. The normal -hardened and LTS entries must stay in `BootOrder`. +Read the new EFI entry number. Set it for one boot with +`sudo efibootmgr --bootnext NNNN`. The test must request the disk passphrase. +Enter it and check the boot journal for the TPM policy failure. If it unlocks +automatically, stop: another automatic path still exists. The normal boot +order must stay unchanged. + +After the refusal test, remove only that test entry and UKI, using its actual +entry number. Boot the normal Hardened entry again if needed. Keep the +passphrase available. ```sh -efibootmgr -v -sudo efibootmgr -b 0001 -B -sudo efibootmgr -b 0002 -B -sudo rm -- /boot/EFI/Linux/arch-linux-hardened-nvpcr-recovery.efi \ - /boot/EFI/Linux/arch-linux-hardened-nvpcr-test.efi -efibootmgr -sudo sbctl verify +sudo efibootmgr -b NNNN -B +sudo rm -- /boot/EFI/Linux/arch-linux-hardened-no-root-approval.efi +sudo rm -- /var/lib/dotfiles/tpm-root/no-root-approval.unsigned.efi just tpm-nvpcr-check +just tpm-unlock-check --trial ``` -The recovery copy is not updated by `mkinitcpio`. Keeping it on the ESP leaves -an old signed boot path that can unlock under the current PCR 7 policy. The -NvPCRs record boot events, but the root TPM enrollment does not require their -values. Removal of these two copies does not prevent booting another old UKI -signed by a trusted Secure Boot key. Keep the disk passphrase and the root-only -backup for recovery from external media. Audit the LUKS TPM tokens separately -before claiming that boot-image measurements are required for unlock. - -### Rollback +## Finish -If a new image does not boot or automatic unlock fails, select the recovery -entry and use the disk passphrase if needed. On the running host, restore the -saved images and exact `/etc` files. Do not remove any TPM indices or keyslots. +When both tests pass, remove only the old, unbound TPM keyslot. This host used +slot 1. Confirm the slot number again before running the command. Do not use a +type-wide `--wipe-slot=tpm2` operation. ```sh -sudo install -d -m 0700 /var/lib/dotfiles/tpm-nvpcr -sudo touch /var/lib/dotfiles/tpm-nvpcr/rollback -sudo bash -euo pipefail -c ' - cp -a /var/lib/dotfiles/tpm-nvpcr/backup/arch-linux-hardened*.efi \ - /var/lib/dotfiles/tpm-nvpcr/backup/arch-linux-lts*.efi /boot/EFI/Linux/ -' -sudo tar -C / -xpf /var/lib/dotfiles/tpm-nvpcr/backup/etc.tar -sudo rm -f /etc/kernel/uki.conf /etc/initcpio/install/nvpcr -sudo systemctl daemon-reload -sudo sbctl verify +sudo cryptsetup luksDump /dev/nvme0n1p2 +sudo cryptsetup luksKillSlot /dev/nvme0n1p2 1 +just tpm-unlock-check +sudo cryptsetup open --test-passphrase --key-slot 0 /dev/nvme0n1p2 +sudo cryptsetup luksHeaderBackup /dev/nvme0n1p2 \ + --header-backup-file /var/lib/dotfiles/tpm-root/final.header +sudo mkinitcpio -P +just tpm-nvpcr-check ``` -Revert this task's repo commit before another `just apply`. The rollback marker -blocks redeployment until then. Keep the key pair and root-only backup for -diagnosis; the temporary recovery UKI is only for first activation. - -Sources: [systemd 262 release notes](https://github.com/systemd/systemd/releases/tag/v262), -[ukify](https://github.com/systemd/systemd/blob/v262/man/ukify.xml), and -[systemd TPM measurement guide](https://github.com/systemd/systemd/blob/v262/docs/TPM2_PCR_MEASUREMENTS.md). +On a later approved reboot, confirm normal automatic unlock. Keep the final +header backup and the passphrase. Remove the pre-migration backup and token +export only after that boot succeeds. ## Recovery -If automatic unlock fails, use the disk passphrase. Inspect the boot journal -and `systemd-pcrlock log` before changing policy. Do not clear the TPM, remove -the policy, or remove password slots. - -After confirming that a Secure Boot change was intended, regenerate its -component files: - -```sh -sudo /usr/lib/systemd/systemd-pcrlock lock-secureboot-policy -sudo /usr/lib/systemd/systemd-pcrlock lock-secureboot-authority -sudo /usr/lib/systemd/systemd-pcrlock --pcr=7 --location=770 predict -``` - -Proceed only if PCR 7 is included. Update the existing policy with its recovery -PIN when the old policy no longer permits updates: +If automatic unlock fails, use the disk passphrase. During the trial, restore +the old token if needed, after checking that token ID 0 is free. This restores +the previous, weaker automatic unlock policy. The old keyslot must still exist. ```sh -sudo /usr/lib/systemd/systemd-pcrlock \ - --pcr=7 --location=770 --recovery-pin=query make-policy +sudo cryptsetup token import --token-id 0 \ + --json-file /var/lib/dotfiles/tpm-root/old-token.json /dev/nvme0n1p2 ``` -Enter the policy recovery PIN, not the disk passphrase. Do not record this -session or share the PIN. Reboot to test automatic unlock. An update to the -existing policy normally needs neither re-enrollment nor a UKI rebuild. +If the LUKS header is damaged, use the saved header and passphrase from an +external recovery system. Header restore replaces all current token and +keyslot changes; do not use it for an ordinary policy failure. Do not clear +the TPM or its NV indices. -Reference: [systemd-pcrlock manual](https://man.archlinux.org/man/systemd-pcrlock.8.en). +For an intended Secure Boot change, review the PCR 7 prediction before +updating its policy. See [systemd-pcrlock](https://man.archlinux.org/man/systemd-pcrlock.8.en), +[systemd-cryptenroll](https://man.archlinux.org/man/systemd-cryptenroll.1.en), +and [cryptsetup-token](https://man.archlinux.org/man/cryptsetup-token.8.en). |
