summaryrefslogtreecommitdiffstatshomepage
path: root/scripts/tpm_nvpcr.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/tpm_nvpcr.py')
-rw-r--r--scripts/tpm_nvpcr.py133
1 files changed, 133 insertions, 0 deletions
diff --git a/scripts/tpm_nvpcr.py b/scripts/tpm_nvpcr.py
new file mode 100644
index 0000000..4f0f0a7
--- /dev/null
+++ b/scripts/tpm_nvpcr.py
@@ -0,0 +1,133 @@
+"""Read-only checks for Halley2's signed NvPCR UKIs."""
+
+import hashlib
+import json
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+BOOT = Path("/boot/EFI/Linux")
+PUBLIC_KEY = Path("/etc/systemd/tpm2-pcr-public-key.pem")
+SB_CERT = Path("/var/lib/sbctl/keys/db/db.pem")
+IMAGES = (
+ "arch-linux-hardened.efi",
+ "arch-linux-hardened-fallback.efi",
+ "arch-linux-lts.efi",
+ "arch-linux-lts-fallback.efi",
+)
+MEASUREMENT_OPTIONS = (
+ "tpm2-device=auto",
+ "tpm2-measure-pcr=15",
+ "tpm2-measure-keyslot-nvpcr=cryptsetup",
+)
+
+
+def run(*command: str) -> str:
+ result = subprocess.run(command, capture_output=True, text=True, check=True)
+ return result.stdout
+
+
+def check_sections(sections: dict, public_key: bytes) -> None:
+ public = sections.get(".pcrpkey")
+ if (
+ not isinstance(public, dict)
+ or public.get("sha256") != hashlib.sha256(public_key).hexdigest()
+ ):
+ raise ValueError("UKI does not embed the expected PCR public key")
+
+ signature = sections.get(".pcrsig")
+ if not isinstance(signature, dict) or not isinstance(signature.get("text"), str):
+ raise TypeError("UKI has no readable PCR signatures")
+ policies = json.loads(signature["text"])
+ if not isinstance(policies, dict) or not any(
+ isinstance(item, dict)
+ and item.get("pcrs") == [11]
+ and item.get("ref") == "initrd"
+ and isinstance(item.get("sig"), str)
+ and bool(item["sig"])
+ for item in policies.get("sha256", [])
+ ):
+ raise ValueError("UKI has no signed initrd PCR 11 policy")
+
+ cmdline = sections.get(".cmdline")
+ if not isinstance(cmdline, dict) or not isinstance(cmdline.get("text"), str):
+ raise TypeError("UKI has no kernel command line")
+ args = cmdline["text"].split()
+ roots = [arg.split("=", 2)[1] for arg in args if arg.startswith("rd.luks.name=")]
+ if len(roots) != 1:
+ raise ValueError("UKI must specify exactly one LUKS root device")
+ options = [
+ arg.removeprefix("rd.luks.options=")
+ for arg in args
+ if arg.startswith("rd.luks.options=")
+ ]
+ if len(options) != 1 or not options[0].startswith(roots[0] + "="):
+ raise ValueError("Root measurement options have the wrong LUKS UUID")
+ selected = options[0].split("=", 1)[1].split(",")
+ if not all(option in selected for option in MEASUREMENT_OPTIONS):
+ raise ValueError("Root measurement or TPM unlock option is missing")
+
+
+def check_image(path: Path, public_key: bytes) -> None:
+ sections = json.loads(
+ run(
+ "ukify",
+ "--json=short",
+ "--section=.pcrpkey:binary",
+ "--section=.pcrsig:text",
+ "--section=.cmdline:text",
+ "inspect",
+ str(path),
+ )
+ )
+ if not isinstance(sections, dict) or "_profiles" in sections:
+ raise ValueError(f"Unsupported multi-profile UKI: {path}")
+ check_sections(sections, public_key)
+ run("sbverify", "--cert", str(SB_CERT), str(path))
+ print(f"verified: {path}")
+
+
+def check_boot() -> None:
+ active = Path("/proc/cmdline").read_text()
+ if not all(option in active for option in MEASUREMENT_OPTIONS):
+ raise ValueError("Current boot did not use the NvPCR root options")
+ nvpcrs = run("systemd-analyze", "nvpcrs", "--no-pager")
+ for name in ("cryptsetup", "hardware", "login", "verity"):
+ if name not in nvpcrs:
+ raise ValueError(f"NvPCR {name} is not available")
+ run(
+ "/usr/lib/systemd/systemd-pcrlock",
+ "--strict=yes",
+ "--pcr=7",
+ "--location=770",
+ "predict",
+ )
+ failed = run("systemctl", "--failed", "--no-legend", "--no-pager")
+ if failed.strip():
+ raise ValueError("Systemd has failed units; inspect systemctl --failed")
+ print(
+ "verified: current boot options, NvPCR availability, and strict PCR 7 prediction"
+ )
+
+
+def main() -> int:
+ if os.geteuid() != 0 or Path("/etc/hostname").read_text().strip() != "halley2":
+ raise ValueError("Run as root on Halley2")
+ public_key = PUBLIC_KEY.read_bytes()
+ if not public_key:
+ raise ValueError("Missing PCR public key")
+ paths = [Path(arg) for arg in sys.argv[1:]] or [BOOT / name for name in IMAGES]
+ for path in paths:
+ check_image(path, public_key)
+ if len(sys.argv) == 1:
+ check_boot()
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(main())
+ except (OSError, TypeError, ValueError, subprocess.CalledProcessError) as exc:
+ print(f"NvPCR check failed: {exc}", file=sys.stderr)
+ sys.exit(1)