diff options
Diffstat (limited to 'tests/test_nvpcr.py')
| -rw-r--r-- | tests/test_nvpcr.py | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/tests/test_nvpcr.py b/tests/test_nvpcr.py new file mode 100644 index 0000000..f5765e4 --- /dev/null +++ b/tests/test_nvpcr.py @@ -0,0 +1,146 @@ +import hashlib +import json +import subprocess +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +from scripts import tpm_nvpcr, tpm_nvpcr_unmask + +ROOT = Path(__file__).resolve().parents[1] + + +class NvPCRConfigTests(unittest.TestCase): + def test_hook_adds_required_initrd_services(self): + hook = ROOT / "etc/initcpio/install/nvpcr" + output = subprocess.check_output( + [ + "bash", + "-c", + 'map() { "$@"; }; add_systemd_unit() { printf "%s\\n" "$@"; }; source "$1"; build', + "bash", + str(hook), + ], + text=True, + ).splitlines() + self.assertEqual( + set(output), + { + "systemd-tpm2-setup-early.service", + "systemd-pcrnvdone.service", + "systemd-pcrextend.socket", + "systemd-pcrextend@.service", + }, + ) + self.assertIn( + "etc/initcpio/install/*)", + (ROOT / "run_onchange_after_deploy-etc.sh.tmpl").read_text(), + ) + + def test_deploy_requires_a_signed_recovery_image_and_backup(self): + script = (ROOT / "run_onchange_after_deploy-etc.sh.tmpl").read_text() + self.assertIn("backup/etc.tar", script) + self.assertIn("backup/arch-linux-lts-fallback.efi", script) + self.assertIn("sudo sbverify --cert", script) + + def test_pcr7_policy_is_unchanged(self): + policy = ( + ROOT / "etc/systemd/system/systemd-pcrlock-make-policy.service.d/pcr7.conf" + ) + self.assertIn("--pcr=7 --location=770", policy.read_text()) + for name in ("systemd-tpm2-setup.service", "systemd-tpm2-setup-early.service"): + self.assertFalse((ROOT / "etc/systemd/system" / name).exists()) + + def test_uki_signs_only_the_initrd_referenced_policy(self): + config = (ROOT / "etc/kernel/uki.conf").read_text() + self.assertIn("PolicyRef=initrd", config) + self.assertIn("Phases=enter-initrd", config) + self.assertNotIn("SignInitrdPCRs=", config) + + def test_old_masks_are_not_in_source(self): + for name in ("cryptsetup", "hardware", "login", "verity"): + self.assertFalse((ROOT / f"etc/nvpcr/{name}.nvpcr").exists()) + self.assertFalse((ROOT / "etc/mkinitcpio.conf.d/60-no-nvpcr.conf").exists()) + for name in ("systemd-pcrlogin@.service", "systemd-pcrproduct.service"): + self.assertFalse((ROOT / "etc/systemd/system" / name).exists()) + + +def sections(public_key, *, policies=None, options=None): + if policies is None: + policies = {"sha256": [{"pcrs": [11], "ref": "initrd", "sig": "signature"}]} + if options is None: + options = ( + "tpm2-device=auto,tpm2-measure-pcr=15,tpm2-measure-keyslot-nvpcr=cryptsetup" + ) + return { + ".pcrpkey": {"sha256": hashlib.sha256(public_key).hexdigest()}, + ".pcrsig": {"text": json.dumps(policies)}, + ".cmdline": { + "text": f"rd.luks.name=uuid=root rd.luks.options=uuid={options} root=/dev/mapper/root" + }, + } + + +class NvPCRImageTests(unittest.TestCase): + def test_valid_initrd_signature_and_root_options(self): + tpm_nvpcr.check_sections(sections(b"public key"), b"public key") + + def test_rejects_wrong_public_key(self): + with self.assertRaisesRegex(ValueError, "public key"): + tpm_nvpcr.check_sections(sections(b"one"), b"two") + + def test_rejects_policy_without_initrd_reference(self): + bad = {"sha256": [{"pcrs": [11], "ref": "system", "sig": "signature"}]} + with self.assertRaisesRegex(ValueError, "initrd PCR 11"): + tpm_nvpcr.check_sections(sections(b"key", policies=bad), b"key") + + def test_rejects_unsigned_initrd_policy(self): + bad = {"sha256": [{"pcrs": [11], "ref": "initrd"}]} + with self.assertRaisesRegex(ValueError, "initrd PCR 11"): + tpm_nvpcr.check_sections(sections(b"key", policies=bad), b"key") + + def test_rejects_missing_tpm_selection(self): + with self.assertRaisesRegex(ValueError, "option is missing"): + tpm_nvpcr.check_sections( + sections( + b"key", + options="tpm2-measure-pcr=15,tpm2-measure-keyslot-nvpcr=cryptsetup", + ), + b"key", + ) + + def test_rejects_wrong_root_uuid(self): + image = sections(b"key") + image[".cmdline"]["text"] = image[".cmdline"]["text"].replace( + "rd.luks.options=uuid=", "rd.luks.options=other=" + ) + with self.assertRaisesRegex(ValueError, "wrong LUKS UUID"): + tpm_nvpcr.check_sections(image, b"key") + + +class NvPCRUnmaskTests(unittest.TestCase): + def test_refuses_any_modified_file_before_deleting_masks(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + first = root / "first.nvpcr" + changed = root / "second.nvpcr" + first.touch() + changed.write_text("owned by user") + with self.assertRaisesRegex(ValueError, "not empty"): + tpm_nvpcr_unmask.validate([first, changed]) + self.assertTrue(first.exists()) + + def test_accepts_only_expected_dropin_contents(self): + with tempfile.TemporaryDirectory() as directory: + dropin = Path(directory) / "dropin.conf" + dropin.write_text("changed") + with ( + mock.patch.object(tpm_nvpcr_unmask, "DROPIN", dropin), + self.assertRaisesRegex(ValueError, "was changed"), + ): + tpm_nvpcr_unmask.validate([dropin]) + + +if __name__ == "__main__": + unittest.main() |
