diff options
| author | sommerfeld <sommerfeld@sommerfeld.dev> | 2026-09-30 08:54:59 +0100 |
|---|---|---|
| committer | sommerfeld <sommerfeld@sommerfeld.dev> | 2026-09-30 08:54:59 +0100 |
| commit | 76db86a22a23ccfed07d6160cfdab4ef60067ef0 (patch) | |
| tree | 777d78d3bc017a21a8ba453fbd258c3be947e678 | |
| parent | 78a74ad7b930b2136dbb4b77a0ed545141b4ad2e (diff) | |
| download | dotfiles-76db86a22a23ccfed07d6160cfdab4ef60067ef0.tar.gz dotfiles-76db86a22a23ccfed07d6160cfdab4ef60067ef0.tar.bz2 dotfiles-76db86a22a23ccfed07d6160cfdab4ef60067ef0.zip | |
Mask unused NvPCR measurements on the Arch host
| -rw-r--r-- | docs/tpm-unlock.md | 51 | ||||
| -rw-r--r-- | etc/mkinitcpio.conf.d/60-no-nvpcr.conf | 9 | ||||
| -rw-r--r-- | etc/nvpcr/cryptsetup.nvpcr | 0 | ||||
| -rw-r--r-- | etc/nvpcr/hardware.nvpcr | 0 | ||||
| -rw-r--r-- | etc/nvpcr/login.nvpcr | 0 | ||||
| -rw-r--r-- | etc/nvpcr/verity.nvpcr | 0 | ||||
| -rw-r--r-- | etc/systemd/system/systemd-pcrlogin@.service | 0 | ||||
| -rw-r--r-- | etc/systemd/system/systemd-pcrproduct.service | 0 | ||||
| -rw-r--r-- | tests/test_nvpcr_masks.py | 43 |
9 files changed, 103 insertions, 0 deletions
diff --git a/docs/tpm-unlock.md b/docs/tpm-unlock.md index d499928..0cd915b 100644 --- a/docs/tpm-unlock.md +++ b/docs/tpm-unlock.md @@ -59,6 +59,57 @@ 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. +## Unused NvPCR Measurements + +This host uses PCR 7 for root unlock. It does not use the default `cryptsetup`, +`hardware`, `login`, or `verity` NvPCR measurements. Systemd 262 requires a signed +initrd PCR policy for NvPCR initialization, which this boot setup does not provide. +This is separate from the NV index that stores the `pcrlock` policy. + +Empty files in `etc/nvpcr/` mask the four vendor definitions. Empty unit files +mask only `systemd-pcrproduct.service` and `systemd-pcrlogin@.service`. Keep these +files at zero bytes: adding comments removes the mask. SRK setup and PCR 7 +policy maintenance remain active. No TPM index or LUKS keyslot is deleted. +The mkinitcpio drop-in includes the masks in each rebuilt initramfs. + +Deploy outside aibox, with the recovery passphrase available: + +```sh +just apply +sudo mkinitcpio -P +sudo sbctl sign-all +sudo sbctl verify +``` + +Stop if image generation or signing fails. Confirm that the rebuilt UKIs are +signed before rebooting. No re-enrollment is required. The current boot's failed +setup status can remain until the next boot; do not restart TPM setup merely +to clear that status. + +After an approved reboot, confirm automatic unlock and inspect: + +```sh +sudo journalctl -b --no-pager \ + -u systemd-tpm2-setup-early.service \ + -u systemd-tpm2-setup.service \ + -u systemd-pcrlock-make-policy.service +systemctl --failed --no-pager +systemctl is-enabled systemd-pcrproduct.service systemd-pcrlogin@.service +``` + +The two measurement units must report `masked`. SRK setup must have no NvPCR +initialization failures, and the policy must retain PCR 7 protection. + +To roll back, remove the four `/etc/nvpcr/*.nvpcr` masks listed above, the two +empty unit files, and `/etc/mkinitcpio.conf.d/60-no-nvpcr.conf`. Remove the same +files from the repo to prevent redeployment. Reload systemd, rebuild and sign +the UKIs, and test at the next approved reboot. This restores the vendor +behavior, including its errors if signed initrd PCR policies are still absent. + +Sources: [systemd 262 release notes](https://github.com/systemd/systemd/releases/tag/v262), +[NvPCR setup](https://github.com/systemd/systemd/blob/v262/src/tpm2-setup/tpm2-setup.c), +and [empty-file mask handling](https://github.com/systemd/systemd/blob/v262/src/basic/conf-files.c). + ## Recovery If automatic unlock fails, use the disk passphrase. Inspect the boot journal diff --git a/etc/mkinitcpio.conf.d/60-no-nvpcr.conf b/etc/mkinitcpio.conf.d/60-no-nvpcr.conf new file mode 100644 index 0000000..06b94ff --- /dev/null +++ b/etc/mkinitcpio.conf.d/60-no-nvpcr.conf @@ -0,0 +1,9 @@ +# Keep the unused NvPCR definitions and consumers masked in the initramfs. +FILES+=( + /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 +) diff --git a/etc/nvpcr/cryptsetup.nvpcr b/etc/nvpcr/cryptsetup.nvpcr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/etc/nvpcr/cryptsetup.nvpcr diff --git a/etc/nvpcr/hardware.nvpcr b/etc/nvpcr/hardware.nvpcr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/etc/nvpcr/hardware.nvpcr diff --git a/etc/nvpcr/login.nvpcr b/etc/nvpcr/login.nvpcr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/etc/nvpcr/login.nvpcr diff --git a/etc/nvpcr/verity.nvpcr b/etc/nvpcr/verity.nvpcr new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/etc/nvpcr/verity.nvpcr diff --git a/etc/systemd/system/systemd-pcrlogin@.service b/etc/systemd/system/systemd-pcrlogin@.service new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/etc/systemd/system/systemd-pcrlogin@.service diff --git a/etc/systemd/system/systemd-pcrproduct.service b/etc/systemd/system/systemd-pcrproduct.service new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/etc/systemd/system/systemd-pcrproduct.service diff --git a/tests/test_nvpcr_masks.py b/tests/test_nvpcr_masks.py new file mode 100644 index 0000000..b2ec429 --- /dev/null +++ b/tests/test_nvpcr_masks.py @@ -0,0 +1,43 @@ +import subprocess +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MASKS = [ + *( + f"etc/nvpcr/{name}.nvpcr" + for name in ("cryptsetup", "hardware", "login", "verity") + ), + "etc/systemd/system/systemd-pcrlogin@.service", + "etc/systemd/system/systemd-pcrproduct.service", +] + + +class NvPCRTests(unittest.TestCase): + def test_masks_are_empty_regular_files(self): + for name in MASKS: + path = ROOT / name + self.assertFalse(path.is_symlink()) + self.assertEqual(path.read_bytes(), b"") + + def test_initramfs_keeps_existing_files_and_includes_masks(self): + config = ROOT / "etc/mkinitcpio.conf.d/60-no-nvpcr.conf" + output = subprocess.check_output( + [ + "bash", + "-c", + 'FILES=(existing); source "$1"; printf "%s\\n" "${FILES[@]}"', + "bash", + str(config), + ], + text=True, + ).splitlines() + self.assertEqual(output, ["existing", *(f"/{name}" for name in MASKS)]) + + def test_srk_setup_and_pcr7_policy_remain_enabled(self): + for name in ("systemd-tpm2-setup.service", "systemd-tpm2-setup-early.service"): + self.assertFalse((ROOT / "etc/systemd/system" / name).exists()) + policy = ( + ROOT / "etc/systemd/system/systemd-pcrlock-make-policy.service.d/pcr7.conf" + ) + self.assertIn("--pcr=7 --location=770", policy.read_text()) |
