summaryrefslogtreecommitdiffstatshomepage
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_nvpcr.py244
1 files changed, 238 insertions, 6 deletions
diff --git a/tests/test_nvpcr.py b/tests/test_nvpcr.py
index 57b3c81..48464a7 100644
--- a/tests/test_nvpcr.py
+++ b/tests/test_nvpcr.py
@@ -1,3 +1,4 @@
+import base64
import hashlib
import json
import subprocess
@@ -6,7 +7,10 @@ import unittest
from pathlib import Path
from unittest import mock
-from scripts import tpm_nvpcr, tpm_nvpcr_unmask
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+
+from scripts import tpm_negative_uki, tpm_nvpcr, tpm_nvpcr_unmask, tpm_unlock
ROOT = Path(__file__).resolve().parents[1]
@@ -48,11 +52,12 @@ class NvPCRConfigTests(unittest.TestCase):
(ROOT / "run_onchange_after_deploy-etc.sh.tmpl").read_text(),
)
- def test_deploy_requires_a_signed_recovery_image_and_backup(self):
+ def test_deploy_requires_a_signed_main_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)
+ self.assertNotIn("arch-linux-hardened-nvpcr-recovery.efi", script)
def test_pcr7_policy_is_unchanged(self):
policy = (
@@ -62,9 +67,10 @@ class NvPCRConfigTests(unittest.TestCase):
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):
+ def test_uki_signs_initrd_and_root_policies(self):
config = (ROOT / "etc/kernel/uki.conf").read_text()
self.assertIn("PolicyRef=initrd", config)
+ self.assertIn("PolicyRef=luks-root", config)
self.assertIn("Phases=enter-initrd", config)
self.assertNotIn("SignInitrdPCRs=", config)
@@ -78,7 +84,12 @@ class NvPCRConfigTests(unittest.TestCase):
def sections(public_key, *, policies=None, options=None):
if policies is None:
- policies = {"sha256": [{"pcrs": [11], "ref": "initrd", "sig": "signature"}]}
+ policies = {
+ "sha256": [
+ {"pcrs": [11], "ref": ref, "sig": "signature"}
+ for ref in tpm_nvpcr.POLICY_REFS
+ ]
+ }
if options is None:
options = (
"tpm2-device=auto,tpm2-measure-pcr=15,tpm2-measure-keyslot-nvpcr=cryptsetup"
@@ -87,7 +98,7 @@ def sections(public_key, *, policies=None, options=None):
".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"
+ "text": f"rd.luks.name={tpm_nvpcr.ROOT_UUID}=root rd.luks.options={tpm_nvpcr.ROOT_UUID}={options} root=/dev/mapper/root"
},
}
@@ -133,6 +144,186 @@ class NvPCRImageTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "initrd PCR 11"):
tpm_nvpcr.check_sections(sections(b"key", policies=bad), b"key")
+ def test_rejects_missing_root_policy(self):
+ bad = {"sha256": [{"pcrs": [11], "ref": "initrd", "sig": "signature"}]}
+ with self.assertRaisesRegex(ValueError, "luks-root PCR 11"):
+ tpm_nvpcr.check_sections(sections(b"key", policies=bad), b"key")
+
+ def test_pcr_signatures_match_measured_sections(self):
+ key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+ private = key.private_bytes(
+ serialization.Encoding.PEM,
+ serialization.PrivateFormat.PKCS8,
+ serialization.NoEncryption(),
+ )
+ public = key.public_key().public_bytes(
+ serialization.Encoding.PEM,
+ serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ private_path = root / "private.pem"
+ public_path = root / "public.pem"
+ private_path.write_bytes(private)
+ private_path.chmod(0o600)
+ public_path.write_bytes(public)
+ image = {
+ ".linux": b"kernel",
+ ".initrd": b"initrd",
+ ".pcrpkey": public,
+ ".cmdline": b"root=/dev/mapper/root",
+ }
+ args = []
+ for name, content in image.items():
+ path = root / name.removeprefix(".")
+ path.write_bytes(content)
+ args.append(f"--{name.removeprefix('.')}={path}")
+ signatures = []
+ for ref in tpm_nvpcr.POLICY_REFS:
+ result = subprocess.check_output(
+ [
+ "/usr/lib/systemd/systemd-measure",
+ "sign",
+ "--bank=sha256",
+ "--phase=enter-initrd",
+ f"--policyref={ref}",
+ f"--private-key={private_path}",
+ f"--public-key={public_path}",
+ *args,
+ ],
+ text=True,
+ )
+ signatures.extend(json.loads(result)["sha256"])
+ image[".pcrsig"] = json.dumps({"sha256": signatures}).encode()
+ with mock.patch.object(tpm_nvpcr, "PUBLIC_KEY", public_path):
+ tpm_nvpcr.verify_pcr_signatures(image, public)
+ changed = dict(image, **{".initrd": b"changed"})
+ with self.assertRaisesRegex(ValueError, "does not match UKI contents"):
+ tpm_nvpcr.verify_pcr_signatures(changed, public)
+ changed = json.loads(image[".pcrsig"])
+ changed["sha256"][1]["sig"] = base64.b64encode(b"wrong").decode()
+ image[".pcrsig"] = json.dumps(changed).encode()
+ with self.assertRaisesRegex(
+ ValueError, "Invalid luks-root PCR 11 signature"
+ ):
+ tpm_nvpcr.verify_pcr_signatures(image, public)
+
+ def test_ukify_image_signatures_match_its_sections(self):
+ key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ private = root / "private.pem"
+ public = root / "public.pem"
+ private.write_bytes(
+ key.private_bytes(
+ serialization.Encoding.PEM,
+ serialization.PrivateFormat.PKCS8,
+ serialization.NoEncryption(),
+ )
+ )
+ private.chmod(0o600)
+ public.write_bytes(
+ key.public_key().public_bytes(
+ serialization.Encoding.PEM,
+ serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+ )
+ initrd = root / "initrd"
+ initrd.write_bytes(b"test initrd")
+ config = root / "uki.conf"
+ config.write_text(
+ f"[UKI]\nPCRPKey={public}\nPCRBanks=sha256\n\n"
+ + "".join(
+ f"[PCRSignature:{ref}]\nPCRPrivateKey={private}\n"
+ f"PCRPublicKey={public}\nPhases=enter-initrd\nPolicyRef={ref}\n\n"
+ for ref in tpm_nvpcr.POLICY_REFS
+ )
+ )
+ output = root / "test.efi"
+ subprocess.run(
+ [
+ "ukify",
+ "build",
+ "--config",
+ str(config),
+ "--linux=/usr/bin/true",
+ f"--initrd={initrd}",
+ f"--output={output}",
+ ],
+ check=True,
+ capture_output=True,
+ )
+ sb_key = root / "secureboot.key"
+ sb_cert = root / "secureboot.pem"
+ subprocess.run(
+ [
+ "openssl",
+ "req",
+ "-x509",
+ "-newkey",
+ "rsa:2048",
+ "-nodes",
+ "-keyout",
+ str(sb_key),
+ "-out",
+ str(sb_cert),
+ "-days",
+ "1",
+ "-subj",
+ "/CN=UKI test",
+ ],
+ check=True,
+ capture_output=True,
+ )
+ signed = root / "signed.efi"
+ subprocess.run(
+ [
+ "sbsign",
+ "--key",
+ str(sb_key),
+ "--cert",
+ str(sb_cert),
+ "--output",
+ str(signed),
+ str(output),
+ ],
+ check=True,
+ capture_output=True,
+ )
+ with mock.patch.object(tpm_nvpcr, "PUBLIC_KEY", public):
+ tpm_nvpcr.verify_pcr_signatures(
+ tpm_nvpcr.read_pe_sections(signed), public.read_bytes()
+ )
+ negative = root / "negative.efi"
+ tpm_negative_uki.remove_root_approval(
+ signed, negative, public.read_bytes()
+ )
+ changed = tpm_nvpcr.read_pe_sections(negative)
+ tpm_nvpcr.verify_pcr_signatures(
+ changed, public.read_bytes(), refs=("initrd",)
+ )
+ with self.assertRaisesRegex(ValueError, "luks-root"):
+ tpm_nvpcr.verify_pcr_signatures(changed, public.read_bytes())
+ resigned = root / "resigned.efi"
+ subprocess.run(
+ [
+ "sbsign",
+ "--key",
+ str(sb_key),
+ "--cert",
+ str(sb_cert),
+ "--output",
+ str(resigned),
+ str(negative),
+ ],
+ check=True,
+ capture_output=True,
+ )
+ subprocess.run(
+ ["sbverify", "--cert", str(sb_cert), str(resigned)],
+ check=True,
+ )
+
def test_rejects_missing_tpm_selection(self):
with self.assertRaisesRegex(ValueError, "option is missing"):
tpm_nvpcr.check_sections(
@@ -146,7 +337,7 @@ class NvPCRImageTests(unittest.TestCase):
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="
+ f"rd.luks.options={tpm_nvpcr.ROOT_UUID}=", "rd.luks.options=other="
)
with self.assertRaisesRegex(ValueError, "wrong LUKS UUID"):
tpm_nvpcr.check_sections(image, b"key")
@@ -175,5 +366,46 @@ class NvPCRUnmaskTests(unittest.TestCase):
tpm_nvpcr_unmask.validate([dropin])
+class TPMUnlockTests(unittest.TestCase):
+ def test_combined_token_and_trial_slot(self):
+ key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+ public = key.public_key().public_bytes(
+ serialization.Encoding.PEM,
+ serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+ token = {
+ "type": "systemd-tpm2",
+ "keyslots": ["2"],
+ "tpm2-pcrs": [],
+ "tpm2-pcr-bank": "sha256",
+ "tpm2_pcrlock": True,
+ "tpm2_pcrlock_nv": base64.b64encode(b"nv").decode(),
+ "tpm2_pubkey_pcrs": [11],
+ "tpm2_pubkey_ref": "luks-root",
+ "tpm2_pubkey": base64.b64encode(public).decode(),
+ "tpm2-blob": [base64.b64encode(b"blob").decode()] * 2,
+ "tpm2-policy-hash": ["a0" * 32] * 2,
+ }
+ header = {"tokens": {"1": token}, "keyslots": {"0": {}, "1": {}, "2": {}}}
+ self.assertEqual(
+ tpm_unlock.check_header(header, public, trial=True), ("1", "2")
+ )
+ with self.assertRaisesRegex(ValueError, "extra keyslot"):
+ tpm_unlock.check_header(header, public)
+ del header["keyslots"]["1"]
+ self.assertEqual(tpm_unlock.check_header(header, public), ("1", "2"))
+ token["tpm2_pubkey_ref"] = "initrd"
+ with self.assertRaisesRegex(ValueError, "luks-root"):
+ tpm_unlock.check_header(header, public)
+ token["tpm2_pubkey_ref"] = "luks-root"
+ token["tpm2_pcrlock"] = False
+ with self.assertRaisesRegex(ValueError, "pcrlock"):
+ tpm_unlock.check_header(header, public)
+ token["tpm2_pcrlock"] = True
+ token["tpm2-blob"] = token["tpm2-blob"][:1]
+ with self.assertRaisesRegex(ValueError, "two sealed blobs"):
+ tpm_unlock.check_header(header, public)
+
+
if __name__ == "__main__":
unittest.main()