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 | e2801701c9256d3868df072ddf60d4c82c78dc17 (patch) | |
| tree | f9c7c3b41a2d7d5866bd449ae442ca3427b685c5 | |
| parent | 2772673455da499cb852d9cf921e49621a0ed6a4 (diff) | |
| download | dotfiles-e2801701c9256d3868df072ddf60d4c82c78dc17.tar.gz dotfiles-e2801701c9256d3868df072ddf60d4c82c78dc17.tar.bz2 dotfiles-e2801701c9256d3868df072ddf60d4c82c78dc17.zip | |
Check NvPCR services in staged UKIs
| -rw-r--r-- | docs/tpm-unlock.md | 8 | ||||
| -rw-r--r-- | scripts/tpm_nvpcr.py | 14 | ||||
| -rw-r--r-- | tests/test_nvpcr.py | 15 |
3 files changed, 33 insertions, 4 deletions
diff --git a/docs/tpm-unlock.md b/docs/tpm-unlock.md index 6a95ac7..653b099 100644 --- a/docs/tpm-unlock.md +++ b/docs/tpm-unlock.md @@ -185,10 +185,10 @@ is agreed. Stop if any command fails. /var/lib/dotfiles/tpm-nvpcr/stage/arch-linux-lts-fallback.efi ``` - The check reads `.pcrpkey`, `.pcrsig`, and `.cmdline` using ukify, then - verifies each Secure Boot signature with the enrolled `sbctl` certificate. - Stop on any mismatch. The `systemd-pcrextend` socket and early setup units - must be present in both initramfs variants. + 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 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 diff --git a/scripts/tpm_nvpcr.py b/scripts/tpm_nvpcr.py index 4f0f0a7..14a250c 100644 --- a/scripts/tpm_nvpcr.py +++ b/scripts/tpm_nvpcr.py @@ -21,6 +21,12 @@ MEASUREMENT_OPTIONS = ( "tpm2-measure-pcr=15", "tpm2-measure-keyslot-nvpcr=cryptsetup", ) +INITRD_UNITS = ( + "systemd-tpm2-setup-early.service", + "systemd-pcrnvdone.service", + "systemd-pcrextend.socket", + "systemd-pcrextend@.service", +) def run(*command: str) -> str: @@ -69,6 +75,13 @@ def check_sections(sections: dict, public_key: bytes) -> None: raise ValueError("Root measurement or TPM unlock option is missing") +def check_initrd_listing(listing: str) -> None: + members = {line.strip().lstrip("./") for line in listing.splitlines()} + for unit in INITRD_UNITS: + if f"usr/lib/systemd/system/{unit}" not in members: + raise ValueError(f"UKI initrd lacks {unit}") + + def check_image(path: Path, public_key: bytes) -> None: sections = json.loads( run( @@ -84,6 +97,7 @@ def check_image(path: Path, public_key: bytes) -> None: if not isinstance(sections, dict) or "_profiles" in sections: raise ValueError(f"Unsupported multi-profile UKI: {path}") check_sections(sections, public_key) + check_initrd_listing(run("lsinitcpio", "--list", str(path))) run("sbverify", "--cert", str(SB_CERT), str(path)) print(f"verified: {path}") diff --git a/tests/test_nvpcr.py b/tests/test_nvpcr.py index f5765e4..5f36bb7 100644 --- a/tests/test_nvpcr.py +++ b/tests/test_nvpcr.py @@ -83,6 +83,21 @@ def sections(public_key, *, policies=None, options=None): class NvPCRImageTests(unittest.TestCase): + def test_initrd_has_required_units(self): + listing = "\n".join( + f"usr/lib/systemd/system/{unit}" for unit in tpm_nvpcr.INITRD_UNITS + ) + tpm_nvpcr.check_initrd_listing(listing) + + def test_rejects_missing_initrd_service(self): + listing = "\n".join( + f"usr/lib/systemd/system/{unit}" + for unit in tpm_nvpcr.INITRD_UNITS + if unit != "systemd-pcrnvdone.service" + ) + with self.assertRaisesRegex(ValueError, "systemd-pcrnvdone.service"): + tpm_nvpcr.check_initrd_listing(listing) + def test_valid_initrd_signature_and_root_options(self): tpm_nvpcr.check_sections(sections(b"public key"), b"public key") |
