diff --git a/.gitignore b/.gitignore index 6575226..a9504a0 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ /v7.9.0.tar.gz /v7.12.1.tar.gz /keylime-selinux-42.1.2.tar.gz +/v7.14.1.tar.gz +/keylime-selinux-43.1.1.tar.gz diff --git a/0001-Fix-timestamp-conversion-to-use-UTC-timezone.patch b/0001-Fix-timestamp-conversion-to-use-UTC-timezone.patch new file mode 100644 index 0000000..2d7d76d --- /dev/null +++ b/0001-Fix-timestamp-conversion-to-use-UTC-timezone.patch @@ -0,0 +1,40 @@ +From 7cf07986522fda7691d9135ad4f8d31d030e8b59 Mon Sep 17 00:00:00 2001 +From: Sergio Correia +Date: Fri, 13 Feb 2026 04:46:20 -0500 +Subject: [PATCH 1/2] Fix timestamp conversion to use UTC timezone + +Ensure Unix timestamps are converted to UTC datetimes by passing +tz=timezone.utc to datetime.fromtimestamp(). Previously, timestamps +were converted using the local timezone, causing test failures when +epoch (0) was incorrectly converted to 1969 instead of 1970. + +Signed-off-by: Sergio Correia +--- + keylime/models/base/types/timestamp.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/keylime/models/base/types/timestamp.py b/keylime/models/base/types/timestamp.py +index 8f6782f..22c1fcb 100644 +--- a/keylime/models/base/types/timestamp.py ++++ b/keylime/models/base/types/timestamp.py +@@ -36,7 +36,7 @@ class Timestamp(ModelType): + + if not ts: + try: +- ts = datetime.fromtimestamp(float(value)) ++ ts = datetime.fromtimestamp(float(value), tz=timezone.utc) + except ValueError: + pass + +@@ -49,7 +49,7 @@ class Timestamp(ModelType): + return self._load_datetime(ts) + + def _load_float(self, value: float) -> datetime: +- ts = datetime.fromtimestamp(value) ++ ts = datetime.fromtimestamp(value, tz=timezone.utc) + return self._load_datetime(ts) + + def _load_int(self, value: int) -> datetime: +-- +2.53.0 + diff --git a/0002-Fix-efivar-availability-check-in-test_create_mb_poli.patch b/0002-Fix-efivar-availability-check-in-test_create_mb_poli.patch new file mode 100644 index 0000000..76601af --- /dev/null +++ b/0002-Fix-efivar-availability-check-in-test_create_mb_poli.patch @@ -0,0 +1,36 @@ +From be3243b5f4f3423b8e8e29245a2401e52dd52baf Mon Sep 17 00:00:00 2001 +From: Sergio Correia +Date: Fri, 13 Feb 2026 07:22:46 -0500 +Subject: [PATCH 2/2] Fix efivar availability check in test_create_mb_policy + +Import tpm_bootlog_enrich instead of the elparsing package so the +CDLL("libefivar.so.1") load is actually triggered, allowing tests +to skip gracefully when the library is absent. + +Signed-off-by: Sergio Correia +--- + test/test_create_mb_policy.py | 7 +++++-- + 1 file changed, 5 insertions(+), 2 deletions(-) + +diff --git a/test/test_create_mb_policy.py b/test/test_create_mb_policy.py +index 04ed779..fc79c3b 100644 +--- a/test/test_create_mb_policy.py ++++ b/test/test_create_mb_policy.py +@@ -10,9 +10,12 @@ import unittest + + from keylime.policy import create_mb_policy + +-# Check if efivarlibs is available for measured boot parsing ++# Check if efivarlibs is available for measured boot parsing. ++# We need to import tpm_bootlog_enrich (not just the elparsing package) ++# because the CDLL("libefivar.so.1") load happens at module level in ++# tpm_bootlog_enrich, and importing just the package won't trigger it. + try: +- from keylime.mba import elparsing # pylint: disable=unused-import ++ from keylime.mba.elparsing import tpm_bootlog_enrich # pylint: disable=unused-import + + EFIVAR_AVAILABLE = True + except Exception: +-- +2.53.0 + diff --git a/0002-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch b/0002-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch deleted file mode 100644 index e6f5bef..0000000 --- a/0002-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch +++ /dev/null @@ -1,29 +0,0 @@ -From 52944972182639a625599e29ebe65b91714a3a41 Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Fri, 8 Aug 2025 16:40:01 +0100 -Subject: [PATCH 2/3] mb: support EV_EFI_HANDOFF_TABLES events on PCR1 - -Allow EV_EFI_HANDOFF_TABLES events on PCR1 alongside the existing -EV_EFI_HANDOFF_TABLES2 support to handle different firmware -implementations, in the example policy. - -Signed-off-by: Sergio Correia ---- - keylime/mba/elchecking/example.py | 1 + - 1 file changed, 1 insertion(+) - -diff --git a/keylime/mba/elchecking/example.py b/keylime/mba/elchecking/example.py -index 2c6f699..a3d918a 100644 ---- a/keylime/mba/elchecking/example.py -+++ b/keylime/mba/elchecking/example.py -@@ -185,6 +185,7 @@ class Example(policies.Policy): - # We only expect one EV_NO_ACTION event at the start. - dispatcher.set((0, "EV_NO_ACTION"), tests.OnceTest(tests.AcceptAll())) - dispatcher.set((1, "EV_CPU_MICROCODE"), tests.OnceTest(tests.AcceptAll())) -+ dispatcher.set((1, "EV_EFI_HANDOFF_TABLES"), tests.OnceTest(tests.AcceptAll())) - dispatcher.set((1, "EV_EFI_HANDOFF_TABLES2"), tests.OnceTest(tests.AcceptAll())) - dispatcher.set((0, "EV_S_CRTM_VERSION"), events_final.get("s_crtms")) - dispatcher.set((0, "EV_EFI_PLATFORM_FIRMWARE_BLOB"), events_final.get("platform_firmware_blobs")) --- -2.47.3 - diff --git a/0003-mb-support-vendor_db-as-logged-by-newer-shim-version.patch b/0003-mb-support-vendor_db-as-logged-by-newer-shim-version.patch deleted file mode 100644 index c079994..0000000 --- a/0003-mb-support-vendor_db-as-logged-by-newer-shim-version.patch +++ /dev/null @@ -1,356 +0,0 @@ -From 34bd283113f13c251114507315c647975beede2f Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Fri, 8 Aug 2025 16:41:54 +0100 -Subject: [PATCH 3/3] mb: support vendor_db as logged by newer shim versions - -- Updated example policy to properly handle different event structures - for vendor_db validation: - - KeySubsetMulti for EV_EFI_VARIABLE_DRIVER_CONFIG (has SignatureType field) - - SignatureSetMember for EV_EFI_VARIABLE_AUTHORITY (direct signature format) - -- Added method to extract vendor_db from EV_EFI_VARIABLE_AUTHORITY events - in reference state generation (keylime-policy create measured-boot and - the legacy create_mb_refstate script) -- Made vendor_db optional for backward compatibility - -This fixes attestation failures when vendor_db variables are present but -missing from reference states or validated with incorrect test types. - -See: https://github.com/rhboot/shim/pull/728 -Signed-off-by: Sergio Correia ---- - keylime/mba/elchecking/example.py | 45 +++++++++ - keylime/policy/create_mb_policy.py | 30 ++++++ - scripts/create_mb_refstate | 30 ++++++ - test/test_create_mb_policy.py | 142 +++++++++++++++++++++++++++++ - 4 files changed, 247 insertions(+) - -diff --git a/keylime/mba/elchecking/example.py b/keylime/mba/elchecking/example.py -index a3d918a..5a933ac 100644 ---- a/keylime/mba/elchecking/example.py -+++ b/keylime/mba/elchecking/example.py -@@ -21,6 +21,7 @@ from . import policies, tests - # kek - list of allowed KEK keys - # db - list of allowed db keys - # dbx - list of required dbx keys -+# vendor_db - list of allowed vendor_db keys (optional, for newer shim versions) - # mokdig - list of allowed digests of MoKList (PCR 14 EV_IPL) - # mokxdig - list of allowed digests of MoKListX (PCR 14 EV_IPL) - # kernels - list of allowed { -@@ -121,6 +122,10 @@ class Example(policies.Policy): - if req not in refstate: - raise Exception(f"refstate lacks {req}") - -+ # vendor_db is optional for backward compatibility -+ if "vendor_db" not in refstate: -+ refstate["vendor_db"] = [] -+ - dispatcher = tests.Dispatcher(("PCRIndex", "EventType")) - vd_driver_config = tests.VariableDispatch() - vd_authority = tests.VariableDispatch() -@@ -268,6 +273,34 @@ class Example(policies.Policy): - "db", - db_test, - ) -+ # Support vendor_db as logged by newer shim versions -+ # See: https://github.com/rhboot/shim/pull/728 -+ if not has_secureboot and not refstate["vendor_db"]: -+ vendor_db_test = tests.OnceTest(tests.AcceptAll()) -+ else: -+ vendor_db_test = tests.OnceTest( -+ tests.Or( -+ tests.KeySubsetMulti( -+ ["a159c0a5-e494-a74a-87b5-ab155c2bf072", "2616c4c1-4c50-9240-aca9-41f936934328"], -+ sigs_strip0x(refstate["vendor_db"]), -+ ), -+ tests.KeySubsetMulti( -+ ["a5c059a1-94e4-4aa7-87b5-ab155c2bf072", "c1c41626-504c-4092-aca9-41f936934328"], -+ sigs_strip0x(refstate["vendor_db"]), -+ ), -+ ) -+ ) -+ -+ vd_driver_config.set( -+ "cbb219d7-3a3d-9645-a3bc-dad00e67656f", -+ "vendor_db", -+ vendor_db_test, -+ ) -+ vd_driver_config.set( -+ "d719b2cb-3d3a-4596-a3bc-dad00e67656f", -+ "vendor_db", -+ vendor_db_test, -+ ) - - if not has_secureboot and not refstate["dbx"]: - dbx_test = tests.OnceTest(tests.AcceptAll()) -@@ -295,6 +328,18 @@ class Example(policies.Policy): - vd_db_test = tests.OnceTest(tests.AcceptAll()) - vd_authority.set("cbb219d7-3a3d-9645-a3bc-dad00e67656f", "db", vd_db_test) - vd_authority.set("d719b2cb-3d3a-4596-a3bc-dad00e67656f", "db", vd_db_test) -+ # Support vendor_db as logged by newer shim versions in EV_EFI_VARIABLE_AUTHORITY events -+ # See: https://github.com/rhboot/shim/pull/728 -+ # EV_EFI_VARIABLE_AUTHORITY events have different structure than EV_EFI_VARIABLE_DRIVER_CONFIG -+ # They contain direct signature data without SignatureType field -+ if not has_secureboot and not refstate["vendor_db"]: -+ vendor_db_authority_test = tests.OnceTest(tests.AcceptAll()) -+ else: -+ vendor_db_authority_test = tests.OnceTest( -+ tests.IterateTest(tests.SignatureSetMember(sigs_strip0x(refstate["vendor_db"]))) -+ ) -+ vd_authority.set("cbb219d7-3a3d-9645-a3bc-dad00e67656f", "vendor_db", vendor_db_authority_test) -+ vd_authority.set("d719b2cb-3d3a-4596-a3bc-dad00e67656f", "vendor_db", vendor_db_authority_test) - # Accept all SbatLevels of the Shim, because we already checked the hash of the Shim itself. - vd_sbat_level_test = tests.OnceTest(tests.AcceptAll()) - vd_authority.set("50ab5d60-46e0-0043-abb6-3dd810dd8b23", "SbatLevel", vd_sbat_level_test) -diff --git a/keylime/policy/create_mb_policy.py b/keylime/policy/create_mb_policy.py -index 859e652..b2b48f7 100644 ---- a/keylime/policy/create_mb_policy.py -+++ b/keylime/policy/create_mb_policy.py -@@ -93,6 +93,35 @@ def get_keys(events: List[Dict[str, Any]]) -> Dict[str, List[Any]]: - return out - - -+def get_vendor_db(events: List[Dict[str, Any]]) -> Dict[str, List[Any]]: -+ """Get vendor_db signatures from EV_EFI_VARIABLE_AUTHORITY events.""" -+ out: Dict[str, List[Any]] = {"vendor_db": []} -+ -+ for event in events: -+ if "EventType" not in event: -+ continue -+ if event["EventType"] != "EV_EFI_VARIABLE_AUTHORITY": -+ continue -+ if "Event" not in event or "UnicodeName" not in event["Event"]: -+ continue -+ -+ event_name = event["Event"]["UnicodeName"].lower() -+ if event_name == "vendor_db": -+ data = None -+ if "VariableData" in event["Event"]: -+ data = event["Event"]["VariableData"] -+ -+ if data is not None: -+ # VariableData for EV_EFI_VARIABLE_AUTHORITY is a list of signatures -+ for entry in data: -+ if "SignatureOwner" in entry and "SignatureData" in entry: -+ out["vendor_db"].append( -+ {"SignatureOwner": entry["SignatureOwner"], "SignatureData": f"0x{entry['SignatureData']}"} -+ ) -+ -+ return out -+ -+ - def get_kernel(events: List[Dict[str, Any]], secure_boot: bool) -> Dict[str, List[Dict[str, Any]]]: - """Extract digest for Shim, Grub, Linux Kernel and initrd.""" - out = [] -@@ -259,6 +288,7 @@ def create_mb_refstate(args: argparse.Namespace) -> Optional[Dict[str, object]]: - } - ], - **get_keys(events), -+ **get_vendor_db(events), - **get_mok(events), - **get_kernel(events, has_secureboot), - } -diff --git a/scripts/create_mb_refstate b/scripts/create_mb_refstate -index 23cafb9..c98e61d 100755 ---- a/scripts/create_mb_refstate -+++ b/scripts/create_mb_refstate -@@ -78,6 +78,35 @@ def get_keys(events): - return out - - -+def get_vendor_db(events): -+ """Get vendor_db signatures from EV_EFI_VARIABLE_AUTHORITY events.""" -+ out = {"vendor_db": []} -+ -+ for event in events: -+ if "EventType" not in event: -+ continue -+ if event["EventType"] != "EV_EFI_VARIABLE_AUTHORITY": -+ continue -+ if "Event" not in event or "UnicodeName" not in event["Event"]: -+ continue -+ -+ event_name = event["Event"]["UnicodeName"].lower() -+ if event_name == "vendor_db": -+ data = None -+ if "VariableData" in event["Event"]: -+ data = event["Event"]["VariableData"] -+ -+ if data is not None: -+ # VariableData for EV_EFI_VARIABLE_AUTHORITY is a list of signatures -+ for entry in data: -+ if "SignatureOwner" in entry and "SignatureData" in entry: -+ out["vendor_db"].append( -+ {"SignatureOwner": entry["SignatureOwner"], "SignatureData": f"0x{entry['SignatureData']}"} -+ ) -+ -+ return out -+ -+ - def get_kernel(events, secure_boot): - """ - Extract digest for Shim, Grub, Linux Kernel and initrd. -@@ -197,6 +226,7 @@ def main(): - } - ], - **get_keys(events), -+ **get_vendor_db(events), - **get_mok(events), - **get_kernel(events, has_secureboot), - } -diff --git a/test/test_create_mb_policy.py b/test/test_create_mb_policy.py -index eaed0e3..aa7a4b9 100644 ---- a/test/test_create_mb_policy.py -+++ b/test/test_create_mb_policy.py -@@ -362,6 +362,148 @@ class CreateMeasuredBootPolicy_Test(unittest.TestCase): - for c in test_cases: - self.assertDictEqual(create_mb_policy.get_mok(c["events"]), c["expected"]) - -+ def test_get_vendor_db(self): -+ test_cases = [ -+ {"events": [], "expected": {"vendor_db": []}}, -+ # No EV_EFI_VARIABLE_AUTHORITY events. -+ { -+ "events": [ -+ { -+ "EventType": "EV_EFI_VARIABLE_DRIVER_CONFIG", -+ "Event": {"UnicodeName": "vendor_db", "VariableData": []}, -+ } -+ ], -+ "expected": {"vendor_db": []}, -+ }, -+ # Good vendor_db event with EV_EFI_VARIABLE_AUTHORITY. -+ { -+ "events": [ -+ { -+ "EventType": "EV_EFI_VARIABLE_AUTHORITY", -+ "Event": { -+ "UnicodeName": "vendor_db", -+ "VariableData": [ -+ { -+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", -+ "SignatureData": "sig-data-1", -+ } -+ ], -+ }, -+ } -+ ], -+ "expected": { -+ "vendor_db": [ -+ {"SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", "SignatureData": "0xsig-data-1"} -+ ] -+ }, -+ }, -+ # Multiple vendor_db signatures. -+ { -+ "events": [ -+ { -+ "EventType": "EV_EFI_VARIABLE_AUTHORITY", -+ "Event": { -+ "UnicodeName": "vendor_db", -+ "VariableData": [ -+ { -+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", -+ "SignatureData": "sig-data-1", -+ }, -+ { -+ "SignatureOwner": "77fa9abd-0359-4d32-bd60-28f4e78f784b", -+ "SignatureData": "sig-data-2", -+ }, -+ ], -+ }, -+ } -+ ], -+ "expected": { -+ "vendor_db": [ -+ {"SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", "SignatureData": "0xsig-data-1"}, -+ {"SignatureOwner": "77fa9abd-0359-4d32-bd60-28f4e78f784b", "SignatureData": "0xsig-data-2"}, -+ ] -+ }, -+ }, -+ # Missing EventType. -+ { -+ "events": [ -+ { -+ "Event": { -+ "UnicodeName": "vendor_db", -+ "VariableData": [ -+ { -+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", -+ "SignatureData": "sig-data-1", -+ } -+ ], -+ } -+ } -+ ], -+ "expected": {"vendor_db": []}, -+ }, -+ # Wrong EventType. -+ { -+ "events": [ -+ { -+ "EventType": "EV_EFI_VARIABLE_DRIVER_CONFIG", -+ "Event": { -+ "UnicodeName": "vendor_db", -+ "VariableData": [ -+ { -+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", -+ "SignatureData": "sig-data-1", -+ } -+ ], -+ }, -+ } -+ ], -+ "expected": {"vendor_db": []}, -+ }, -+ # Missing Event. -+ { -+ "events": [{"EventType": "EV_EFI_VARIABLE_AUTHORITY"}], -+ "expected": {"vendor_db": []}, -+ }, -+ # Missing UnicodeName. -+ { -+ "events": [ -+ { -+ "EventType": "EV_EFI_VARIABLE_AUTHORITY", -+ "Event": { -+ "VariableData": [ -+ { -+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", -+ "SignatureData": "sig-data-1", -+ } -+ ] -+ }, -+ } -+ ], -+ "expected": {"vendor_db": []}, -+ }, -+ # Wrong UnicodeName. -+ { -+ "events": [ -+ { -+ "EventType": "EV_EFI_VARIABLE_AUTHORITY", -+ "Event": { -+ "UnicodeName": "db", -+ "VariableData": [ -+ { -+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", -+ "SignatureData": "sig-data-1", -+ } -+ ], -+ }, -+ } -+ ], -+ "expected": {"vendor_db": []}, -+ }, -+ ] -+ -+ for c in test_cases: -+ self.assertDictEqual(create_mb_policy.get_vendor_db(c["events"]), c["expected"]) -+ - def test_get_kernel(self): - test_cases = [ - {"events": [], "secureboot": False, "expected": {}}, --- -2.47.3 - diff --git a/0004-verifier-Gracefully-shutdown-on-signal.patch b/0004-verifier-Gracefully-shutdown-on-signal.patch deleted file mode 100644 index 39f6327..0000000 --- a/0004-verifier-Gracefully-shutdown-on-signal.patch +++ /dev/null @@ -1,42 +0,0 @@ -From c530c332321c1daffa5bfcd08754179012dd21cc Mon Sep 17 00:00:00 2001 -From: Anderson Toshiyuki Sasaki -Date: Mon, 18 Aug 2025 12:12:16 +0000 -Subject: [PATCH 4/7] verifier: Gracefully shutdown on signal - -Wait for the processes to finish when interrupted by a signal. Do not -call exit(0) in the signal handler. - -Assisted-by: Claude 4 Sonnet -Signed-off-by: Anderson Toshiyuki Sasaki ---- - keylime/cloud_verifier_tornado.py | 10 +++++++++- - 1 file changed, 9 insertions(+), 1 deletion(-) - -diff --git a/keylime/cloud_verifier_tornado.py b/keylime/cloud_verifier_tornado.py -index 7553ac8..7065661 100644 ---- a/keylime/cloud_verifier_tornado.py -+++ b/keylime/cloud_verifier_tornado.py -@@ -2138,7 +2138,7 @@ def main() -> None: - revocation_notifier.stop_broker() - for p in processes: - p.join() -- sys.exit(0) -+ # Do not call sys.exit(0) here as it interferes with multiprocessing cleanup - - signal.signal(signal.SIGINT, sig_handler) - signal.signal(signal.SIGTERM, sig_handler) -@@ -2159,3 +2159,11 @@ def main() -> None: - process = Process(target=server_process, args=(task_id, active_agents)) - process.start() - processes.append(process) -+ -+ # Wait for all worker processes to complete -+ try: -+ for p in processes: -+ p.join() -+ except KeyboardInterrupt: -+ # Signal handler will take care of cleanup -+ pass --- -2.47.3 - diff --git a/0005-revocations-Try-to-send-notifications-on-shutdown.patch b/0005-revocations-Try-to-send-notifications-on-shutdown.patch deleted file mode 100644 index dea95ca..0000000 --- a/0005-revocations-Try-to-send-notifications-on-shutdown.patch +++ /dev/null @@ -1,308 +0,0 @@ -From 565889ab6c90823a5096e39a58e9599fa49072f6 Mon Sep 17 00:00:00 2001 -From: Anderson Toshiyuki Sasaki -Date: Wed, 23 Jul 2025 15:39:49 +0200 -Subject: [PATCH 5/7] revocations: Try to send notifications on shutdown - -During verifier shutdown, try to send any pending revocation -notification in a best-effort manner. In future, the pending revocation -notifications should be persisted to be processed during next startup. - -Assisted-by: Claude 4 Sonnet -Signed-off-by: Anderson Toshiyuki Sasaki ---- - keylime/cloud_verifier_tornado.py | 7 + - keylime/revocation_notifier.py | 239 ++++++++++++++++++++++-------- - 2 files changed, 184 insertions(+), 62 deletions(-) - -diff --git a/keylime/cloud_verifier_tornado.py b/keylime/cloud_verifier_tornado.py -index 7065661..89aa703 100644 ---- a/keylime/cloud_verifier_tornado.py -+++ b/keylime/cloud_verifier_tornado.py -@@ -2109,6 +2109,10 @@ def main() -> None: - # Stop server to not accept new incoming connections - server.stop() - -+ # Gracefully shutdown webhook workers to prevent connection errors -+ if "webhook" in revocation_notifier.get_notifiers(): -+ revocation_notifier.shutdown_webhook_workers() -+ - # Wait for all connections to be closed and then stop ioloop - async def stop() -> None: - await server.close_all_connections() -@@ -2136,6 +2140,9 @@ def main() -> None: - def sig_handler(*_: Any) -> None: - if run_revocation_notifier: - revocation_notifier.stop_broker() -+ # Gracefully shutdown webhook workers to prevent connection errors -+ if "webhook" in revocation_notifier.get_notifiers(): -+ revocation_notifier.shutdown_webhook_workers() - for p in processes: - p.join() - # Do not call sys.exit(0) here as it interferes with multiprocessing cleanup -diff --git a/keylime/revocation_notifier.py b/keylime/revocation_notifier.py -index 5a7cc4b..c154028 100644 ---- a/keylime/revocation_notifier.py -+++ b/keylime/revocation_notifier.py -@@ -18,6 +18,174 @@ broker_proc: Optional[Process] = None - - _SOCKET_PATH = "/var/run/keylime/keylime.verifier.ipc" - -+# Global webhook manager instance (initialized when needed) -+_webhook_manager: Optional["WebhookNotificationManager"] = None -+ -+ -+class WebhookNotificationManager: -+ """Manages webhook worker threads and graceful shutdown for revocation notifications.""" -+ -+ def __init__(self) -> None: -+ self._shutdown_event = threading.Event() -+ self._workers: Set[threading.Thread] = set() -+ self._workers_lock = threading.Lock() -+ -+ def notify_webhook(self, tosend: Dict[str, Any]) -> None: -+ """Send webhook notification with worker thread management.""" -+ url = config.get("verifier", "webhook_url", section="revocations", fallback="") -+ # Check if a url was specified -+ if url == "": -+ return -+ -+ # Similarly to notify(), let's convert `tosend' to str to prevent -+ # possible issues with json handling by python-requests. -+ tosend = json.bytes_to_str(tosend) -+ -+ def worker_webhook(tosend: Dict[str, Any], url: str) -> None: -+ is_shutdown_mode = False -+ try: -+ interval = config.getfloat("verifier", "retry_interval") -+ exponential_backoff = config.getboolean("verifier", "exponential_backoff") -+ -+ max_retries = config.getint("verifier", "max_retries") -+ if max_retries <= 0: -+ logger.info("Invalid value found in 'max_retries' option for verifier, using default value") -+ max_retries = 5 -+ -+ # During shutdown, use fewer retries but still make best effort -+ if self._shutdown_event.is_set(): -+ is_shutdown_mode = True -+ max_retries = min(max_retries, 3) # Reduce retries during shutdown but still try -+ logger.info( -+ "Shutdown mode: attempting to send critical revocation notification with %d retries", -+ max_retries, -+ ) -+ -+ # Get TLS options from the configuration -+ (cert, key, trusted_ca, key_password), verify_server_cert = web_util.get_tls_options( -+ "verifier", is_client=True, logger=logger -+ ) -+ -+ # Generate the TLS context using the obtained options -+ tls_context = web_util.generate_tls_context( -+ cert, key, trusted_ca, key_password, is_client=True, logger=logger -+ ) -+ -+ logger.info("Sending revocation event via webhook to %s ...", url) -+ for i in range(max_retries): -+ next_retry = retry.retry_time(exponential_backoff, interval, i, logger) -+ -+ with RequestsClient( -+ url, -+ verify_server_cert, -+ tls_context, -+ ) as client: -+ try: -+ res = client.post("", json=tosend, timeout=5) -+ except requests.exceptions.SSLError as ssl_error: -+ if "TLSV1_ALERT_UNKNOWN_CA" in str(ssl_error): -+ logger.warning( -+ "Keylime does not recognize certificate from peer. Check if verifier 'trusted_server_ca' is configured correctly" -+ ) -+ -+ raise ssl_error from ssl_error -+ except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: -+ # During shutdown, only suppress errors on the final attempt after all retries exhausted -+ if is_shutdown_mode and i == max_retries - 1: -+ logger.warning( -+ "Final attempt to send revocation notification failed during shutdown: %s", e -+ ) -+ return -+ # Otherwise, let the retry logic handle it -+ raise e -+ -+ if res and res.status_code in [200, 202]: -+ if is_shutdown_mode: -+ logger.info("Successfully sent revocation notification during shutdown") -+ break -+ -+ logger.debug( -+ "Unable to publish revocation message %d times via webhook, " -+ "trying again in %d seconds. " -+ "Server returned status code: %s", -+ i + 1, -+ next_retry, -+ res.status_code, -+ ) -+ -+ # During shutdown, use shorter retry intervals to complete faster -+ if is_shutdown_mode: -+ next_retry = min(next_retry, 2.0) # Cap retry interval during shutdown -+ -+ time.sleep(next_retry) -+ -+ except Exception as e: -+ # Only suppress errors during final shutdown phase and log appropriately -+ if is_shutdown_mode: -+ logger.warning("Failed to send revocation notification during shutdown: %s", e) -+ else: -+ logger.error("Error in webhook worker: %s", e) -+ finally: -+ # Remove this worker from the active set -+ current_thread = threading.current_thread() -+ with self._workers_lock: -+ self._workers.discard(current_thread) -+ -+ w = functools.partial(worker_webhook, tosend, url) -+ t = threading.Thread(target=w, daemon=True) -+ -+ # Add this worker to the active set -+ with self._workers_lock: -+ self._workers.add(t) -+ -+ t.start() -+ -+ def shutdown_workers(self) -> None: -+ """Signal webhook workers to shut down gracefully and wait for them to complete. -+ -+ This gives workers time to complete their critical revocation notifications -+ before the service shuts down completely. -+ """ -+ logger.info("Shutting down webhook workers gracefully...") -+ self._shutdown_event.set() -+ -+ # Give workers generous time to complete critical revocation notifications -+ timeout = 30.0 # Increased timeout for critical security notifications -+ end_time = time.time() + timeout -+ -+ with self._workers_lock: -+ workers_to_wait = list(self._workers) -+ -+ if workers_to_wait: -+ logger.info("Waiting for %d webhook workers to complete revocation notifications...", len(workers_to_wait)) -+ -+ for worker in workers_to_wait: -+ remaining_time = max(0, end_time - time.time()) -+ if remaining_time > 0: -+ logger.debug( -+ "Waiting for webhook worker %s to complete (timeout: %.1f seconds)", worker.name, remaining_time -+ ) -+ worker.join(timeout=remaining_time) -+ if worker.is_alive(): -+ logger.warning("Webhook worker %s did not complete within timeout", worker.name) -+ else: -+ logger.warning("Timeout exceeded while waiting for webhook workers") -+ break -+ -+ # Clean up completed workers -+ with self._workers_lock: -+ self._workers.clear() -+ -+ logger.info("Webhook workers shutdown complete") -+ -+ -+def _get_webhook_manager() -> WebhookNotificationManager: -+ """Get the global webhook manager instance, creating it if needed.""" -+ global _webhook_manager -+ if _webhook_manager is None: -+ _webhook_manager = WebhookNotificationManager() -+ return _webhook_manager -+ - - # return the revocation notification methods for cloud verifier - def get_notifiers() -> Set[str]: -@@ -83,6 +251,12 @@ def stop_broker() -> None: - broker_proc.kill() # pylint: disable=E1101 - - -+def shutdown_webhook_workers() -> None: -+ """Convenience function to shutdown webhook workers using the global manager.""" -+ manager = _get_webhook_manager() -+ manager.shutdown_workers() -+ -+ - def notify(tosend: Dict[str, Any]) -> None: - assert "zeromq" in get_notifiers() - try: -@@ -127,68 +301,9 @@ def notify(tosend: Dict[str, Any]) -> None: - - - def notify_webhook(tosend: Dict[str, Any]) -> None: -- url = config.get("verifier", "webhook_url", section="revocations", fallback="") -- # Check if a url was specified -- if url == "": -- return -- -- # Similarly to notify(), let's convert `tosend' to str to prevent -- # possible issues with json handling by python-requests. -- tosend = json.bytes_to_str(tosend) -- -- def worker_webhook(tosend: Dict[str, Any], url: str) -> None: -- interval = config.getfloat("verifier", "retry_interval") -- exponential_backoff = config.getboolean("verifier", "exponential_backoff") -- -- max_retries = config.getint("verifier", "max_retries") -- if max_retries <= 0: -- logger.info("Invalid value found in 'max_retries' option for verifier, using default value") -- max_retries = 5 -- -- # Get TLS options from the configuration -- (cert, key, trusted_ca, key_password), verify_server_cert = web_util.get_tls_options( -- "verifier", is_client=True, logger=logger -- ) -- -- # Generate the TLS context using the obtained options -- tls_context = web_util.generate_tls_context(cert, key, trusted_ca, key_password, is_client=True, logger=logger) -- -- logger.info("Sending revocation event via webhook to %s ...", url) -- for i in range(max_retries): -- next_retry = retry.retry_time(exponential_backoff, interval, i, logger) -- -- with RequestsClient( -- url, -- verify_server_cert, -- tls_context, -- ) as client: -- try: -- res = client.post("", json=tosend, timeout=5) -- except requests.exceptions.SSLError as ssl_error: -- if "TLSV1_ALERT_UNKNOWN_CA" in str(ssl_error): -- logger.warning( -- "Keylime does not recognize certificate from peer. Check if verifier 'trusted_server_ca' is configured correctly" -- ) -- -- raise ssl_error from ssl_error -- -- if res and res.status_code in [200, 202]: -- break -- -- logger.debug( -- "Unable to publish revocation message %d times via webhook, " -- "trying again in %d seconds. " -- "Server returned status code: %s", -- i + 1, -- next_retry, -- res.status_code, -- ) -- -- time.sleep(next_retry) -- -- w = functools.partial(worker_webhook, tosend, url) -- t = threading.Thread(target=w, daemon=True) -- t.start() -+ """Send webhook notification using the global webhook manager.""" -+ manager = _get_webhook_manager() -+ manager.notify_webhook(tosend) - - - cert_key = None --- -2.47.3 - diff --git a/0006-requests_client-close-the-session-at-the-end-of-the-.patch b/0006-requests_client-close-the-session-at-the-end-of-the-.patch deleted file mode 100644 index 7fb869a..0000000 --- a/0006-requests_client-close-the-session-at-the-end-of-the-.patch +++ /dev/null @@ -1,45 +0,0 @@ -From e6fb5090df3e35c7d44bc8f7f37d420d7ee8a05c Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Wed, 4 Jun 2025 19:52:37 +0100 -Subject: [PATCH 6/7] requests_client: close the session at the end of the - resource manager - -We had an issue in the past in which the webhook worker would not -properly close the opened session. This was fixed in #1456 (Close -session in worker_webhook function). - -At some later point, in #1566 (revocation_notifier: Take into account CA -certificates added via configuration), some refactoring around the -webhook_worker() in revocation_notifier happened and it started using -the RequestsClient resource manager. - -However, the RequestsClient does not close the session at its end, which -in turns makes that the old issue of not closing properly the session -in the webhook_worker() returned. - -We now issue a session.close() at the end of the RequestsClient. - -Signed-off-by: Sergio Correia ---- - keylime/requests_client.py | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/keylime/requests_client.py b/keylime/requests_client.py -index 16615f7..b7da484 100644 ---- a/keylime/requests_client.py -+++ b/keylime/requests_client.py -@@ -40,7 +40,10 @@ class RequestsClient: - return self - - def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: -- pass -+ try: -+ self.session.close() -+ except Exception: -+ pass - - def request(self, method: str, url: str, **kwargs: Any) -> requests.Response: - return self.session.request(method, self.base_url + url, **kwargs) --- -2.47.3 - diff --git a/0007-tests-change-test_mba_parsing-to-not-need-keylime-in.patch b/0007-tests-change-test_mba_parsing-to-not-need-keylime-in.patch deleted file mode 100644 index 3937579..0000000 --- a/0007-tests-change-test_mba_parsing-to-not-need-keylime-in.patch +++ /dev/null @@ -1,91 +0,0 @@ -From 39ea2efb72b383f729474a1583d4b8c097cf848a Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Thu, 6 Feb 2025 21:29:56 +0000 -Subject: [PATCH 07/10] tests: change test_mba_parsing to not need keylime - installed - -This test needs the verifier configuration file available, and on -systems that do not have keylime installed (hence, no config file), -it would fail. - -This commit changes the test so that it creates a verifier conf file -in a temporary directory with default values, so that it can use it. - -Signed-off-by: Sergio Correia ---- - test/test_mba_parsing.py | 52 +++++++++++++++++++++++++++++----------- - 1 file changed, 38 insertions(+), 14 deletions(-) - -diff --git a/test/test_mba_parsing.py b/test/test_mba_parsing.py -index 670a602..4ee4e3b 100644 ---- a/test/test_mba_parsing.py -+++ b/test/test_mba_parsing.py -@@ -1,27 +1,51 @@ - import os -+import tempfile - import unittest -+from configparser import RawConfigParser - -+from keylime import config -+from keylime.cmd import convert_config - from keylime.common.algorithms import Hash - from keylime.mba import mba - -+TEMPLATES_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "templates")) -+ - - class TestMBAParsing(unittest.TestCase): - def test_parse_bootlog(self): - """Test parsing binary measured boot event log""" -- mba.load_imports() -- # Use the file that triggered https://github.com/keylime/keylime/issues/1153 -- mb_log_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "data/mb_log.b64")) -- with open(mb_log_path, encoding="utf-8") as f: -- # Read the base64 input and remove the newlines -- b64 = "".join(f.read().splitlines()) -- pcr_hashes, boot_aggregates, measurement_data, failure = mba.bootlog_parse(b64, Hash.SHA256) -- -- self.assertFalse( -- failure, f"Parsing of measured boot log failed with: {list(map(lambda x: x.context, failure.events))}" -- ) -- self.assertTrue(isinstance(pcr_hashes, dict)) -- self.assertTrue(isinstance(boot_aggregates, dict)) -- self.assertTrue(isinstance(measurement_data, dict)) -+ # This test requires the verifier configuration file, so let's create -+ # one with the default values to use, so that we do not depend on the -+ # configuration files existing in the test system. -+ with tempfile.TemporaryDirectory() as config_dir: -+ # Let's write the config file for the verifier. -+ verifier_config = convert_config.process_versions(["verifier"], TEMPLATES_DIR, RawConfigParser(), True) -+ convert_config.output(["verifier"], verifier_config, TEMPLATES_DIR, config_dir) -+ -+ # As we want to use a config file from a different location, the -+ # proper way would be to define an environment variable for the -+ # module of interest, e.g. in our case it would be the -+ # KEYLIME_VERIFIER_CONFIG variable. However, the config module -+ # reads such env vars at first load, and there is no clean way -+ # to have it re-read them, so for this test we will override it -+ # manually. -+ config.CONFIG_ENV["verifier"] = os.path.abspath(os.path.join(config_dir, "verifier.conf")) -+ -+ mba.load_imports() -+ # Use the file that triggered https://github.com/keylime/keylime/issues/1153 -+ mb_log_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "data/mb_log.b64")) -+ with open(mb_log_path, encoding="utf-8") as f: -+ # Read the base64 input and remove the newlines -+ b64 = "".join(f.read().splitlines()) -+ pcr_hashes, boot_aggregates, measurement_data, failure = mba.bootlog_parse(b64, Hash.SHA256) -+ -+ self.assertFalse( -+ failure, -+ f"Parsing of measured boot log failed with: {list(map(lambda x: x.context, failure.events))}", -+ ) -+ self.assertTrue(isinstance(pcr_hashes, dict)) -+ self.assertTrue(isinstance(boot_aggregates, dict)) -+ self.assertTrue(isinstance(measurement_data, dict)) - - - if __name__ == "__main__": --- -2.47.3 - diff --git a/0008-tests-skip-measured-boot-related-tests-for-s390x-and.patch b/0008-tests-skip-measured-boot-related-tests-for-s390x-and.patch deleted file mode 100644 index 14e7247..0000000 --- a/0008-tests-skip-measured-boot-related-tests-for-s390x-and.patch +++ /dev/null @@ -1,53 +0,0 @@ -From 1496567e4b06f7a8eff9f758ea2e4e00ffa89f9b Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Wed, 4 Jun 2025 07:28:54 +0100 -Subject: [PATCH 08/10] tests: skip measured-boot related tests for s390x and - ppc64le - -Signed-off-by: Sergio Correia ---- - test/test_create_mb_policy.py | 2 ++ - test/test_mba_parsing.py | 2 ++ - 2 files changed, 4 insertions(+) - -diff --git a/test/test_create_mb_policy.py b/test/test_create_mb_policy.py -index aa7a4b9..cd32bda 100644 ---- a/test/test_create_mb_policy.py -+++ b/test/test_create_mb_policy.py -@@ -5,6 +5,7 @@ Copyright 2024 Red Hat, Inc. - - import argparse - import os -+import platform - import unittest - - from keylime.policy import create_mb_policy -@@ -12,6 +13,7 @@ from keylime.policy import create_mb_policy - DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "data", "create-mb-policy")) - - -+@unittest.skipIf(platform.machine() in ["ppc64le", "s390x"], "ppc64le and s390x are not supported") - class CreateMeasuredBootPolicy_Test(unittest.TestCase): - def test_event_to_sha256(self): - test_cases = [ -diff --git a/test/test_mba_parsing.py b/test/test_mba_parsing.py -index 4ee4e3b..82e6086 100644 ---- a/test/test_mba_parsing.py -+++ b/test/test_mba_parsing.py -@@ -1,4 +1,5 @@ - import os -+import platform - import tempfile - import unittest - from configparser import RawConfigParser -@@ -11,6 +12,7 @@ from keylime.mba import mba - TEMPLATES_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "templates")) - - -+@unittest.skipIf(platform.machine() in ["ppc64le", "s390x"], "ppc64le and s390x are not supported") - class TestMBAParsing(unittest.TestCase): - def test_parse_bootlog(self): - """Test parsing binary measured boot event log""" --- -2.47.3 - diff --git a/0009-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch b/0009-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch deleted file mode 100644 index 9643ec5..0000000 --- a/0009-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch +++ /dev/null @@ -1,58 +0,0 @@ -From be968fd54198042d2014ad63368b78e9d4609169 Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Thu, 22 May 2025 11:25:15 -0400 -Subject: [PATCH 09/10] tests: fix rpm repo tests from create-runtime-policy - -Signed-off-by: Sergio Correia ---- - .../create-runtime-policy/setup-rpm-tests | 28 +++++++++++++------ - 1 file changed, 20 insertions(+), 8 deletions(-) - -diff --git a/test/data/create-runtime-policy/setup-rpm-tests b/test/data/create-runtime-policy/setup-rpm-tests -index 708438c..b62729b 100755 ---- a/test/data/create-runtime-policy/setup-rpm-tests -+++ b/test/data/create-runtime-policy/setup-rpm-tests -@@ -217,20 +217,32 @@ create_rpm() { - # https://github.com/rpm-software-management/rpm/commit/96467dce18f264b278e17ffe1859c88d9b5aa4b6 - _pkgname="DUMMY-${_name}-${_version}-${_rel}.noarch.rpm" - -- _expected_pkg="${RPMSDIR}/noarch/${_pkgname}" -- [ -e "${_expected_pkg}" ] && return 0 -+ # For some reason, it may not store the built package within the -+ # noarch directory, but directly in RPMS, so let's check both -+ # locations. -+ _expected_pkg="${RPMSDIR}/noarch/${_pkgname} ${RPMSDIR}/${_pkgname}" -+ for _expected in ${_expected_pkg}; do -+ if [ -e "${_expected}" ]; then -+ echo "(create_rpm) CREATED RPM: ${_expected}" >&2 -+ return 0 -+ fi -+ done - - # OK, the package was not built where it should. Let us see if - # it was built in ~/rpmbuild instead, and if that is the case, - # copy it to the expected location. -- _bad_location_pkg="${HOME}/rpmbuild/RPMS/noarch/${_pkgname}" -- if [ -e "${_bad_location_pkg}" ]; then -- echo "WARNING: the package ${_pkgname} was built into ~/rpmbuild despite rpmbuild being instructed to build it at a different location. Probably a fallout from https://github.com/rpm-software-management/rpm/commit/96467dce" >&2 -- install -D -m644 "${_bad_location_pkg}" "${_expected_pkg}" -- return 0 -- fi -+ _bad_location_pkg="${HOME}/rpmbuild/RPMS/noarch/${_pkgname} ${HOME}/rpmbuild/RPMS/${_pkgname}" -+ for _bad_l in ${_bad_location_pkg}; do -+ if [ -e "${_bad_l}" ]; then -+ echo "WARNING: the package ${_pkgname} was built into ~/rpmbuild despite rpmbuild being instructed to build it at a different location. Probably a fallout from https://github.com/rpm-software-management/rpm/commit/96467dce" >&2 -+ install -D -m644 "${_bad_l}" "${RPMSDIR}/noarch/${_pkgname}" -+ echo "(create_rpm) CREATED RPM: ${RPMSDIR}/noarch/${_pkgname}" >&2 -+ return 0 -+ fi -+ done - - # Should not be here. -+ echo "create_rpm() ended with error; probably an issue with the location where the RPMs were built" >&2 - return 1 - } - --- -2.47.3 - diff --git a/0010-mba-normalize-vendor_db-in-EV_EFI_VARIABLE_AUTHORITY.patch b/0010-mba-normalize-vendor_db-in-EV_EFI_VARIABLE_AUTHORITY.patch deleted file mode 100644 index 59cf28c..0000000 --- a/0010-mba-normalize-vendor_db-in-EV_EFI_VARIABLE_AUTHORITY.patch +++ /dev/null @@ -1,281 +0,0 @@ -From 05b694b83ecd62680b64f4a27a95562b87352a46 Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Tue, 19 Aug 2025 20:35:50 +0100 -Subject: [PATCH 10/10] mba: normalize vendor_db in EV_EFI_VARIABLE_AUTHORITY - events - -tpm2_eventlog may provide the vendor_db data as either a parsed signature -list or raw hex bytes, depending on the version used. - -In this commit we add a enrich_vendor_db_authority_variable() function to -make sure we end up with a signature list independent on the format of -the data obtained from tpm2_eventlog. - -Signed-off-by: Sergio Correia ---- - keylime/mba/elparsing/tpm_bootlog_enrich.py | 87 +++++++++++++- - test/test_mba_parsing.py | 120 ++++++++++++++++++++ - 2 files changed, 205 insertions(+), 2 deletions(-) - -diff --git a/keylime/mba/elparsing/tpm_bootlog_enrich.py b/keylime/mba/elparsing/tpm_bootlog_enrich.py -index 4551995..d2df533 100644 ---- a/keylime/mba/elparsing/tpm_bootlog_enrich.py -+++ b/keylime/mba/elparsing/tpm_bootlog_enrich.py -@@ -88,6 +88,18 @@ def getGUID(b: bytes) -> str: - # - ################################################################################## - -+EFI_SIGNATURE_OWNER_SIZE = 16 # Size of SignatureOwner field (GUID). -+ -+# DER (Distinguished Encoding Rules) ASN.1 constants for X.509 certificate parsing. -+# X.509 certificates start with: 0x30 0x82 [length-high] [length-low] [certificate-data...] -+# where 0x30 = SEQUENCE tag, 0x82 = long form length encoding (next 2 bytes = length). -+DER_SEQUENCE_TAG = 0x30 # ASN.1 SEQUENCE tag. -+DER_LONG_LENGTH_FORM = 0x82 # Long form length encoding (2 bytes follow). -+DER_TAG_BYTES = 2 # Bytes needed to check tag + length form (0x30 0x82). -+DER_LENGTH_BYTES = 2 # Length field size in long form encoding. -+DER_HEADER_SIZE = 4 # Total DER header size (tag + length-form + 2-byte length). -+MAX_HEADER_SEARCH_BYTES = 100 # Maximum bytes to search for DER certificate start after GUID. -+ - ################################################################################## - # Parse EFI_SIGNATURE_DATA - ################################################################################## -@@ -95,10 +107,10 @@ def getGUID(b: bytes) -> str: - - def getKey(b: bytes, start: int, size: int) -> Dict[str, Any]: - key = {} -- signatureOwner = getGUID(b[start : start + 16]) -+ signatureOwner = getGUID(b[start : start + EFI_SIGNATURE_OWNER_SIZE]) - key["SignatureOwner"] = signatureOwner - -- signatureData = b[start + 16 : start + size] -+ signatureData = b[start + EFI_SIGNATURE_OWNER_SIZE : start + size] - key["SignatureData"] = signatureData.hex() - return key - -@@ -200,6 +212,73 @@ def enrich_boot_variable(d: Dict[str, Any]) -> None: - d["VariableData"] = k - - -+def enrich_vendor_db_authority_variable(d: Dict[str, Any]) -> None: -+ """Normalize vendor_db in EV_EFI_VARIABLE_AUTHORITY events to signature list format. -+ -+ Different versions of tmp2_eventlog may provide vendor_db data in different formats: -+ - Some versions output hex strings containing raw signature data (GUID + certificate data) -+ - Other versions output parsed signature lists -+ -+ This function ensures we always end up with a list of signatures, regardless of -+ how tpm2_eventlog provided the data. -+ """ -+ # We are only interested in the vendor_db variable, and when it is an hex string. -+ if d.get("UnicodeName") != "vendor_db": -+ return -+ -+ if not isinstance(d.get("VariableData"), str): -+ return -+ -+ try: -+ b = bytes.fromhex(d["VariableData"]) -+ signatures = [] -+ -+ offset = 0 -+ while offset < len(b): -+ if offset + EFI_SIGNATURE_OWNER_SIZE >= len(b): -+ break -+ -+ # Extract GUID at current offset. -+ guid_bytes = b[offset : offset + EFI_SIGNATURE_OWNER_SIZE] -+ guid = getGUID(guid_bytes) -+ -+ # Look for DER certificate signature (SEQUENCE + long form length) after some header data. -+ cert_start = None -+ search_end = min(offset + EFI_SIGNATURE_OWNER_SIZE + MAX_HEADER_SEARCH_BYTES, len(b) - DER_TAG_BYTES) -+ for i in range(offset + EFI_SIGNATURE_OWNER_SIZE, search_end): -+ if b[i] == DER_SEQUENCE_TAG and b[i + 1] == DER_LONG_LENGTH_FORM: -+ cert_start = i -+ break -+ -+ if cert_start is None: -+ break -+ -+ # Parse DER certificate length. -+ if cert_start + DER_HEADER_SIZE > len(b): -+ break -+ -+ cert_length_bytes = b[cert_start + DER_TAG_BYTES : cert_start + DER_HEADER_SIZE] -+ cert_length = (cert_length_bytes[0] << 8) | cert_length_bytes[1] -+ cert_end = cert_start + DER_HEADER_SIZE + cert_length -+ -+ if cert_end > len(b): -+ break -+ -+ # Extract certificate data (from GUID start to end of certificate). -+ sig_data = b[offset + EFI_SIGNATURE_OWNER_SIZE : cert_end] -+ -+ signatures.append({"SignatureOwner": guid, "SignatureData": sig_data.hex()}) -+ -+ # Move to next signature. -+ offset = cert_end -+ -+ if signatures: -+ d["VariableData"] = signatures -+ except Exception: -+ # If parsing fails, leave the hex string unchanged. -+ pass -+ -+ - def enrich(log: Dict[str, Any]) -> None: - """Make the given BIOS boot log easier to understand and process""" - if "events" in log: -@@ -220,6 +299,10 @@ def enrich(log: Dict[str, Any]) -> None: - if "Event" in event: - d = event["Event"] - enrich_boot_variable(d) -+ elif t == "EV_EFI_VARIABLE_AUTHORITY": -+ if "Event" in event: -+ d = event["Event"] -+ enrich_vendor_db_authority_variable(d) - - - def main() -> None: -diff --git a/test/test_mba_parsing.py b/test/test_mba_parsing.py -index 82e6086..04d7afb 100644 ---- a/test/test_mba_parsing.py -+++ b/test/test_mba_parsing.py -@@ -9,6 +9,11 @@ from keylime.cmd import convert_config - from keylime.common.algorithms import Hash - from keylime.mba import mba - -+try: -+ from keylime.mba.elparsing import tpm_bootlog_enrich -+except Exception: -+ unittest.skip(f"tpm_bootlog_enrich not available, architecture ({platform.machine()}) not supported") -+ - TEMPLATES_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "templates")) - - -@@ -49,6 +54,121 @@ class TestMBAParsing(unittest.TestCase): - self.assertTrue(isinstance(boot_aggregates, dict)) - self.assertTrue(isinstance(measurement_data, dict)) - -+ def test_vendor_db_enrichment_actual_hex(self): -+ """Test vendor_db enrichment with actual hex string from real vendor_db event. -+ -+ Different versions of tpm2_eventlog may provide vendor_db data in different formats. -+ This test uses actual hex data and verifies it gets normalized to signature list format. -+ """ -+ # Actual vendor_db hex string from real EV_EFI_VARIABLE_AUTHORITY event. -+ actual_vendor_db_hex = "dbed230279908843af772d65b1c35d3b308203943082027ca00302010202090083730d2b7280d15a300d06092a864886f70d01010b0500305f31163014060355040a0c0d526564204861742c20496e632e3121301f06035504030c18526564204861742053656375726520426f6f7420434120353122302006092a864886f70d0109011613736563616c657274407265646861742e636f6d301e170d3230303630393038313533365a170d3338303131383038313533365a305f31163014060355040a0c0d526564204861742c20496e432e3121301f06035504030c18526564204861742053656375726520426f6f7420434120353122302006092a864886f70d0109011613736563616c657274407265646861742e636f6d30820122300d06092a864886f70d01010105000382010f003082010a0282010100cebaea41171c81a18809bfa1d4a9fa532e9d9ebcfc3b289c3052a00bf4000f36c88341f6a9c915496564d5b2769e58c12e1eeacf93386b47d6ba92c5f800e777a55769df41b1c4905b2d20c174aa038680b6a459efa988445e5240d47715a104859ceff3c69ff30f0fd68446e466dc266ad6d88a6e474acae34c431574997a06328ce033bfe5f846673dea0e943bbf3ddd8bf67f308c45540ba4de23355a997305d880e765141a07302c7386b02da3a636a64d815d91a767bbea3b5b828a9ccf83da31d1543416bc1907172a944ef0cecf0dbaf4fbe4d44889238b8cdc8e4513d77aa8d5e5840313520206c2d590763ab5d7b89d7ab0c9d09869fb8e0d01f5850203010001a3533051301d0603551d0e04160414cc6fa5e72868ba494e939bbd680b9144769a9f8f301f0603551d23041830168014cc6fa5e72868ba494e939bbd680b9144769a9f8f300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101001de75e426a66cc723e9b5cc9afa3ca542eed64abc0b917be27a91e58b1593c4d1174d1971a520584058ad9f085c8f5ec8f9ce9e7086dbb3acbfa6f3c33e6784d75bddfc095729f0350d2752a7cb481e08762945cefcf6bda3ae3bf6e18743455500c22518eaa5830bebd3e304db697b5131b6daf6c183b714a09a18917a7e718f56d51b1d310c80ed6e43219024b1ab2d2dc29a326951d0106e452697806d3304444b07577cc54ade46e2222ff5dff93060cf9983a9c39b70c81d0f3f807a7098b6f9c8ae1adfc419850a65f0bbaa57f1cfc838d06592e9e6ebff43ec31a746625948a5dbf21b6139b9f67f87edc421f4c0edd88737d8c95d03f77c190b864f1" -+ -+ # Expected parsed format - certificate data without the GUID prefix. -+ expected_cert_data = actual_vendor_db_hex[32:] # Skip first 32 chars (16 bytes GUID). -+ expected_parsed_format = [ -+ {"SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", "SignatureData": expected_cert_data} -+ ] -+ -+ # Test event matching the structure from the debug logs. -+ test_event = { -+ "VariableName": "d719b2cb-3d3a-4596-a3bc-dad00e67656f", -+ "UnicodeNameLength": 9, -+ "VariableDataLength": len(actual_vendor_db_hex) // 2, -+ "UnicodeName": "vendor_db", -+ "VariableData": actual_vendor_db_hex, -+ } -+ -+ # Apply vendor_db enrichment. -+ tpm_bootlog_enrich.enrich_vendor_db_authority_variable(test_event) # type: ignore[reportPossiblyUnboundVariable] -+ -+ # Verify that VariableData gets normalized to signature list format. -+ self.assertIsInstance(test_event["VariableData"], list) -+ self.assertEqual(len(test_event["VariableData"]), 1) -+ -+ signature = test_event["VariableData"][0] -+ self.assertIn("SignatureOwner", signature) -+ self.assertIn("SignatureData", signature) -+ # pylint: disable=invalid-sequence-index -+ self.assertEqual(signature["SignatureOwner"], expected_parsed_format[0]["SignatureOwner"]) -+ self.assertEqual(signature["SignatureData"], expected_parsed_format[0]["SignatureData"]) -+ # pylint: enable=invalid-sequence-index -+ -+ def test_vendor_db_enrichment_multiple_certificates_real_data(self): -+ """Test vendor_db enrichment with real data containing multiple certificates. -+ -+ This test uses actual hex data from a real secureboot db variable containing -+ multiple Microsoft certificates to verify correct parsing of complex vendor_db data. -+ """ -+ # Real hex string from secureboot db variable with multiple certificates. -+ # The format of db is similar to vendor_db, so we use it here to test tpm_bootlog_enrich with multiple certificates. -+ real_vendor_db_hex = "a159c0a5e494a74a87b5ab155c2bf0720706000000000000eb050000bd9afa775903324dbd6028f4e78f784b308205d7308203bfa003020102020a61077656000000000008300d06092a864886f70d01010b0500308188310b3009060355040613025553311330110603550408130a57617368696e67746f6e3110300e060355040713075265646d6f6e64311e301c060355040a13154d6963726f736f667420436f72706f726174696f6e31323030060355040313294d6963726f736f667420526f6f7420436572746966696361746520417574686f726974792032303130301e170d3131313031393138343134325a170d3236313031393138353134325a308184310b3009060355040613025553311330110603550408130a57617368696e67746f6e3110300e060355040713075265646d6f6e64311e301c060355040a13154d6963726f736f667420436f72706f726174696f6e312e302c060355040313254d6963726f736f66742057696e646f77732050726f64756374696f6e20504341203230313130820122300d06092a864886f70d01010105000382010f003082010a0282010100dd0cbba2e42e09e3e7c5f79669bc0021bd693333efad04cb5480ee0683bbc52084d9f7d28bf338b0aba4ad2d7c627905ffe34a3f04352070e3c4e76be09cc03675e98a31dd8d70e5dc37b5744696285b8760232cbfdc47a567f751279e72eb07a6c9b91e3b53357ce5d3ec27b9871cfeb9c923096fa84691c16e963c41d3cba33f5d026a4dec691f25285c36fffd43150a94e019b4cfdfc212e2c25b27ee2778308b5b2a096b22895360162cc0681d53baec49f39d618c85680973445d7da2542bdd79f715cf355d6c1c2b5ccebc9c238b6f6eb526d93613c34fd627aeb9323b41922ce1c7cd77e8aa544ef75c0b048765b44318a8b2e06d1977ec5a24fa48030203010001a38201433082013f301006092b06010401823715010403020100301d0603551d0e04160414a92902398e16c49778cd90f99e4f9ae17c55af53301906092b0601040182371402040c1e0a00530075006200430041300b0603551d0f040403020186300f0603551d130101ff040530030101ff301f0603551d23041830168014d5f656cb8fe8a25c6268d13d94905bd7ce9a18c430560603551d1f044f304d304ba049a0478645687474703a2f2f63726c2e6d6963726f736f66742e636f6d2f706b692f63726c2f70726f64756374732f4d6963526f6f4365724175745f323031302d30362d32332e63726c305a06082b06010505070101044e304c304a06082b06010505073002863e687474703a2f2f7777772e6d6963726f736f66742e636f6d2f706b692f63657274732f4d6963526f6f4365724175745f323031302d30362d32332e637274300d06092a864886f70d01010b0500038202010014fc7c7151a579c26eb2ef393ebc3c520f6e2b3f101373fea868d048a6344d8a960526ee3146906179d6ff382e456bf4c0e528b8da1d8f8adb09d71ac74c0a36666a8cec1bd70490a81817a49bb9e240323676c4c15ac6bfe404c0ea16d3acc368ef62acdd546c503058a6eb7cfe94a74e8ef4ec7c867357c2522173345af3a38a56c804da0709edf88be3cef47e8eaef0f60b8a08fb3fc91d727f53b8ebbe63e0e33d3165b081e5f2accd16a49f3da8b19bc242d090845f541dff89eaba1d47906fb0734e419f409f5fe5a12ab21191738a2128f0cede73395f3eab5c60ecdf0310a8d309e9f4f69685b67f51886647198da2b0123d812a680577bb914c627bb6c107c7ba7a8734030e4b627a99e9cafcce4a37c92da4577c1cfe3ddcb80f5afad6c4b30285023aeab3d96ee4692137de81d1f675190567d393575e291b39c8ee2de1cde445735bd0d2ce7aab1619824658d05e9d81b367af6c35f2bce53f24e235a20a7506f6185699d4782cd1051bebd088019daa10f105dfba7e2c63b7069b2321c4f9786ce2581706362b911203cca4d9f22dbaf9949d40ed1845f1ce8a5c6b3eab03d370182a0a6ae05f47d1d5630a32f2afd7361f2a705ae5425908714b57ba7e8381f0213cf41cc1c5b990930e88459386e9b12099be98cbc595a45d62d6a0630820bd7510777d3df345b99f979fcb57806f33a904cf77a4621c597ea159c0a5e494a74a87b5ab155c2bf072da05000000000000be050000bd9afa775903324dbd6028f4e78f784b308205aa30820392a0030201020213330000001a888b9800562284c100000000001a300d06092a864886f70d01010b0500308188310b3009060355040613025553311330110603550408130a57617368696e67746f6e3110300e060355040713075265646d6f6e64311e301c060355040a13154d6963726f736f667420436f72706f726174696f6e31323030060355040313294d6963726f736f667420526f6f7420436572746966696361746520417574686f726974792032303130301e170d3233303631333138353832395a170d3335303631333139303832395a304c310b3009060355040613025553311e301c060355040a13154d6963726f736f667420436f72706f726174696f6e311d301b0603550403131457696e646f77732055454649204341203230323330820122300d06092a864886f70d01010105000382010f003082010a0282010100bcb235d15479b48fcc812a6eb312d69397307c385cbf7992190a0f2d0afebfe0a8d8323fd2ab6f6f81c14d176945cf858027a37cb331cca5a74df943d05a2fd7181bd258960539a395b7bcdd79c1a0cf8fe2531e2b2662a81cae361e4fa1dfb913ba0c25bb24656701aa1d4110b736c16b2eb56c10d34e96d09f2aa1f1eda1150b8295c5ff638a13b592341e315e6111ae5dccf110e64c79c972b2348a82562dab0f7cc04f938e59754186ac091009f2516550b5f521b326398daac491b3dcac642306cd355f0d42499c4f0dce80838259fedf4b44e140c83d63b6cfb4420d395cd242100c08c274eb1cdc6ebc0aac98bbccfa1e3ca78316c5db02dad996df6b0203010001a382014630820142300e0603551d0f0101ff040403020186301006092b06010401823715010403020100301d0603551d0e04160414aefc5fbbbe055d8f8daa585473499417ab5a5272301906092b0601040182371402040c1e0a00530075006200430041300f0603551d130101ff040530030101ff301f0603551d23041830168014d5f656cb8fe8a25c6268d13d94905bd7ce9a18c430560603551d1f044f304d304ba049a0478645687474703a2f2f63726c2e6d6963726f736f66742e636f6d2f706b692f63726c2f70726f64756374732f4d6963526f6f4365724175745f323031302d30362d32332e63726c305a06082b06010505070101044e304c304a06082b06010505073002863e687474703a2f2f7777772e6d6963726f736f66742e636f6d2f706b692f63657274732f4d6963526f6f4365724175745f323031302d30362d32332e637274300d06092a864886f70d01010b050003820201009fc9b6ff6ee19c3b55f6fe8b39dd61046fd0ad63cd17764aa843898df8c6f28c5e90e1e468a515ecb8d3600c40571ffb5e357261de97316c79a0f516ae4b1ced010ceff7570f42301869f8a1a32e9792b8be1bfe2b865e4242118f8e704d90a7fd0163f264bf9be27b0881cf49f23717dff1f972d3c31dc390454de68006bdfde56a69ceb37e4e315b8473a8e8723f2735c97c20ce009b4fe04cb43669cbf734111174127aa88c2e816ca650ad19faa846456fb16773c36be340e82a698f2410e1296e8d1688ee8e7f6693026f5b9e048ccc811cad9754f1182e7e5290bc51de2a0eae66eabc646ea09164e42f12a8bce76bbac71b9b791a6466f143b4d1c346213881794cfaf0310dd379ff7a12a51dd9ddaca20f7182f793ff5ca161ae65f21481ed795a9a87ea607bcbb34f7534cabaa1efa2f6a28045a18b2781cdd577383eca4edd28ea58bac5a029de868c88fc952751ddabd3d05b0d77c76c8f55d7d4a20e5be4344614161de31cd66d99ad4cec71732fabceb2b429de553053393a328bf0ea9c88123b056819bfcf875210fbd61360f34164f4085781cb9d11a58ef4e527f5a33aece43d4ab7cef9880d9fbdca6dd24abc58768e3204946eddf4cf6d476dc2d76adc8771eaa4bfef67979cb8c780362a2a59c9c00ca744a073b58ccf385aaef8bb8695f044ad667a33ed71e4458783e5a7cea240d072d24800faf91aa159c0a5e494a74a87b5ab155c2bf072400600000000000024060000bd9afa775903324dbd6028f4e78f784b308205aa30820392a0030201020213330000001a888b9800562284c100000000001a300d06092a864886f70d01010b0500308188310b3009060355040613025553311330110603550408130a57617368696e67746f6e3110300e060355040713075265646d6f6e64311e301c060355040a13154d6963726f736f667420436f72706f726174696f6e31323030060355040313294d6963726f736f667420526f6f7420436572746966696361746520417574686f726974792032303130301e170d3233303631333138353832395a170d3335303631333139303832395a304c310b3009060355040613025553311e301c060355040a13154d6963726f736f667420436f72706f726174696f6e311d301b0603550403131457696e646f77732055454649204341203230323330820122300d06092a864886f70d01010105000382010f003082010a0282010100bcb235d15479b48fcc812a6eb312d69397307c385cbf7992190a0f2d0afebfe0a8d8323fd2ab6f6f81c14d176945cf858027a37cb331cca5a74df943d05a2fd7181bd258960539a395b7bcdd79c1a0cf8fe2531e2b2662a81cae361e4fa1dfb913ba0c25bb24656701aa1d4110b736c16b2eb56c10d34e96d09f2aa1f1eda1150b8295c5ff638a13b592341e315e6111ae5dccf110e64c79c972b2348a82562dab0f7cc04f938e59754186ac091009f2516550b5f521b326398daac491b3dcac642306cd355f0d42499c4f0dce80838259fedf4b44e140c83d63b6cfb4420d395cd242100c08c274eb1cdc6ebc0aac98bbccfa1e3ca78316c5db02dad996df6b0203010001a382014630820142300e0603551d0f0101ff040403020186301006092b06010401823715010403020100301d0603551d0e04160414aefc5fbbbe055d8f8daa585473499417ab5a5272301906092b0601040182371402040c1e0a00530075006200430041300f0603551d130101ff040530030101ff301f0603551d23041830168014d5f656cb8fe8a25c6268d13d94905bd7ce9a18c430560603551d1f044f304d304ba049a0478645687474703a2f2f63726c2e6d6963726f736f66742e636f6d2f706b692f63726c2f70726f64756374732f4d6963526f6f4365724175745f323031302d30362d32332e63726c305a06082b06010505070101044e304c304a06082b06010505073002863e687474703a2f2f7777772e6d6963726f736f66742e636f6d2f706b692f63657274732f4d6963526f6f4365724175745f323031302d30362d32332e637274300d06092a864886f70d01010b050003820201009fc9b6ff6ee19c3b55f6fe8b39dd61046fd0ad63cd17764aa843898df8c6f28c5e90e1e468a515ecb8d3600c40571ffb5e357261de97316c79a0f516ae4b1ced010ceff7570f42301869f8a1a32e9792b8be1bfe2b865e4242118f8e704d90a7fd0163f264bf9be27b0881cf49f23717dff1f972d3c31dc390454de68006bdfde56a69ceb37e4e315b8473a8e8723f2735c97c20ce009b4fe04cb43669cbf734111174127aa88c2e816ca650ad19faa846456fb16773c36be340e82a698f2410e1296e8d1688ee8e7f6693026f5b9e048ccc811cad9754f1182e7e5290bc51de2a0eae66eabc646ea09164e42f12a8bce76bbac71b9b791a6466f143b4d1c346213881794cfaf0310dd379ff7a12a51dd9ddaca20f7182f793ff5ca161ae65f21481ed795a9a87ea607bcbb34f7534cabaa1efa2f6a28045a18b2781cdd577383eca4edd28ea58bac5a029de868c88fc952751ddabd3d05b0d77c76c8f55d7d4a20e5be4344614161de31cd66d99ad4cec71732fabceb2b429de553053393a328bf0ea9c88123b056819bfcf875210fbd61360f34164f4085781cb9d11a58ef4e527f5a33aece43d4ab7cef9880d9fbdca6dd24abc58768e3204946eddf4cf6d476dc2d76adc8771eaa4bfef67979cb8c780362a2a59c9c00ca744a073b58ccf385aaef8bb8695f044ad667a33ed71e4458783e5a7cea240d072d24800faf91aa159c0a5e494a74a87b5ab155c2bf072d405000000000000b8050000bd9afa775903324dbd6028f4e78f784b308205a43082038ca0030201020213330000001636bf36899f1575cc000000000016300d06092a864886f70d01010b0500305a310b3009060355040613025553311e301c060355040a13154d6963726f736f667420436f72706f726174696f6e312b3029060355040313224d6963726f736f667420525341204465766963657320526f6f742043412032303231301e170d3233303631333139323134375a170d3338303631333139333134375a304e310b3009060355040613025553311e301c060355040a13154d6963726f736f667420436f72706f726174696f6e311f301d060355040313164d6963726f736f66742055454649204341203230323330820122300d06092a864886f70d01010105000382010f003082010a0282010100bd222aaeef1a3185137851a79bfdfc78d163b81a9b63f51206db4b41356a6fabf56a04cc97cfbbd408091a613a0de6b3a046ff09adde8024dc1280f25fd916ede2429dcd2f4d6102618a1c4b1d186239869771ad3e7f5d71134be92a00c1bed5b7009f5e65b22c1aff74edea83d239893335737da0a2fa40e4665058aafc87e85c208334ecabe20bc55f3eff482b119126ef186e57c59f187399efe16a742bbb2f7f508e1dda3d76b604e5cc2e10c7831b83a3e4a51313716e3378a3a83cec48265ec7c65e0d879aaacc553481ad9d90f5e69663a6e8072017c8931ed2aea4dcae7d59bf885e620cae5bf22940561d2640de85a6ad56d1cf5547765f9c39db030203010001a382016d30820169300e0603551d0f0101ff040403020186301006092b06010401823715010403020100301d0603551d0e0416041481aa6b3244c935bce0d6628af39827421e32497d301906092b0601040182371402040c1e0a00530075006200430041300f0603551d130101ff040530030101ff301f0603551d230418301680148444860600983f2caab3c589f3ac2ec9e69d090330650603551d1f045e305c305aa058a0568654687474703a2f2f7777772e6d6963726f736f66742e636f6d2f706b696f70732f63726c2f4d6963726f736f667425323052534125323044657669636573253230526f6f742532304341253230323032312e63726c307206082b0601050507010104663064306206082b060105050730028656687474703a2f2f7777772e6d6963726f736f66742e636f6d2f706b696f70732f63657274732f4d6963726f736f667425323052534125323044657669636573253230526f6f742532304341253230323032312e637274300d06092a864886f70d01010b050003820201000760132a5387120f1af35a149517e5d8d795549b8b0edd91a5edc75d47509345b795885f1719416376b582b0a8c59d9915368949be12c266fb830cb081cee5a4abc2a09aebf5073cfe21f89adc19210c9e242cd15ca2160a4bebec489cb15b74db0164c2e3806aab1acd771b6a399ab7ba7044ff6794c58106f0cb810493272199bd8788149c22710e0b2f5cbeb890547cc01ebc2b9ba356174b97e7e37f1334fab0346b9bf6b22df7d87bd820d35ca7954c4f2af9e71e68affc6c8fc8863d9fc8d1ef4d1ac8d1f6fd2d7ce3e841c1ea27c1fb8e25865a89a610becee38fa57bc41aa0e87590fd21b0c1a3c516235e3cce2ffe8c98bf085cf6b9c5b23cb6ccc8ec7fd27774cbedf396c98b8d1c2a890fa38fbdce2a85469a23a28f42c099d6ea851f6119be1635b775a09580650687d40b35c8c4aa0ecea20a6360ca4b2b5c270482af3e58837a5ad8673f1053f50c16f7264b8a80b9c51fa0ded8d361441445a7f5ab9a8817fdb79454028be4b753a13e8d9e5082a800e078941bbeb3c4301fb20edbf04690c1e657fe7cc170b21c4b64d910031b34fb66cf826e9e40a81137f2658b2109af3c93623df3bc83dd3f559015d231af11e7f8caa082e1b9cfb35793c75537ac7f41bf1f963cf32694f9d8d255248a8ab641f0e016c023928c710a4c6a0d1955f73a9c922196a1d5f80a8c9dbfc9ebca8842fc4bb4efff27302161" -+ -+ # Test event structure matching EV_EFI_VARIABLE_AUTHORITY format. -+ test_event = { -+ "VariableName": "d719b2cb-3d3a-4596-a3bc-dad00e67656f", -+ "UnicodeNameLength": 9, -+ "VariableDataLength": len(real_vendor_db_hex) // 2, -+ "UnicodeName": "vendor_db", -+ "VariableData": real_vendor_db_hex, -+ } -+ -+ # Apply vendor_db enrichment -+ tpm_bootlog_enrich.enrich_vendor_db_authority_variable(test_event) # type: ignore[reportPossiblyUnboundVariable] -+ -+ # Verify enrichment results. -+ self.assertIsInstance(test_event["VariableData"], list) -+ # Real data should contain multiple certificates (4 in this case based on the structure). -+ self.assertGreater(len(test_event["VariableData"]), 1, "Real vendor_db should contain multiple certificates") -+ -+ # Verify each certificate has the required structure. -+ for i, signature in enumerate(test_event["VariableData"]): -+ with self.subTest(certificate=i): -+ self.assertIn("SignatureOwner", signature, f"Certificate {i} missing SignatureOwner") -+ self.assertIn("SignatureData", signature, f"Certificate {i} missing SignatureData") -+ -+ # Verify SignatureOwner is a valid GUID format. -+ guid = signature["SignatureOwner"] -+ self.assertRegex( -+ guid, -+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", -+ f"Certificate {i} has invalid GUID format: {guid}", -+ ) -+ -+ # Verify SignatureData is hex string and not empty. -+ cert_data = signature["SignatureData"] -+ self.assertIsInstance(cert_data, str, f"Certificate {i} SignatureData should be string") -+ self.assertGreater(len(cert_data), 0, f"Certificate {i} SignatureData should not be empty") -+ # Verify it's valid hex. -+ try: -+ bytes.fromhex(cert_data) -+ except ValueError: -+ self.fail(f"Certificate {i} SignatureData is not valid hex: {cert_data[:100]}...") -+ -+ def test_vendor_db_enrichment_preserves_signature_lists(self): -+ """Test that enrichment preserves VariableData that's already in signature list format""" -+ # VariableData that is already in the expected signature list format. -+ signature_list_format = [ -+ { -+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", -+ "SignatureData": "308203943082027ca00302010202090083730d2b7280d15a300d06092a864886f70d01010b0500305f31163014060355040a0c0d526564204861742c20496e632e3121301f06035504030c18526564204861742053656375726520426f6f7420434120353122302006092a864886f70d0109011613736563616c657274407265646861742e636f6d301e170d3230303630393038313533365a170d3338303131383038313533365a305f31163014060355040a0c0d526564204861742c20496e632e3121301f06035504030c18526564204861742053656375726520426f6f7420434120353122302006092a864886f70d0109011613736563616c657274407265646861742e636f6d30820122300d06092a864886f70d01010105000382010f003082010a0282010100cebaea41171c81a18809bfa1d4a9fa532e9d9ebcfc3b289c3052a00bf4000f36c88341f6a9c915496564d5b2769e58c12e1eeacf93386b47d6ba92c5f800e777a55769df41b1c4905b2d20c174aa038680b6a459efa988445e5240d47715a104859ceff3c69ff30f0fd68446e466dc266ad6d88a6e474acae34c431574997a06328ce033bfe5f846673dea0e943bbf3ddd8bf67f308c45540ba4de23355a997305d880e765141a07302c7386b02da3a636a64d815d91a767bbea3b5b828a9ccf83da31d1543416bc1907172a944ef0cecf0dbaf4fbe4d44889238b8cdc8e4513d77aa8d5e5840313520206c2d590763ab5d7b89d7ab0c9d09869fb8e0d01f5850203010001a3533051301d0603551d0e04160414cc6fa5e72868ba494e939bbd680b9144769a9f8f301f0603551d23041830168014cc6fa5e72868ba494e939bbd680b9144769a9f8f300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101001de75e426a66cc723e9b5cc9afa3ca542eed64abc0b917be27a91e58b1593c4d1174d1971a520584058ad9f085c8f5ec8f9ce9e7086dbb3acbfa6f3c33e6784d75bddfc095729f0350d2752a7cb481e08762945cefcf6bda3ae3bf6e18743455500c22518eaa5830bebd3e304db697b5131b6daf6c183b714a09a18917a7e718f56d51b1d310c80ed6e43219024b1ab2d2dc29a326951d0106e452697806d3304444b07577cc54ade46e2222ff5dff93060cf9983a9c39b70c81d0f3f807a7098b6f9c8ae1adfc419850a65f0bbaa57f1cfc838d06592e9e6ebff43ec31a746625948a5dbf21b6139b9f67f87edc421f4c0edd88737d8c95d03f77c190b864f1", -+ } -+ ] -+ -+ # Test event with VariableData already in signature list format. -+ test_event = { -+ "VariableName": "d719b2cb-3d3a-4596-a3bc-dad00e67656f", -+ "UnicodeName": "vendor_db", -+ "VariableData": signature_list_format.copy(), -+ } -+ -+ original_data = test_event["VariableData"].copy() -+ -+ # Apply enrichment -+ tpm_bootlog_enrich.enrich_vendor_db_authority_variable(test_event) # type: ignore[reportPossiblyUnboundVariable] -+ -+ # Verify that VariableData in signature list format remains unchanged -+ self.assertEqual(test_event["VariableData"], original_data) -+ - - if __name__ == "__main__": - unittest.main() --- -2.47.3 - diff --git a/0011-fix-malformed-certs-workaround.patch b/0011-fix-malformed-certs-workaround.patch deleted file mode 100644 index 1891260..0000000 --- a/0011-fix-malformed-certs-workaround.patch +++ /dev/null @@ -1,1304 +0,0 @@ -From 531a07ce041426efc762afb48abcd0e1abaa90ec Mon Sep 17 00:00:00 2001 -From: Anderson Toshiyuki Sasaki -Date: Mon, 8 Sep 2025 13:55:24 +0200 -Subject: [PATCH] Avoid re-encoding certificate stored in DB - -The previous attempt (see 85b2bf6eeb2326b1a8b28848c32d442c0f464d6f) to -fix the re-encoding of malformed certificates that do not strictly -follow ASN.1 encoding rules, but has a valid certificate signature, was -incorrect because the cached original certificate would affect all -certificate objects. In that case, the cache was stored in the model -definition, which is shared between all certificate object instances. - -This changes the approach by creating a new class `CertificateWrapper` -that holds the original bytes in a cache in the object itself. This -makes the cache from a certificate to be independent from the cache of -another certificate. - -The python-cryptography ASN.1 parser is strict and does not accept -malformed certificates. This makes it impossible to use some affected -devices, notably TPM certificates from Nuvoton. To workaround this, the -certificate is re-encoded using pyasn1, but this effectively modify the -certificate making its signature invalid. To avoid storing invalid -certificates to the database, the original bits of the certificate (as -received from the agent) are cached and later used when storing the -certificate into the database. - -Assisted-by: Claude -Signed-off-by: Anderson Toshiyuki Sasaki ---- - keylime/certificate_wrapper.py | 99 +++++ - keylime/models/base/types/certificate.py | 70 ++-- - keylime/models/registrar/registrar_agent.py | 25 +- - test/test_certificate_modeltype.py | 197 ++++++++++ - test/test_certificate_wrapper.py | 385 +++++++++++++++++++ - test/test_registrar_agent_cert_compliance.py | 289 ++++++++++++++ - 6 files changed, 1029 insertions(+), 36 deletions(-) - create mode 100644 keylime/certificate_wrapper.py - create mode 100644 test/test_certificate_modeltype.py - create mode 100644 test/test_certificate_wrapper.py - create mode 100644 test/test_registrar_agent_cert_compliance.py - -diff --git a/keylime/certificate_wrapper.py b/keylime/certificate_wrapper.py -new file mode 100644 -index 000000000..899a19a8d ---- /dev/null -+++ b/keylime/certificate_wrapper.py -@@ -0,0 +1,99 @@ -+""" -+X.509 Certificate wrapper that preserves original bytes for malformed certificates. -+ -+This module provides a wrapper around cryptography.x509.Certificate that preserves -+the original certificate bytes when the certificate required pyasn1 re-encoding -+due to ASN.1 DER non-compliance. This ensures signature validity is maintained -+throughout the database lifecycle. -+""" -+ -+import base64 -+from typing import Any, Dict, Optional -+ -+import cryptography.x509 -+from cryptography.hazmat.primitives.serialization import Encoding -+ -+ -+class CertificateWrapper: -+ """ -+ A wrapper around cryptography.x509.Certificate that preserves original bytes -+ when malformed certificates require pyasn1 re-encoding. -+ -+ This class wraps a cryptography.x509.Certificate and adds the ability -+ to store the original certificate bytes when the certificate was malformed -+ and required re-encoding using pyasn1. This ensures that signature validation -+ works correctly even for certificates that don't strictly follow ASN.1 DER. -+ """ -+ -+ def __init__(self, cert: cryptography.x509.Certificate, original_bytes: Optional[bytes] = None): -+ """ -+ Initialize the wrapper certificate. -+ -+ :param cert: The cryptography.x509.Certificate object -+ :param original_bytes: The original DER bytes if certificate was re-encoded, None otherwise -+ """ -+ self._cert = cert -+ self._original_bytes = original_bytes -+ -+ def __getattr__(self, name: str) -> Any: -+ """Delegate attribute access to the wrapped certificate.""" -+ return getattr(self._cert, name) -+ -+ def __setstate__(self, state: Dict[str, Any]) -> None: -+ """Support for pickling.""" -+ self.__dict__.update(state) -+ -+ def __getstate__(self) -> Dict[str, Any]: -+ """Support for pickling.""" -+ return self.__dict__ -+ -+ @property -+ def has_original_bytes(self) -> bool: -+ """Check if this certificate has preserved original bytes.""" -+ return self._original_bytes is not None -+ -+ @property -+ def original_bytes(self) -> Optional[bytes]: -+ """Return the preserved original bytes if available.""" -+ return self._original_bytes -+ -+ def public_bytes(self, encoding: Encoding) -> bytes: -+ """ -+ Return certificate bytes, using original bytes when available. -+ -+ For certificates with preserved original bytes, this method always uses -+ the original DER bytes to maintain signature validity. For PEM encoding, -+ it converts the original DER bytes to PEM format. -+ """ -+ if self.has_original_bytes: -+ if encoding == Encoding.DER: -+ return self._original_bytes # type: ignore[return-value] -+ if encoding == Encoding.PEM: -+ # Convert original DER bytes to PEM format -+ der_b64 = base64.b64encode(self._original_bytes).decode("utf-8") # type: ignore[arg-type] -+ # Split into 64-character lines per PEM specification (RFC 1421) -+ lines = [der_b64[i : i + 64] for i in range(0, len(der_b64), 64)] -+ # Create PEM format with proper headers -+ pem_content = "\n".join(["-----BEGIN CERTIFICATE-----"] + lines + ["-----END CERTIFICATE-----"]) + "\n" -+ return pem_content.encode("utf-8") -+ -+ # For certificates without original bytes, use standard method -+ return self._cert.public_bytes(encoding) -+ -+ # Delegate common certificate methods to maintain full compatibility -+ def __str__(self) -> str: -+ return f"CertificateWrapper(subject={self._cert.subject})" -+ -+ def __repr__(self) -> str: -+ return f"CertificateWrapper(subject={self._cert.subject}, has_original_bytes={self.has_original_bytes})" -+ -+ -+def wrap_certificate(cert: cryptography.x509.Certificate, original_bytes: Optional[bytes] = None) -> CertificateWrapper: -+ """ -+ Factory function to create a wrapped certificate. -+ -+ :param cert: The cryptography.x509.Certificate object -+ :param original_bytes: The original DER bytes if certificate was re-encoded -+ :returns: Wrapped certificate that preserves original bytes -+ """ -+ return CertificateWrapper(cert, original_bytes) -diff --git a/keylime/models/base/types/certificate.py b/keylime/models/base/types/certificate.py -index 2c27603ba..bf2d62f9d 100644 ---- a/keylime/models/base/types/certificate.py -+++ b/keylime/models/base/types/certificate.py -@@ -12,6 +12,7 @@ - from pyasn1_modules import rfc2459 as pyasn1_rfc2459 - from sqlalchemy.types import Text - -+from keylime.certificate_wrapper import CertificateWrapper, wrap_certificate - from keylime.models.base.type import ModelType - - -@@ -78,19 +79,20 @@ def _schema(self): - cert = Certificate().cast("-----BEGIN CERTIFICATE-----\nMIIE...") - """ - -- IncomingValue: TypeAlias = Union[cryptography.x509.Certificate, bytes, str, None] -+ IncomingValue: TypeAlias = Union[cryptography.x509.Certificate, CertificateWrapper, bytes, str, None] - - def __init__(self) -> None: - super().__init__(Text) - -- def _load_der_cert(self, der_cert_data: bytes) -> cryptography.x509.Certificate: -- """Loads a binary x509 certificate encoded using ASN.1 DER as a ``cryptography.x509.Certificate`` object. This -+ def _load_der_cert(self, der_cert_data: bytes) -> CertificateWrapper: -+ """Loads a binary x509 certificate encoded using ASN.1 DER as a ``CertificateWrapper`` object. This - method does not require strict adherence to ASN.1 DER thereby making it possible to accept certificates which do - not follow every detail of the spec (this is the case for a number of TPM certs) [1,2]. - - It achieves this by first using the strict parser provided by python-cryptography. If that fails, it decodes the - certificate and re-encodes it using the more-forgiving pyasn1 library. The re-encoded certificate is then -- re-parsed by python-cryptography. -+ re-parsed by python-cryptography. For malformed certificates requiring re-encoding, the original bytes are -+ preserved in the wrapper to maintain signature validity. - - This method is equivalent to the ``cert_utils.x509_der_cert`` function but does not produce a warning when the - backup parser is used, allowing this condition to be optionally detected and handled by the model where -@@ -106,24 +108,28 @@ def _load_der_cert(self, der_cert_data: bytes) -> cryptography.x509.Certificate: - - :raises: :class:`SubstrateUnderrunError`: cert could not be deserialized even using the fallback pyasn1 parser - -- :returns: A ``cryptography.x509.Certificate`` object -+ :returns: A ``CertificateWrapper`` object - """ - - try: -- return cryptography.x509.load_der_x509_certificate(der_cert_data) -+ cert = cryptography.x509.load_der_x509_certificate(der_cert_data) -+ return wrap_certificate(cert, None) - except Exception: - pyasn1_cert = pyasn1_decoder.decode(der_cert_data, asn1Spec=pyasn1_rfc2459.Certificate())[0] -- return cryptography.x509.load_der_x509_certificate(pyasn1_encoder.encode(pyasn1_cert)) -+ cert = cryptography.x509.load_der_x509_certificate(pyasn1_encoder.encode(pyasn1_cert)) -+ # Preserve the original bytes when re-encoding is necessary -+ return wrap_certificate(cert, der_cert_data) - -- def _load_pem_cert(self, pem_cert_data: str) -> cryptography.x509.Certificate: -+ def _load_pem_cert(self, pem_cert_data: str) -> CertificateWrapper: - """Loads a text x509 certificate encoded using PEM (Base64ed DER with header and footer) as a -- ``cryptography.x509.Certificate`` object. This method does not require strict adherence to ASN.1 DER thereby -+ ``CertificateWrapper`` object. This method does not require strict adherence to ASN.1 DER thereby - making it possible to accept certificates which do not follow every detail of the spec (this is the case for - a number of TPM certs) [1,2]. - - It achieves this by first using the strict parser provided by python-cryptography. If that fails, it decodes the - certificate and re-encodes it using the more-forgiving pyasn1 library. The re-encoded certificate is then -- re-parsed by python-cryptography. -+ re-parsed by python-cryptography. For malformed certificates requiring re-encoding, the original DER bytes are -+ preserved in the wrapper to maintain signature validity. - - This method is equivalent to the ``cert_utils.x509_der_cert`` function but does not produce a warning when the - backup parser is used, allowing this condition to be optionally detected and handled by the model where -@@ -135,19 +141,24 @@ def _load_pem_cert(self, pem_cert_data: str) -> cryptography.x509.Certificate: - [2] https://github.com/pyca/cryptography/issues/7189 - [3] https://github.com/keylime/keylime/issues/1559 - -- :param der_cert_data: the DER bytes of the certificate -+ :param pem_cert_data: the PEM text of the certificate - - :raises: :class:`SubstrateUnderrunError`: cert could not be deserialized even using the fallback pyasn1 parser - -- :returns: A ``cryptography.x509.Certificate`` object -+ :returns: A ``CertificateWrapper`` object - """ - - try: -- return cryptography.x509.load_pem_x509_certificate(pem_cert_data.encode("utf-8")) -+ cert = cryptography.x509.load_pem_x509_certificate(pem_cert_data.encode("utf-8")) -+ return wrap_certificate(cert, None) - except Exception: - der_data = pyasn1_pem.readPemFromFile(io.StringIO(pem_cert_data)) - pyasn1_cert = pyasn1_decoder.decode(der_data, asn1Spec=pyasn1_rfc2459.Certificate())[0] -- return cryptography.x509.load_der_x509_certificate(pyasn1_encoder.encode(pyasn1_cert)) -+ cert = cryptography.x509.load_der_x509_certificate(pyasn1_encoder.encode(pyasn1_cert)) -+ # Only preserve original bytes if we have valid DER data -+ original_bytes = der_data if isinstance(der_data, bytes) and der_data else None -+ # Preserve the original bytes when re-encoding is necessary -+ return wrap_certificate(cert, original_bytes) - - def infer_encoding(self, value: IncomingValue) -> Optional[str]: - """Tries to infer the certificate encoding from the given value based on the data type and other surface-level -@@ -159,15 +170,21 @@ def infer_encoding(self, value: IncomingValue) -> Optional[str]: - :returns: ``"der"`` when the value appears to be DER encoded - :returns: ``"pem"`` when the value appears to be PEM encoded - :returns: ``"base64"`` when the value appears to be Base64(DER) encoded (without PEM headers) -+ :returns: ``"wrapped"`` when the value is already a ``CertificateWrapper`` object - :returns: ``"decoded"`` when the value is already a ``cryptography.x509.Certificate`` object -+ :returns: ``"disabled"`` when the value is the string "disabled" - :returns: ``None`` when the encoding cannot be inferred - """ - # pylint: disable=no-else-return - -- if isinstance(value, cryptography.x509.Certificate): -+ if isinstance(value, CertificateWrapper): -+ return "wrapped" -+ elif isinstance(value, cryptography.x509.Certificate): - return "decoded" - elif isinstance(value, bytes): - return "der" -+ elif isinstance(value, str) and value == "disabled": -+ return "disabled" - elif isinstance(value, str) and value.startswith("-----BEGIN CERTIFICATE-----"): - return "pem" - elif isinstance(value, str): -@@ -190,18 +207,24 @@ def asn1_compliant(self, value: IncomingValue) -> Optional[bool]: - :param value: The value in DER, Base64(DER), or PEM format (or an already deserialized certificate object) - - :returns: ``"True"`` if the value can be deserialized by python-cryptography and is ASN.1 DER compliant -+ :returns: ``"True"`` if the value is the string "disabled" (considered compliant as it's a valid field value) - :returns: ``"False"`` if the value cannot be deserialized by python-cryptography - :returns: ``None`` if the value is already a deserialized certificate of type ``cryptography.x509.Certificate`` - """ - - try: - match self.infer_encoding(value): -+ case "wrapped": -+ # For CertificateWrapper objects, check if they have original bytes (indicating re-encoding was needed) -+ return not value.has_original_bytes # type: ignore[union-attr] - case "decoded": - return None -+ case "disabled": -+ return True - case "der": - cryptography.x509.load_der_x509_certificate(value) # type: ignore[reportArgumentType, arg-type] - case "pem": -- cryptography.x509.load_pem_x509_certificate(value) # type: ignore[reportArgumentType, arg-type] -+ cryptography.x509.load_pem_x509_certificate(value.encode("utf-8")) # type: ignore[reportArgumentType, arg-type, union-attr] - case "base64": - der_value = base64.b64decode(value, validate=True) # type: ignore[reportArgumentType, arg-type] - cryptography.x509.load_der_x509_certificate(der_value) -@@ -212,24 +235,27 @@ def asn1_compliant(self, value: IncomingValue) -> Optional[bool]: - - return True - -- def cast(self, value: IncomingValue) -> Optional[cryptography.x509.Certificate]: -+ def cast(self, value: IncomingValue) -> Optional[CertificateWrapper]: - """Tries to interpret the given value as an X.509 certificate and convert it to a -- ``cryptography.x509.Certificate`` object. Values which do not require conversion are returned unchanged. -+ ``CertificateWrapper`` object. Values which do not require conversion are returned unchanged. - - :param value: The value to convert (may be in DER, Base64(DER), or PEM format) - - :raises: :class:`TypeError`: ``value`` is of an unexpected data type - :raises: :class:`ValueError`: ``value`` does not contain data which is interpretable as a certificate - -- :returns: A ``cryptography.x509.Certificate`` object or None if an empty value is given -+ :returns: A ``CertificateWrapper`` object or None if an empty value is given - """ - - if not value: - return None - - match self.infer_encoding(value): -+ case "wrapped": -+ return value # type: ignore[return-value] - case "decoded": -- return value # type: ignore[reportReturnType, return-value] -+ # Wrap raw cryptography certificate without original bytes -+ return wrap_certificate(value, None) # type: ignore[arg-type] - case "der": - try: - return self._load_der_cert(value) # type: ignore[reportArgumentType, arg-type] -@@ -269,7 +295,6 @@ def _dump(self, value: IncomingValue) -> Optional[str]: - if not cert: - return None - -- # Save as Base64-encoded value (without the PEM "BEGIN" and "END" header/footer for efficiency) - return base64.b64encode(cert.public_bytes(Encoding.DER)).decode("utf-8") - - def render(self, value: IncomingValue) -> Optional[str]: -@@ -279,9 +304,8 @@ def render(self, value: IncomingValue) -> Optional[str]: - if not cert: - return None - -- # Render certificate in PEM format - return cert.public_bytes(Encoding.PEM).decode("utf-8") # type: ignore[no-any-return] - - @property - def native_type(self) -> type: -- return cryptography.x509.Certificate -+ return CertificateWrapper -diff --git a/keylime/models/registrar/registrar_agent.py b/keylime/models/registrar/registrar_agent.py -index 560c18838..fc7e1be87 100644 ---- a/keylime/models/registrar/registrar_agent.py -+++ b/keylime/models/registrar/registrar_agent.py -@@ -1,7 +1,6 @@ - import base64 - import hmac - --import cryptography.x509 - from cryptography.hazmat.primitives.asymmetric import ec, rsa - from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat - -@@ -116,35 +115,35 @@ def _check_cert_trust_status(self, cert_field, cert_type=""): - if not cert_utils.verify_cert(cert, trust_store, cert_type): - self._add_error(cert_field, "must contain a certificate issued by a CA present in the trust store") - -- def _check_cert_compliance(self, cert_field, raw_cert): -+ def _check_cert_compliance(self, cert_field): - new_cert = self.changes.get(cert_field) - old_cert = self.values.get(cert_field) - - # If the certificate field has not been changed, no need to perform check -- if not raw_cert or not new_cert: -+ if not new_cert: -+ return True -+ -+ # If the certificate field is set as "disabled" (for mtls_cert) -+ if new_cert == "disabled": - return True - - # If the new certificate value is the same as the old certificate value, no need to perform check -- if ( -- isinstance(new_cert, cryptography.x509.Certificate) -- and isinstance(old_cert, cryptography.x509.Certificate) -- and new_cert.public_bytes(Encoding.DER) == old_cert.public_bytes(Encoding.DER) -- ): -+ if old_cert and new_cert.public_bytes(Encoding.DER) == old_cert.public_bytes(Encoding.DER): - return True - -- compliant = Certificate().asn1_compliant(raw_cert) -+ compliant = Certificate().asn1_compliant(new_cert) - - if not compliant: - if config.get("registrar", "malformed_cert_action") == "reject": -- self._add_error(cert_field, Certificate().generate_error_msg(raw_cert)) -+ self._add_error(cert_field, Certificate().generate_error_msg(new_cert)) - - return compliant - -- def _check_all_cert_compliance(self, data): -+ def _check_all_cert_compliance(self): - non_compliant_certs = [] - - for field_name in ("ekcert", "iak_cert", "idevid_cert", "mtls_cert"): -- if not self._check_cert_compliance(field_name, data.get(field_name)): -+ if not self._check_cert_compliance(field_name): - non_compliant_certs.append(f"'{field_name}'") - - if not non_compliant_certs: -@@ -290,7 +289,7 @@ def update(self, data): - # Ensure either an EK or IAK/IDevID is present, depending on configuration - self._check_root_identity_presence() - # Handle certificates which are not fully compliant with ASN.1 DER -- self._check_all_cert_compliance(data) -+ self._check_all_cert_compliance() - - # Basic validation of values - self.validate_required(["aik_tpm"]) -diff --git a/test/test_certificate_modeltype.py b/test/test_certificate_modeltype.py -new file mode 100644 -index 000000000..335ae0fc8 ---- /dev/null -+++ b/test/test_certificate_modeltype.py -@@ -0,0 +1,197 @@ -+""" -+Unit tests for the Certificate ModelType class. -+ -+This module tests the certificate model type functionality including -+encoding inference and ASN.1 compliance checking. -+""" -+ -+import base64 -+import unittest -+ -+import cryptography.x509 -+from cryptography.hazmat.primitives.serialization import Encoding -+ -+from keylime.certificate_wrapper import CertificateWrapper, wrap_certificate -+from keylime.models.base.types.certificate import Certificate -+ -+ -+class TestCertificateModelType(unittest.TestCase): -+ """Test cases for Certificate ModelType class.""" -+ -+ def setUp(self): -+ """Set up test fixtures.""" -+ self.cert_type = Certificate() -+ -+ # Compliant certificate for testing (loads fine with python-cryptography) -+ self.compliant_cert_pem = """-----BEGIN CERTIFICATE----- -+MIIClzCCAX+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARUZXN0 -+MB4XDTI1MDkxMTEyNDU1MVoXDTI2MDkxMTEyNDU1MVowDzENMAsGA1UEAwwEVGVz -+dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO2V27HsMnKczHCaLgf9 -+FtxuorvkA5OMkz6KsW1eyryHr0TJ801prLpeNnMZ3U4pqLMqocMc7T2KO6nPZJxO -+7zRzehyo9pBBVO4pUR1QMGoTWuJQbqNieDQ4V9dW67N5wp/UWEkK6CNNd6aXjswb -+dVaDbIfDL8hMX6Lil3+pTysRWGqjRvBGJxS9r/mYRAvbz1JHPjfegSc0uxnUE+qZ -+SrbWa3TN82LX6jw6tKk0Z3CcPJC6QN+ijCxxAoHyLRYUIgZbAKe/FGRbjO0fuW11 -+L7TcE1k3eaC7RkvotIaCOW/RMOkwKu1MbCzFEA2YRYf9covEwdItzI4FE++ZJrsz -+LhUCAwEAaTANBgkqhkiG9w0BAQsFAAOCAQEAeqqJT0LnmAluAjrsCSK/eYYjwjhZ -+aKMi/iBO10zfb+GvT4yqEL5gnuWxJEx4TTcDww1clvOC1EcPUZFaKR3GIBGy0ZgJ -+zGCfg+sC6liyZ+4PSWSJHD2dT5N3IGp4/hPsrhKnVb9fYbRc0Bc5VHeS9QQoSJDH -+f9EbxCcwdErVllRter29OZCb4XnEEbTqLIKRYVrbsu/t4C+vzi0tmKg5HZXf9PMo -+D28zJGsCAr8sKW/iUKObqDOHEn56lk12NTJmJmi+g6rEikk/0czJlRjSGnJQLjUg -+d4wslruibXBsLPtJw2c6vTC2SV2F1PXwy5j1OKU+D6nxaaItQvWADEjcTg== -+-----END CERTIFICATE-----""" -+ -+ # Malformed certificate that requires pyasn1 re-encoding (fails with python-cryptography) -+ self.malformed_cert_b64 = ( -+ "MIIDUjCCAvegAwIBAgILAI5xYHQ14nH5hdYwCgYIKoZIzj0EAwIwVTFTMB8GA1UEAxMYTnV2b3Rv" -+ "biBUUE0gUm9vdCBDQSAyMTExMCUGA1UEChMeTnV2b3RvbiBUZWNobm9sb2d5IENvcnBvcmF0aW9u" -+ "MAkGA1UEBhMCVFcwHhcNMTkwNzIzMTcxNTEzWhcNMzkwNzE5MTcxNTEzWjAAMIIBIjANBgkqhkiG" -+ "9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk8kCj7srY/Zlvm1795fVXdyX44w5qsd1m5VywMDgSOavzPKO" -+ "kgbHgQNx6Ak5+4Q43EJ/5qsaDBv59F8W7K69maUwcMNq1xpuq0V/LiwgJVAtc3CdvlxtwQrn7+Uq" -+ "ieIGf+i8sGxpeUCSmYHJPTHNHqjQnvUtdGoy/+WO0i7WsAvX3k/gHHr4p58a8urjJ1RG2Lk1g48D" -+ "ESwl+D7atQEPWzgjr6vK/s5KpLrn7M+dh97TUbG1510AOWBPP35MtT8IZbqC4hs2Ol16gT1M3a9e" -+ "+GaMZkItLUwV76vKDNEgTZG8M1C9OItA/xwzlfXbPepzpxWb4kzHS4qZoQtl4vBZrQIDAQABo4IB" -+ "NjCCATIwUAYDVR0RAQH/BEYwRKRCMEAxPjAUBgVngQUCARMLaWQ6NEU1NDQzMDAwEAYFZ4EFAgIT" -+ "B05QQ1Q3NXgwFAYFZ4EFAgMTC2lkOjAwMDcwMDAyMAwGA1UdEwEB/wQCMAAwEAYDVR0lBAkwBwYF" -+ "Z4EFCAEwHwYDVR0jBBgwFoAUI/TiKtO+N0pEl3KVSqKDrtdSVy4wDgYDVR0PAQH/BAQDAgUgMCIG" -+ "A1UdCQQbMBkwFwYFZ4EFAhAxDjAMDAMyLjACAQACAgCKMGkGCCsGAQUFBwEBBF0wWzBZBggrBgEF" -+ "BQcwAoZNaHR0cHM6Ly93d3cubnV2b3Rvbi5jb20vc2VjdXJpdHkvTlRDLVRQTS1FSy1DZXJ0L051" -+ "dm90b24gVFBNIFJvb3QgQ0EgMjExMS5jZXIwCgYIKoZIzj0EAwIDSQAwRgIhAPHOFiBDZd0dfml2" -+ "a/KlPFhmX7Ahpd0Wq11ZUW1/ixviAiEAlex8BB5nsR6w8QrANwCxc7fH/YnbjXfMCFiWzeZH7ps=" -+ ) -+ -+ # Load certificates for testing -+ self.compliant_cert = cryptography.x509.load_pem_x509_certificate(self.compliant_cert_pem.encode()) -+ self.malformed_cert_der = base64.b64decode(self.malformed_cert_b64) -+ -+ def test_infer_encoding_wrapped_certificate(self): -+ """Test that CertificateWrapper objects are identified as 'wrapped'.""" -+ wrapped_cert = wrap_certificate(self.compliant_cert, None) -+ encoding = self.cert_type.infer_encoding(wrapped_cert) -+ self.assertEqual(encoding, "wrapped") -+ -+ def test_infer_encoding_raw_certificate(self): -+ """Test that raw cryptography.x509.Certificate objects are identified as 'decoded'.""" -+ encoding = self.cert_type.infer_encoding(self.compliant_cert) -+ self.assertEqual(encoding, "decoded") -+ -+ def test_infer_encoding_der_bytes(self): -+ """Test that DER bytes are identified as 'der'.""" -+ der_bytes = self.compliant_cert.public_bytes(Encoding.DER) -+ encoding = self.cert_type.infer_encoding(der_bytes) -+ self.assertEqual(encoding, "der") -+ -+ def test_infer_encoding_pem_string(self): -+ """Test that PEM strings are identified as 'pem'.""" -+ encoding = self.cert_type.infer_encoding(self.compliant_cert_pem) -+ self.assertEqual(encoding, "pem") -+ -+ def test_infer_encoding_base64_string(self): -+ """Test that Base64 strings are identified as 'base64'.""" -+ encoding = self.cert_type.infer_encoding(self.malformed_cert_b64) -+ self.assertEqual(encoding, "base64") -+ -+ def test_infer_encoding_none_for_invalid(self): -+ """Test that invalid types return None.""" -+ encoding = self.cert_type.infer_encoding(12345) # type: ignore[arg-type] # Testing invalid type -+ self.assertIsNone(encoding) -+ -+ def test_asn1_compliant_wrapped_without_original_bytes(self): -+ """Test that CertificateWrapper without original bytes is ASN.1 compliant.""" -+ wrapped_cert = wrap_certificate(self.compliant_cert, None) -+ compliant = self.cert_type.asn1_compliant(wrapped_cert) -+ self.assertTrue(compliant) -+ -+ def test_asn1_compliant_wrapped_with_original_bytes(self): -+ """Test that CertificateWrapper with original bytes is not ASN.1 compliant.""" -+ wrapped_cert = wrap_certificate(self.compliant_cert, b"fake_original_bytes") -+ compliant = self.cert_type.asn1_compliant(wrapped_cert) -+ self.assertFalse(compliant) -+ -+ def test_asn1_compliant_raw_certificate(self): -+ """Test that raw cryptography.x509.Certificate returns None (already decoded).""" -+ compliant = self.cert_type.asn1_compliant(self.compliant_cert) -+ self.assertIsNone(compliant) -+ -+ def test_asn1_compliant_pem_strings(self): -+ """Test ASN.1 compliance checking on PEM strings.""" -+ # The regular certificate and TPM certificate from test_registrar_db.py are actually ASN.1 compliant -+ # and can be loaded directly by python-cryptography without requiring pyasn1 re-encoding -+ compliant_regular = self.cert_type.asn1_compliant(self.compliant_cert_pem) -+ # Only test one certificate since both are the same type (ASN.1 compliant) -+ -+ # Should be ASN.1 compliant (True) since it loads fine with python-cryptography -+ self.assertTrue(compliant_regular) -+ -+ def test_asn1_compliant_der_and_base64(self): -+ """Test ASN.1 compliance checking on DER and Base64 formats.""" -+ # Test DER bytes - regular certificate should be compliant -+ der_bytes = self.compliant_cert.public_bytes(Encoding.DER) -+ compliant_der = self.cert_type.asn1_compliant(der_bytes) -+ self.assertTrue(compliant_der) -+ -+ # Test Base64 string - regular certificate should be compliant -+ b64_string = base64.b64encode(der_bytes).decode("utf-8") -+ compliant_b64 = self.cert_type.asn1_compliant(b64_string) -+ self.assertTrue(compliant_b64) -+ -+ def test_asn1_compliant_malformed_certificate(self): -+ """Test ASN.1 compliance checking on a truly malformed certificate.""" -+ # Test the malformed certificate that requires pyasn1 re-encoding -+ compliant = self.cert_type.asn1_compliant(self.malformed_cert_b64) -+ self.assertFalse(compliant) # Should be non-compliant since it needs pyasn1 fallback -+ -+ def test_asn1_compliant_invalid_data(self): -+ """Test that invalid certificate data is not ASN.1 compliant.""" -+ compliant = self.cert_type.asn1_compliant("invalid_certificate_data") -+ self.assertFalse(compliant) -+ -+ def test_cast_wrapped_certificate(self): -+ """Test that CertificateWrapper objects are returned unchanged.""" -+ wrapped_cert = wrap_certificate(self.compliant_cert, None) -+ result = self.cert_type.cast(wrapped_cert) -+ self.assertIs(result, wrapped_cert) -+ -+ def test_cast_raw_certificate_to_wrapped(self): -+ """Test that raw certificates are wrapped without original bytes.""" -+ result = self.cert_type.cast(self.compliant_cert) -+ self.assertIsInstance(result, CertificateWrapper) -+ assert result is not None # For type checker -+ self.assertFalse(result.has_original_bytes) -+ -+ def test_cast_pem_strings(self): -+ """Test casting PEM strings to CertificateWrapper.""" -+ # Test regular certificate - should be ASN.1 compliant, no original bytes needed -+ result_regular = self.cert_type.cast(self.compliant_cert_pem) -+ self.assertIsInstance(result_regular, CertificateWrapper) -+ assert result_regular is not None # For type checker -+ self.assertFalse(result_regular.has_original_bytes) -+ -+ # Note: Only testing compliant certificate since we now use one consistent certificate for all compliant scenarios -+ -+ def test_cast_malformed_certificate(self): -+ """Test casting the malformed certificate that requires pyasn1 re-encoding.""" -+ result = self.cert_type.cast(self.malformed_cert_b64) -+ self.assertIsInstance(result, CertificateWrapper) -+ assert result is not None # For type checker -+ # Malformed certificate should have original bytes since it needs re-encoding -+ self.assertTrue(result.has_original_bytes) -+ -+ def test_cast_der_bytes(self): -+ """Test casting DER bytes to CertificateWrapper.""" -+ der_bytes = self.compliant_cert.public_bytes(Encoding.DER) -+ result = self.cert_type.cast(der_bytes) -+ self.assertIsInstance(result, CertificateWrapper) -+ -+ def test_cast_none_value(self): -+ """Test that None values return None.""" -+ result = self.cert_type.cast(None) -+ self.assertIsNone(result) -+ -+ def test_cast_empty_string(self): -+ """Test that empty strings return None.""" -+ result = self.cert_type.cast("") -+ self.assertIsNone(result) -+ -+ -+if __name__ == "__main__": -+ unittest.main() -diff --git a/test/test_certificate_wrapper.py b/test/test_certificate_wrapper.py -new file mode 100644 -index 000000000..6b47260d9 ---- /dev/null -+++ b/test/test_certificate_wrapper.py -@@ -0,0 +1,385 @@ -+""" -+Unit tests for the CertificateWrapper class. -+ -+This module tests the certificate wrapper functionality that preserves original bytes -+for malformed certificates requiring pyasn1 re-encoding. -+""" -+ -+import base64 -+import subprocess -+import tempfile -+import unittest -+from unittest.mock import Mock -+ -+import cryptography.x509 -+from cryptography.hazmat.primitives.serialization import Encoding -+from pyasn1.codec.der import decoder as pyasn1_decoder -+from pyasn1.codec.der import encoder as pyasn1_encoder -+from pyasn1_modules import rfc2459 as pyasn1_rfc2459 -+ -+from keylime.certificate_wrapper import CertificateWrapper, wrap_certificate -+ -+ -+class TestCertificateWrapper(unittest.TestCase): -+ """Test cases for CertificateWrapper class.""" -+ -+ def setUp(self): -+ """Set up test fixtures.""" -+ # Malformed certificate (Base64 encoded) that requires pyasn1 re-encoding -+ # This is a real TPM certificate that doesn't strictly follow ASN.1 DER rules -+ self.malformed_cert_b64 = ( -+ "MIIDUjCCAvegAwIBAgILAI5xYHQ14nH5hdYwCgYIKoZIzj0EAwIwVTFTMB8GA1UEAxMYTnV2b3Rv" -+ "biBUUE0gUm9vdCBDQSAyMTExMCUGA1UEChMeTnV2b3RvbiBUZWNobm9sb2d5IENvcnBvcmF0aW9u" -+ "MAkGA1UEBhMCVFcwHhcNMTkwNzIzMTcxNTEzWhcNMzkwNzE5MTcxNTEzWjAAMIIBIjANBgkqhkiG" -+ "9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk8kCj7srY/Zlvm1795fVXdyX44w5qsd1m5VywMDgSOavzPKO" -+ "kgbHgQNx6Ak5+4Q43EJ/5qsaDBv59F8W7K69maUwcMNq1xpuq0V/LiwgJVAtc3CdvlxtwQrn7+Uq" -+ "ieIGf+i8sGxpeUCSmYHJPTHNHqjQnvUtdGoy/+WO0i7WsAvX3k/gHHr4p58a8urjJ1RG2Lk1g48D" -+ "ESwl+D7atQEPWzgjr6vK/s5KpLrn7M+dh97TUbG1510AOWBPP35MtT8IZbqC4hs2Ol16gT1M3a9e" -+ "+GaMZkItLUwV76vKDNEgTZG8M1C9OItA/xwzlfXbPepzpxWb4kzHS4qZoQtl4vBZrQIDAQABo4IB" -+ "NjCCATIwUAYDVR0RAQH/BEYwRKRCMEAxPjAUBgVngQUCARMLaWQ6NEU1NDQzMDAwEAYFZ4EFAgIT" -+ "B05QQ1Q3NXgwFAYFZ4EFAgMTC2lkOjAwMDcwMDAyMAwGA1UdEwEB/wQCMAAwEAYDVR0lBAkwBwYF" -+ "Z4EFCAEwHwYDVR0jBBgwFoAUI/TiKtO+N0pEl3KVSqKDrtdSVy4wDgYDVR0PAQH/BAQDAgUgMCIG" -+ "A1UdCQQbMBkwFwYFZ4EFAhAxDjAMDAMyLjACAQACAgCKMGkGCCsGAQUFBwEBBF0wWzBZBggrBgEF" -+ "BQcwAoZNaHR0cHM6Ly93d3cubnV2b3Rvbi5jb20vc2VjdXJpdHkvTlRDLVRQTS1FSy1DZXJ0L051" -+ "dm90b24gVFBNIFJvb3QgQ0EgMjExMS5jZXIwCgYIKoZIzj0EAwIDSQAwRgIhAPHOFiBDZd0dfml2" -+ "a/KlPFhmX7Ahpd0Wq11ZUW1/ixviAiEAlex8BB5nsR6w8QrANwCxc7fH/YnbjXfMCFiWzeZH7ps=" -+ ) -+ self.malformed_cert_der = base64.b64decode(self.malformed_cert_b64) -+ -+ # Create a mock certificate for testing -+ self.mock_cert = Mock(spec=cryptography.x509.Certificate) -+ self.mock_cert.subject = Mock() -+ self.mock_cert.subject.__str__ = Mock(return_value="CN=Test Certificate") -+ self.mock_cert.public_bytes.return_value = b"mock_der_data" -+ -+ def test_init_without_original_bytes(self): -+ """Test wrapper initialization without original bytes.""" -+ wrapper = CertificateWrapper(self.mock_cert) -+ -+ # Test through public interface -+ self.assertFalse(wrapper.has_original_bytes) -+ self.assertIsNone(wrapper.original_bytes) -+ # Test delegation works -+ self.assertEqual(wrapper.subject, self.mock_cert.subject) -+ -+ def test_init_with_original_bytes(self): -+ """Test wrapper initialization with original bytes.""" -+ original_data = b"original_certificate_data" -+ wrapper = CertificateWrapper(self.mock_cert, original_data) -+ -+ # Test through public interface -+ self.assertTrue(wrapper.has_original_bytes) -+ self.assertEqual(wrapper.original_bytes, original_data) -+ # Test delegation works -+ self.assertEqual(wrapper.subject, self.mock_cert.subject) -+ -+ def test_getattr_delegation(self): -+ """Test that attributes are properly delegated to the wrapped certificate.""" -+ wrapper = CertificateWrapper(self.mock_cert) -+ -+ # Access an attribute that should be delegated -+ result = wrapper.subject -+ self.assertEqual(result, self.mock_cert.subject) -+ -+ def test_public_bytes_der_without_original(self): -+ """Test public_bytes DER encoding without original bytes.""" -+ wrapper = CertificateWrapper(self.mock_cert) -+ -+ result = wrapper.public_bytes(Encoding.DER) -+ -+ self.mock_cert.public_bytes.assert_called_once_with(Encoding.DER) -+ self.assertEqual(result, b"mock_der_data") -+ -+ def test_public_bytes_der_with_original(self): -+ """Test public_bytes DER encoding with original bytes.""" -+ original_data = b"original_certificate_data" -+ wrapper = CertificateWrapper(self.mock_cert, original_data) -+ -+ result = wrapper.public_bytes(Encoding.DER) -+ -+ # Should return original bytes, not call the wrapped certificate -+ self.mock_cert.public_bytes.assert_not_called() -+ self.assertEqual(result, original_data) -+ -+ def test_public_bytes_pem_without_original(self): -+ """Test public_bytes PEM encoding without original bytes.""" -+ self.mock_cert.public_bytes.return_value = b"-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n" -+ wrapper = CertificateWrapper(self.mock_cert) -+ -+ result = wrapper.public_bytes(Encoding.PEM) -+ -+ self.mock_cert.public_bytes.assert_called_once_with(Encoding.PEM) -+ self.assertEqual(result, b"-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n") -+ -+ def test_public_bytes_pem_with_original(self): -+ """Test public_bytes PEM encoding with original bytes.""" -+ original_data = self.malformed_cert_der -+ wrapper = CertificateWrapper(self.mock_cert, original_data) -+ -+ result = wrapper.public_bytes(Encoding.PEM) -+ -+ # Should not call the wrapped certificate's method -+ self.mock_cert.public_bytes.assert_not_called() -+ -+ # Result should be PEM format derived from original bytes -+ self.assertIsInstance(result, bytes) -+ result_str = result.decode("utf-8") -+ self.assertTrue(result_str.startswith("-----BEGIN CERTIFICATE-----")) -+ self.assertTrue(result_str.endswith("-----END CERTIFICATE-----\n")) -+ -+ # Verify that the PEM content can be converted back to the original DER -+ pem_lines = result_str.strip().split("\n") -+ pem_content = "".join(pem_lines[1:-1]) # Remove headers and join -+ recovered_der = base64.b64decode(pem_content) -+ self.assertEqual(recovered_der, original_data) -+ -+ def test_pem_line_length_compliance(self): -+ """Test that PEM output follows RFC 1421 line length requirements (64 chars).""" -+ original_data = self.malformed_cert_der -+ wrapper = CertificateWrapper(self.mock_cert, original_data) -+ -+ result = wrapper.public_bytes(Encoding.PEM) -+ result_str = result.decode("utf-8") -+ -+ lines = result_str.strip().split("\n") -+ # Check that content lines (excluding headers) are max 64 chars -+ for line in lines[1:-1]: # Skip header and footer -+ self.assertLessEqual(len(line), 64) -+ -+ def test_str_representation(self): -+ """Test string representation of the wrapper.""" -+ wrapper = CertificateWrapper(self.mock_cert) -+ -+ result = str(wrapper) -+ -+ expected = f"CertificateWrapper(subject={self.mock_cert.subject})" -+ self.assertEqual(result, expected) -+ -+ def test_repr_representation_without_original(self): -+ """Test repr representation without original bytes.""" -+ wrapper = CertificateWrapper(self.mock_cert) -+ -+ result = repr(wrapper) -+ -+ expected = f"CertificateWrapper(subject={self.mock_cert.subject}, has_original_bytes=False)" -+ self.assertEqual(result, expected) -+ -+ def test_repr_representation_with_original(self): -+ """Test repr representation with original bytes.""" -+ original_data = b"original_data" -+ wrapper = CertificateWrapper(self.mock_cert, original_data) -+ -+ result = repr(wrapper) -+ -+ expected = f"CertificateWrapper(subject={self.mock_cert.subject}, has_original_bytes=True)" -+ self.assertEqual(result, expected) -+ -+ def test_pickling_support(self): -+ """Test that the wrapper supports pickling operations.""" -+ original_data = b"test_data" -+ wrapper = CertificateWrapper(self.mock_cert, original_data) -+ -+ # Test getstate -+ state = wrapper.__getstate__() -+ self.assertIsInstance(state, dict) -+ self.assertIn("_cert", state) -+ self.assertIn("_original_bytes", state) -+ -+ # Test setstate -+ new_wrapper = CertificateWrapper(Mock(), None) -+ new_wrapper.__setstate__(state) -+ # Verify state was restored correctly through public interface -+ self.assertTrue(new_wrapper.has_original_bytes) -+ self.assertEqual(new_wrapper.original_bytes, original_data) -+ -+ def test_wrap_certificate_function_without_original(self): -+ """Test the wrap_certificate factory function without original bytes.""" -+ wrapper = wrap_certificate(self.mock_cert) -+ -+ self.assertIsInstance(wrapper, CertificateWrapper) -+ self.assertFalse(wrapper.has_original_bytes) -+ self.assertIsNone(wrapper.original_bytes) -+ -+ def test_wrap_certificate_function_with_original(self): -+ """Test the wrap_certificate factory function with original bytes.""" -+ original_data = b"original_certificate_data" -+ wrapper = wrap_certificate(self.mock_cert, original_data) -+ -+ self.assertIsInstance(wrapper, CertificateWrapper) -+ self.assertTrue(wrapper.has_original_bytes) -+ self.assertEqual(wrapper.original_bytes, original_data) -+ -+ def test_real_malformed_certificate_handling(self): -+ """Test with a real malformed certificate that requires pyasn1 re-encoding.""" -+ # This test simulates the scenario where a malformed certificate is processed -+ -+ # Mock the scenario where cryptography fails but pyasn1 succeeds -+ mock_reencoded_cert = Mock(spec=cryptography.x509.Certificate) -+ mock_reencoded_cert.subject = Mock() -+ mock_reencoded_cert.subject.__str__ = Mock(return_value="CN=Nuvoton TPM") -+ -+ # Create wrapper as if it came from the certificate loading process -+ wrapper = wrap_certificate(mock_reencoded_cert, self.malformed_cert_der) -+ -+ # Test that original bytes are preserved -+ self.assertTrue(wrapper.has_original_bytes) -+ self.assertEqual(wrapper.original_bytes, self.malformed_cert_der) -+ -+ # Test DER output uses original bytes -+ der_output = wrapper.public_bytes(Encoding.DER) -+ self.assertEqual(der_output, self.malformed_cert_der) -+ -+ # Test PEM output is derived from original bytes -+ pem_output = wrapper.public_bytes(Encoding.PEM) -+ self.assertIsInstance(pem_output, bytes) -+ -+ # Verify PEM can be converted back to original DER -+ pem_str = pem_output.decode("utf-8") -+ lines = pem_str.strip().split("\n") -+ content = "".join(lines[1:-1]) -+ recovered_der = base64.b64decode(content) -+ self.assertEqual(recovered_der, self.malformed_cert_der) -+ -+ def test_unsupported_encoding_fallback(self): -+ """Test that unsupported encoding types fall back to wrapped certificate.""" -+ # Create a custom encoding that's not DER or PEM -+ custom_encoding = Mock() -+ custom_encoding.name = "CUSTOM" -+ -+ original_data = b"original_data" -+ wrapper = CertificateWrapper(self.mock_cert, original_data) -+ -+ # Should fall back to wrapped certificate for unknown encoding -+ wrapper.public_bytes(custom_encoding) -+ self.mock_cert.public_bytes.assert_called_once_with(custom_encoding) -+ -+ def test_malformed_certificate_cryptography_failure_and_verification(self): -+ """ -+ Comprehensive test demonstrating that the malformed certificate: -+ 1. Fails to load with python-cryptography -+ 2. Can be verified with OpenSSL -+ 3. Is successfully handled by our wrapper after pyasn1 re-encoding -+ """ -+ # Test 1: Demonstrate that python-cryptography fails to load the malformed certificate -+ with self.assertRaises(Exception) as context: -+ cryptography.x509.load_der_x509_certificate(self.malformed_cert_der) -+ -+ # The specific exception type may vary, but it should fail -+ self.assertIsInstance(context.exception, Exception) -+ -+ # Test 2: Demonstrate that pyasn1 can handle the malformed certificate -+ try: -+ # Decode and re-encode using pyasn1 (simulating what the Certificate type does) -+ pyasn1_cert = pyasn1_decoder.decode(self.malformed_cert_der, asn1Spec=pyasn1_rfc2459.Certificate())[0] -+ reencoded_der = pyasn1_encoder.encode(pyasn1_cert) -+ -+ # Now cryptography should be able to load the re-encoded certificate -+ reencoded_cert = cryptography.x509.load_der_x509_certificate(reencoded_der) -+ self.assertIsNotNone(reencoded_cert) -+ -+ except Exception as e: -+ self.fail(f"pyasn1 should handle the malformed certificate, but got: {e}") -+ -+ # Test 3: Verify that our wrapper preserves the original bytes correctly -+ wrapper = wrap_certificate(reencoded_cert, self.malformed_cert_der) -+ -+ # The wrapper should preserve original bytes -+ self.assertTrue(wrapper.has_original_bytes) -+ self.assertEqual(wrapper.original_bytes, self.malformed_cert_der) -+ -+ # DER output should use original bytes -+ der_output = wrapper.public_bytes(Encoding.DER) -+ self.assertEqual(der_output, self.malformed_cert_der) -+ -+ # PEM output should be derived from original bytes -+ pem_output = wrapper.public_bytes(Encoding.PEM) -+ pem_str = pem_output.decode("utf-8") -+ -+ # Verify PEM format is correct -+ self.assertTrue(pem_str.startswith("-----BEGIN CERTIFICATE-----")) -+ self.assertTrue(pem_str.endswith("-----END CERTIFICATE-----\n")) -+ -+ # Test 4: Demonstrate OpenSSL can verify the certificate structure -+ # (Even without the root CA, OpenSSL should be able to parse the certificate) -+ try: -+ with tempfile.NamedTemporaryFile(mode="wb", suffix=".der", delete=False) as temp_file: -+ temp_file.write(self.malformed_cert_der) -+ temp_file.flush() -+ -+ # Use OpenSSL to parse the certificate (should succeed) -+ result = subprocess.run( -+ ["openssl", "x509", "-in", temp_file.name, "-inform", "DER", "-text", "-noout"], -+ capture_output=True, -+ text=True, -+ check=False, -+ ) -+ -+ # OpenSSL should successfully parse the certificate -+ self.assertEqual(result.returncode, 0) -+ self.assertIn("Nuvoton TPM Root CA 2111", result.stdout) -+ self.assertIn("Certificate:", result.stdout) -+ -+ except (subprocess.CalledProcessError, FileNotFoundError) as e: -+ # Skip if OpenSSL is not available, but don't fail the test -+ self.skipTest(f"OpenSSL not available for verification test: {e}") -+ -+ # Test 5: Verify certificate details are accessible through wrapper -+ # The subject should be empty (as shown in the OpenSSL output) -+ self.assertEqual(len(reencoded_cert.subject), 0) -+ -+ # The issuer should contain Nuvoton information -+ issuer_attrs = {} -+ for attr in reencoded_cert.issuer: -+ # Use dotted string representation to avoid accessing private _name -+ oid_name = attr.oid.dotted_string -+ if oid_name == "2.5.4.3": # Common Name OID -+ issuer_attrs["commonName"] = attr.value -+ self.assertIn("commonName", issuer_attrs) -+ self.assertEqual(issuer_attrs["commonName"], "Nuvoton TPM Root CA 2111") -+ -+ # Test 6: Demonstrate that even re-encoded certificates may have parsing issues -+ # This shows why preserving original bytes is crucial -+ try: -+ # Try to access extensions - this may fail due to malformed ASN.1 -+ extensions = list(reencoded_cert.extensions) -+ # If it succeeds, verify it has the expected Subject Alternative Name -+ # Subject Alternative Name OID is 2.5.29.17 -+ has_subject_alt_name = any(ext.oid.dotted_string == "2.5.29.17" for ext in extensions) -+ self.assertTrue(has_subject_alt_name, "EK certificate should have Subject Alternative Name extension") -+ except (ValueError, Exception) as e: -+ # This is actually expected for malformed certificates! -+ # Even after pyasn1 re-encoding, some parsing issues may remain -+ self.assertIn("parsing asn1", str(e).lower(), f"Expected ASN.1 parsing error, got: {e}") -+ # This demonstrates why our wrapper preserves original bytes - -+ # they maintain signature validity even when parsing has issues -+ -+ def test_certificate_chain_verification_simulation(self): -+ """ -+ Test that simulates certificate chain verification where original bytes matter. -+ This demonstrates why preserving original bytes is crucial for signature validation. -+ """ -+ # Create a wrapper with the malformed certificate -+ mock_reencoded_cert = Mock(spec=cryptography.x509.Certificate) -+ mock_reencoded_cert.subject = Mock() -+ mock_reencoded_cert.public_key.return_value = Mock() -+ -+ wrapper = wrap_certificate(mock_reencoded_cert, self.malformed_cert_der) -+ -+ # Simulate signature verification scenario -+ # In real verification, the signature is computed over the exact DER bytes -+ original_bytes_for_verification = wrapper.public_bytes(Encoding.DER) -+ -+ # Should get the original malformed bytes (preserving signature validity) -+ self.assertEqual(original_bytes_for_verification, self.malformed_cert_der) -+ -+ # If we didn't preserve original bytes, we'd get re-encoded bytes which would -+ # invalidate the signature even though the certificate content is the same -+ mock_reencoded_cert.public_bytes.return_value = b"reencoded_different_bytes" -+ -+ # Verify that using the wrapper gets original bytes, not re-encoded bytes -+ self.assertNotEqual(original_bytes_for_verification, b"reencoded_different_bytes") -+ self.assertEqual(original_bytes_for_verification, self.malformed_cert_der) -+ -+ -+if __name__ == "__main__": -+ unittest.main() -diff --git a/test/test_registrar_agent_cert_compliance.py b/test/test_registrar_agent_cert_compliance.py -new file mode 100644 -index 000000000..ede9b9f26 ---- /dev/null -+++ b/test/test_registrar_agent_cert_compliance.py -@@ -0,0 +1,289 @@ -+""" -+Integration tests for RegistrarAgent certificate compliance functionality. -+ -+This module tests the simplified certificate compliance checking methods -+to ensure they work correctly with the new CertificateWrapper-based approach. -+""" -+ -+import types -+import unittest -+from unittest.mock import Mock, patch -+ -+import cryptography.x509 -+ -+from keylime.certificate_wrapper import wrap_certificate -+from keylime.models.base.types.certificate import Certificate -+from keylime.models.registrar.registrar_agent import RegistrarAgent -+ -+ -+class TestRegistrarAgentCertCompliance(unittest.TestCase): -+ """Test cases for RegistrarAgent certificate compliance methods.""" -+ -+ # pylint: disable=protected-access,not-callable # Testing protected methods and dynamic method binding -+ -+ def setUp(self): -+ """Set up test fixtures.""" -+ # Create a test certificate -+ self.valid_cert_pem = """-----BEGIN CERTIFICATE----- -+MIIEnzCCA4egAwIBAgIEMV64bDANBgkqhkiG9w0BAQUFADBtMQswCQYDVQQGEwJE -+RTEQMA4GA1UECBMHQmF2YXJpYTEhMB8GA1UEChMYSW5maW5lb24gVGVjaG5vbG9n -+aWVzIEFHMQwwCgYDVQQLEwNBSU0xGzAZBgNVBAMTEklGWCBUUE0gRUsgUm9vdCBD -+QTAeFw0wNTEwMjAxMzQ3NDNaFw0yNTEwMjAxMzQ3NDNaMHcxCzAJBgNVBAYTAkRF -+MQ8wDQYDVQQIEwZTYXhvbnkxITAfBgNVBAoTGEluZmluZW9uIFRlY2hub2xvZ2ll -+cyBBRzEMMAoGA1UECxMDQUlNMSYwJAYDVQQDEx1JRlggVFBNIEVLIEludGVybWVk -+aWF0ZSBDQSAwMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALftPhYN -+t4rE+JnU/XOPICbOBLvfo6iA7nuq7zf4DzsAWBdsZEdFJQfaK331ihG3IpQnlQ2i -+YtDim289265f0J4OkPFpKeFU27CsfozVaNUm6UR/uzwA8ncxFc3iZLRMRNLru/Al -+VG053ULVDQMVx2iwwbBSAYO9pGiGbk1iMmuZaSErMdb9v0KRUyZM7yABiyDlM3cz -+UQX5vLWV0uWqxdGoHwNva5u3ynP9UxPTZWHZOHE6+14rMzpobs6Ww2RR8BgF96rh -+4rRAZEl8BXhwiQq4STvUXkfvdpWH4lzsGcDDtrB6Nt3KvVNvsKz+b07Dk+Xzt+EH -+NTf3Byk2HlvX+scCAwEAAaOCATswggE3MB0GA1UdDgQWBBQ4k8292HPEIzMV4bE7 -+qWoNI8wQxzAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgwBgEB/wIBADBYBgNV -+HSABAf8ETjBMMEoGC2CGSAGG+EUBBy8BMDswOQYIKwYBBQUHAgEWLWh0dHA6Ly93 -+d3cudmVyaXNpZ24uY29tL3JlcG9zaXRvcnkvaW5kZXguaHRtbDCBlwYDVR0jBIGP -+MIGMgBRW65FEhWPWcrOu1EWWC/eUDlRCpqFxpG8wbTELMAkGA1UEBhMCREUxEDAO -+BgNVBAgTB0JhdmFyaWExITAfBgNVBAoTGEluZmluZW9uIFRlY2hub2xvZ2llcyBB -+RzEMMAoGA1UECxMDQUlNMRswGQYDVQQDExJJRlggVFBNIEVLIFJvb3QgQ0GCAQMw -+DQYJKoZIhvcNAQEFBQADggEBABJ1+Ap3rNlxZ0FW0aIgdzktbNHlvXWNxFdYIBbM -+OKjmbOos0Y4O60eKPu259XmMItCUmtbzF3oKYXq6ybARUT2Lm+JsseMF5VgikSlU -+BJALqpKVjwAds81OtmnIQe2LSu4xcTSavpsL4f52cUAu/maMhtSgN9mq5roYptq9 -+DnSSDZrX4uYiMPl//rBaNDBflhJ727j8xo9CCohF3yQUoQm7coUgbRMzyO64yMIO -+3fhb+Vuc7sNwrMOz3VJN14C3JMoGgXy0c57IP/kD5zGRvljKEvrRC2I147+fPeLS -+DueRMS6lblvRKiZgmGAg7YaKOkOaEmVDMQ+fTo2Po7hI5wc= -+-----END CERTIFICATE-----""" -+ -+ self.valid_cert = cryptography.x509.load_pem_x509_certificate(self.valid_cert_pem.encode()) -+ -+ # Malformed certificate that actually requires pyasn1 re-encoding -+ self.malformed_cert_b64 = ( -+ "MIIDUjCCAvegAwIBAgILAI5xYHQ14nH5hdYwCgYIKoZIzj0EAwIwVTFTMB8GA1UEAxMYTnV2b3Rv" -+ "biBUUE0gUm9vdCBDQSAyMTExMCUGA1UEChMeTnV2b3RvbiBUZWNobm9sb2d5IENvcnBvcmF0aW9u" -+ "MAkGA1UEBhMCVFcwHhcNMTkwNzIzMTcxNTEzWhcNMzkwNzE5MTcxNTEzWjAAMIIBIjANBgkqhkiG" -+ "9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk8kCj7srY/Zlvm1795fVXdyX44w5qsd1m5VywMDgSOavzPKO" -+ "kgbHgQNx6Ak5+4Q43EJ/5qsaDBv59F8W7K69maUwcMNq1xpuq0V/LiwgJVAtc3CdvlxtwQrn7+Uq" -+ "ieIGf+i8sGxpeUCSmYHJPTHNHqjQnvUtdGoy/+WO0i7WsAvX3k/gHHr4p58a8urjJ1RG2Lk1g48D" -+ "ESwl+D7atQEPWzgjr6vK/s5KpLrn7M+dh97TUbG1510AOWBPP35MtT8IZbqC4hs2Ol16gT1M3a9e" -+ "+GaMZkItLUwV76vKDNEgTZG8M1C9OItA/xwzlfXbPepzpxWb4kzHS4qZoQtl4vBZrQIDAQABo4IB" -+ "NjCCATIwUAYDVR0RAQH/BEYwRKRCMEAxPjAUBgVngQUCARMLaWQ6NEU1NDQzMDAwEAYFZ4EFAgIT" -+ "B05QQ1Q3NXgwFAYFZ4EFAgMTC2lkOjAwMDcwMDAyMAwGA1UdEwEB/wQCMAAwEAYDVR0lBAkwBwYF" -+ "Z4EFCAEwHwYDVR0jBBgwFoAUI/TiKtO+N0pEl3KVSqKDrtdSVy4wDgYDVR0PAQH/BAQDAgUgMCIG" -+ "A1UdCQQbMBkwFwYFZ4EFAhAxDjAMDAMyLjACAQACAgCKMGkGCCsGAQUFBwEBBF0wWzBZBggrBgEF" -+ "BQcwAoZNaHR0cHM6Ly93d3cubnV2b3Rvbi5jb20vc2VjdXJpdHkvTlRDLVRQTS1FSy1DZXJ0L051" -+ "dm90b24gVFBNIFJvb3QgQ0EgMjExMS5jZXIwCgYIKoZIzj0EAwIDSQAwRgIhAPHOFiBDZd0dfml2" -+ "a/KlPFhmX7Ahpd0Wq11ZUW1/ixviAiEAlex8BB5nsR6w8QrANwCxc7fH/YnbjXfMCFiWzeZH7ps=" -+ ) -+ -+ # Create wrapped certificates for testing using Certificate type to ensure proper behavior -+ cert_type = Certificate() -+ -+ # Create compliant certificate (no original bytes needed) -+ self.compliant_wrapped_cert = wrap_certificate(self.valid_cert, None) -+ -+ # Create non-compliant certificate using the malformed cert data -+ self.non_compliant_wrapped_cert = cert_type.cast(self.malformed_cert_b64) -+ -+ def create_mock_registrar_agent(self): -+ """Create a mock RegistrarAgent with necessary attributes.""" -+ agent = Mock() -+ agent.changes = {} -+ agent.values = {} -+ agent._add_error = Mock() -+ -+ # Bind the actual methods to the mock instance -+ agent._check_cert_compliance = types.MethodType(RegistrarAgent._check_cert_compliance, agent) -+ agent._check_all_cert_compliance = types.MethodType(RegistrarAgent._check_all_cert_compliance, agent) -+ -+ return agent -+ -+ def test_check_cert_compliance_no_new_cert(self): -+ """Test _check_cert_compliance when no new certificate is provided.""" -+ agent = self.create_mock_registrar_agent() -+ agent.changes = {} # No new certificate -+ -+ result = agent._check_cert_compliance("ekcert") -+ self.assertTrue(result) -+ agent._add_error.assert_not_called() -+ -+ def test_check_cert_compliance_same_cert(self): -+ """Test _check_cert_compliance when new cert is same as old cert.""" -+ agent = self.create_mock_registrar_agent() -+ agent.changes = {"ekcert": self.compliant_wrapped_cert} -+ agent.values = {"ekcert": self.compliant_wrapped_cert} -+ -+ result = agent._check_cert_compliance("ekcert") -+ self.assertTrue(result) -+ agent._add_error.assert_not_called() -+ -+ def test_check_cert_compliance_different_cert_same_der(self): -+ """Test _check_cert_compliance when certificates have same DER bytes.""" -+ agent = self.create_mock_registrar_agent() -+ # Create two different wrapper objects but with same underlying certificate -+ cert1 = wrap_certificate(self.valid_cert, None) -+ cert2 = wrap_certificate(self.valid_cert, None) -+ -+ agent.changes = {"ekcert": cert1} -+ agent.values = {"ekcert": cert2} -+ -+ result = agent._check_cert_compliance("ekcert") -+ self.assertTrue(result) -+ agent._add_error.assert_not_called() -+ -+ @patch("keylime.config.get") -+ def test_check_cert_compliance_compliant_cert(self, mock_config): -+ """Test _check_cert_compliance with ASN.1 compliant certificate.""" -+ mock_config.return_value = "warn" # Default action -+ -+ agent = self.create_mock_registrar_agent() -+ agent.changes = {"ekcert": self.compliant_wrapped_cert} -+ agent.values = {} # No old certificate -+ -+ result = agent._check_cert_compliance("ekcert") -+ self.assertTrue(result) -+ agent._add_error.assert_not_called() -+ -+ @patch("keylime.config.get") -+ def test_check_cert_compliance_non_compliant_cert_warn(self, mock_config): -+ """Test _check_cert_compliance with non-compliant certificate (warn mode).""" -+ mock_config.return_value = "warn" # Warn action -+ -+ agent = self.create_mock_registrar_agent() -+ agent.changes = {"ekcert": self.non_compliant_wrapped_cert} -+ agent.values = {} # No old certificate -+ -+ result = agent._check_cert_compliance("ekcert") -+ self.assertFalse(result) -+ agent._add_error.assert_not_called() # Should not add error in warn mode -+ -+ @patch("keylime.config.get") -+ def test_check_cert_compliance_non_compliant_cert_reject(self, mock_config): -+ """Test _check_cert_compliance with non-compliant certificate (reject mode).""" -+ mock_config.return_value = "reject" # Reject action -+ -+ agent = self.create_mock_registrar_agent() -+ agent.changes = {"ekcert": self.non_compliant_wrapped_cert} -+ agent.values = {} # No old certificate -+ -+ result = agent._check_cert_compliance("ekcert") -+ self.assertFalse(result) -+ agent._add_error.assert_called_once() # Should add error in reject mode -+ -+ @patch("keylime.config.get") -+ def test_check_all_cert_compliance_no_non_compliant(self, mock_config): -+ """Test _check_all_cert_compliance when all certificates are compliant.""" -+ mock_config.return_value = "warn" -+ -+ agent = self.create_mock_registrar_agent() -+ agent.changes = { -+ "ekcert": self.compliant_wrapped_cert, -+ "iak_cert": self.compliant_wrapped_cert, -+ } -+ agent.values = {} -+ -+ # Should not raise any exceptions or log warnings -+ with patch("keylime.models.registrar.registrar_agent.logger") as mock_logger: -+ agent._check_all_cert_compliance() -+ mock_logger.warning.assert_not_called() -+ mock_logger.error.assert_not_called() -+ -+ @patch("keylime.config.get") -+ def test_check_all_cert_compliance_with_non_compliant_warn(self, mock_config): -+ """Test _check_all_cert_compliance with non-compliant certificates (warn mode).""" -+ mock_config.return_value = "warn" -+ -+ agent = self.create_mock_registrar_agent() -+ agent.changes = { -+ "ekcert": self.non_compliant_wrapped_cert, -+ "iak_cert": self.compliant_wrapped_cert, -+ "idevid_cert": self.non_compliant_wrapped_cert, -+ } -+ agent.values = {} -+ -+ with patch("keylime.models.registrar.registrar_agent.logger") as mock_logger: -+ agent._check_all_cert_compliance() -+ # Should log warning for non-compliant certificates -+ mock_logger.warning.assert_called_once() -+ format_string = mock_logger.warning.call_args[0][0] -+ cert_names = mock_logger.warning.call_args[0][1] -+ self.assertIn("Certificate(s) %s may not conform", format_string) -+ self.assertEqual("'ekcert' and 'idevid_cert'", cert_names) -+ -+ @patch("keylime.config.get") -+ def test_check_all_cert_compliance_with_non_compliant_reject(self, mock_config): -+ """Test _check_all_cert_compliance with non-compliant certificates (reject mode).""" -+ mock_config.return_value = "reject" -+ -+ agent = self.create_mock_registrar_agent() -+ agent.changes = { -+ "ekcert": self.non_compliant_wrapped_cert, -+ "mtls_cert": self.non_compliant_wrapped_cert, -+ } -+ agent.values = {} -+ -+ with patch("keylime.models.registrar.registrar_agent.logger") as mock_logger: -+ agent._check_all_cert_compliance() -+ # Should log error for non-compliant certificates -+ mock_logger.error.assert_called_once() -+ format_string = mock_logger.error.call_args[0][0] -+ cert_names = mock_logger.error.call_args[0][1] -+ self.assertIn("Certificate(s) %s may not conform", format_string) -+ self.assertIn("were rejected due to config", format_string) -+ self.assertEqual("'ekcert' and 'mtls_cert'", cert_names) -+ -+ @patch("keylime.config.get") -+ def test_check_all_cert_compliance_ignore_mode(self, mock_config): -+ """Test _check_all_cert_compliance with ignore mode.""" -+ mock_config.return_value = "ignore" -+ -+ agent = self.create_mock_registrar_agent() -+ agent.changes = { -+ "ekcert": self.non_compliant_wrapped_cert, -+ "iak_cert": self.non_compliant_wrapped_cert, -+ } -+ agent.values = {} -+ -+ with patch("keylime.models.registrar.registrar_agent.logger") as mock_logger: -+ agent._check_all_cert_compliance() -+ # Should not log anything in ignore mode -+ mock_logger.warning.assert_not_called() -+ mock_logger.error.assert_not_called() -+ -+ def test_check_all_cert_compliance_single_non_compliant(self): -+ """Test _check_all_cert_compliance message formatting for single certificate.""" -+ agent = self.create_mock_registrar_agent() -+ agent.changes = {"ekcert": self.non_compliant_wrapped_cert} -+ agent.values = {} -+ -+ with patch("keylime.config.get", return_value="warn"): -+ with patch("keylime.models.registrar.registrar_agent.logger") as mock_logger: -+ agent._check_all_cert_compliance() -+ # Should format message correctly for single certificate -+ format_string = mock_logger.warning.call_args[0][0] -+ cert_names = mock_logger.warning.call_args[0][1] -+ self.assertIn("Certificate(s) %s may not conform", format_string) -+ self.assertEqual("'ekcert'", cert_names) -+ self.assertNotIn(" and", cert_names) # Should not have "and" for single cert -+ -+ def test_field_names_coverage(self): -+ """Test that all expected certificate field names are checked.""" -+ agent = self.create_mock_registrar_agent() -+ agent.changes = { -+ "ekcert": self.non_compliant_wrapped_cert, -+ "iak_cert": self.non_compliant_wrapped_cert, -+ "idevid_cert": self.non_compliant_wrapped_cert, -+ "mtls_cert": self.non_compliant_wrapped_cert, -+ } -+ agent.values = {} -+ -+ with patch("keylime.config.get", return_value="warn"): -+ with patch("keylime.models.registrar.registrar_agent.logger") as mock_logger: -+ agent._check_all_cert_compliance() -+ # Should check all four certificate fields -+ format_string = mock_logger.warning.call_args[0][0] -+ cert_names = mock_logger.warning.call_args[0][1] -+ self.assertIn("Certificate(s) %s may not conform", format_string) -+ expected_names = "'ekcert', 'iak_cert', 'idevid_cert' and 'mtls_cert'" -+ self.assertEqual(expected_names, cert_names) -+ -+ -+if __name__ == "__main__": -+ unittest.main() diff --git a/0012-keylime-policy-avoid-opening-dev-stdout.patch b/0012-keylime-policy-avoid-opening-dev-stdout.patch deleted file mode 100644 index eea629d..0000000 --- a/0012-keylime-policy-avoid-opening-dev-stdout.patch +++ /dev/null @@ -1,37 +0,0 @@ -From e9a6615ea3ab60b9248377071ea2f5cc7b45dfda Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Thu, 28 Aug 2025 14:33:59 +0100 -Subject: [PATCH] policy/sign: use print() when writing to /dev/stdout - -Signed-off-by: Sergio Correia ---- - keylime/policy/sign_runtime_policy.py | 9 +++++++-- - 1 file changed, 7 insertions(+), 2 deletions(-) - -diff --git a/keylime/policy/sign_runtime_policy.py b/keylime/policy/sign_runtime_policy.py -index 87529065d..316ee15aa 100644 ---- a/keylime/policy/sign_runtime_policy.py -+++ b/keylime/policy/sign_runtime_policy.py -@@ -2,6 +2,7 @@ - - import argparse - import json -+import sys - from json.decoder import JSONDecodeError - from typing import TYPE_CHECKING, Any, Optional - -@@ -191,8 +192,12 @@ def sign_runtime_policy(args: argparse.Namespace) -> Optional[str]: - return None - - try: -- with open(args.output_file, "wb") as f: -- f.write(signed_policy.encode("UTF-8")) -+ if args.output_file == "/dev/stdout": -+ # Let's simply print to stdout the regular way. -+ print(signed_policy, file=sys.stdout) -+ else: -+ with open(args.output_file, "wb") as f: -+ f.write(signed_policy.encode("UTF-8")) - except Exception as exc: - logger.error("Unable to write signed policy to destination file '%s': %s", args.output_file, exc) - return None diff --git a/0013-algorithms-add-support-for-specific-ECC-curve-algori.patch b/0013-algorithms-add-support-for-specific-ECC-curve-algori.patch deleted file mode 100644 index 81a6398..0000000 --- a/0013-algorithms-add-support-for-specific-ECC-curve-algori.patch +++ /dev/null @@ -1,382 +0,0 @@ -From 7a723f0938edf9ccc597507a4230922e9235cf18 Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Wed, 24 Sep 2025 07:20:53 +0100 -Subject: [PATCH 13/18] algorithms: add support for specific ECC curve - algorithms - -Extended the Encrypt enum to support specific ECC curves including: -- ecc192 (P-192) -- ecc224 (P-224) -- ecc256 (P-256) -- ecc384 (P-384) -- ecc521 (P-521) - -This enables Keylime to accept and validate different ECC curves -for TPM attestation operations. - -Also, when agent reports specific algorithm like 'ecc256' but tenant -configuration uses generic 'ecc', the is_accepted function now uses -bidirectional normalization to properly match algorithms. - -Signed-off-by: Sergio Correia ---- - keylime/common/algorithms.py | 28 +++- - test/test_algorithms.py | 275 ++++++++++++++++++++++++++++++++++- - 2 files changed, 301 insertions(+), 2 deletions(-) - -diff --git a/keylime/common/algorithms.py b/keylime/common/algorithms.py -index db12c26..bb22fb6 100644 ---- a/keylime/common/algorithms.py -+++ b/keylime/common/algorithms.py -@@ -9,7 +9,18 @@ def is_accepted(algorithm: str, accepted: List[Any]) -> bool: - @param algorithm: algorithm to be checked - @param accepted: a list of acceptable algorithms - """ -- return algorithm in accepted -+ # Check direct match first. -+ if algorithm in accepted: -+ return True -+ -+ # Check if any accepted algorithm normalizes to the same value as our algorithm -+ # This handles backwards compatibility cases like "ecc" accepting "ecc256". -+ normalized_algorithm = Encrypt.normalize(algorithm) -+ for accepted_alg in accepted: -+ if Encrypt.normalize(str(accepted_alg)) == normalized_algorithm: -+ return True -+ -+ return False - - - class Hash(str, enum.Enum): -@@ -74,11 +85,26 @@ class Hash(str, enum.Enum): - class Encrypt(str, enum.Enum): - RSA = "rsa" - ECC = "ecc" -+ ECC192 = "ecc192" -+ ECC224 = "ecc224" -+ ECC256 = "ecc256" -+ ECC384 = "ecc384" -+ ECC521 = "ecc521" - - @staticmethod - def is_recognized(algorithm: str) -> bool: -+ # Handle aliases to match agent behavior -+ if algorithm == "ecc": -+ algorithm = "ecc256" # Default ECC alias maps to P-256, same as the agent. - return algorithm in list(Encrypt) - -+ @staticmethod -+ def normalize(algorithm: str) -> str: -+ """Normalize algorithm string to handle aliases, matching the agent behavior""" -+ if algorithm == "ecc": -+ return "ecc256" # Default ECC alias maps to P-256. -+ return algorithm -+ - - class Sign(str, enum.Enum): - RSASSA = "rsassa" -diff --git a/test/test_algorithms.py b/test/test_algorithms.py -index b5a29c7..8a31fa9 100644 ---- a/test/test_algorithms.py -+++ b/test/test_algorithms.py -@@ -2,7 +2,7 @@ import os - import tempfile - import unittest - --from keylime.common.algorithms import Encrypt, Hash, Sign -+from keylime.common.algorithms import Encrypt, Hash, Sign, is_accepted - - - class TestHash(unittest.TestCase): -@@ -117,11 +117,88 @@ class TestEncrypt(unittest.TestCase): - "enc": "ecc", - "valid": True, - }, -+ { -+ "enc": "ecc192", -+ "valid": True, -+ }, -+ { -+ "enc": "ecc224", -+ "valid": True, -+ }, -+ { -+ "enc": "ecc256", -+ "valid": True, -+ }, -+ { -+ "enc": "ecc384", -+ "valid": True, -+ }, -+ { -+ "enc": "ecc521", -+ "valid": True, -+ }, - ] - - for c in test_cases: - self.assertEqual(Encrypt.is_recognized(c["enc"]), c["valid"], msg=f"enc = {c['enc']}") - -+ def test_enum_membership(self): -+ """Test that all ECC curve algorithms are members of the Encrypt enum""" -+ self.assertTrue(Encrypt.RSA in Encrypt) -+ self.assertTrue(Encrypt.ECC in Encrypt) -+ self.assertTrue(Encrypt.ECC192 in Encrypt) -+ self.assertTrue(Encrypt.ECC224 in Encrypt) -+ self.assertTrue(Encrypt.ECC256 in Encrypt) -+ self.assertTrue(Encrypt.ECC384 in Encrypt) -+ self.assertTrue(Encrypt.ECC521 in Encrypt) -+ -+ def test_normalize(self): -+ """Test the normalize method for handling ECC aliases""" -+ test_cases = [ -+ { -+ "input": "ecc", -+ "expected": "ecc256", -+ }, -+ { -+ "input": "ecc192", -+ "expected": "ecc192", -+ }, -+ { -+ "input": "ecc224", -+ "expected": "ecc224", -+ }, -+ { -+ "input": "ecc256", -+ "expected": "ecc256", -+ }, -+ { -+ "input": "ecc384", -+ "expected": "ecc384", -+ }, -+ { -+ "input": "ecc521", -+ "expected": "ecc521", -+ }, -+ { -+ "input": "rsa", -+ "expected": "rsa", -+ }, -+ ] -+ -+ for c in test_cases: -+ self.assertEqual(Encrypt.normalize(c["input"]), c["expected"], msg=f"input = {c['input']}") -+ -+ def test_normalize_ecc_alias_behavior(self): -+ """Test that ECC alias normalization matches agent behavior""" -+ # Test that "ecc" is recognized through alias handling -+ self.assertTrue(Encrypt.is_recognized("ecc")) -+ -+ # Test that normalize converts "ecc" to "ecc256" (P-256) -+ self.assertEqual(Encrypt.normalize("ecc"), "ecc256") -+ -+ # Test that direct ecc256 works -+ self.assertTrue(Encrypt.is_recognized("ecc256")) -+ - - class TestSign(unittest.TestCase): - def test_is_recognized(self): -@@ -158,3 +235,199 @@ class TestSign(unittest.TestCase): - - for c in test_cases: - self.assertEqual(Sign.is_recognized(c["sign"]), c["valid"], msg=f"sign = {c['sign']}") -+ -+ -+class TestIsAccepted(unittest.TestCase): -+ def test_direct_algorithm_matching(self): -+ """Test that direct algorithm matches work correctly""" -+ test_cases = [ -+ { -+ "algorithm": "ecc256", -+ "accepted": ["ecc256"], -+ "expected": True, -+ }, -+ { -+ "algorithm": "rsa", -+ "accepted": ["rsa"], -+ "expected": True, -+ }, -+ { -+ "algorithm": "ecc384", -+ "accepted": ["ecc256", "ecc384"], -+ "expected": True, -+ }, -+ { -+ "algorithm": "ecc521", -+ "accepted": ["ecc256"], -+ "expected": False, -+ }, -+ { -+ "algorithm": "unknown", -+ "accepted": ["rsa", "ecc256"], -+ "expected": False, -+ }, -+ ] -+ -+ for c in test_cases: -+ result = is_accepted(c["algorithm"], c["accepted"]) -+ self.assertEqual(result, c["expected"], msg=f"algorithm='{c['algorithm']}', accepted={c['accepted']}") -+ -+ def test_backwards_compatibility_ecc_normalization(self): -+ """Test backwards compatibility: 'ecc' in accepted list should accept specific ECC algorithms""" -+ test_cases = [ -+ { -+ "algorithm": "ecc256", -+ "accepted": ["ecc"], -+ "expected": True, -+ "desc": "ecc256 should be accepted when 'ecc' is in accepted list", -+ }, -+ { -+ "algorithm": "ecc384", -+ "accepted": ["ecc"], -+ "expected": False, -+ "desc": "ecc384 should NOT be accepted when only 'ecc' is in accepted list (ecc maps to ecc256)", -+ }, -+ { -+ "algorithm": "ecc521", -+ "accepted": ["ecc"], -+ "expected": False, -+ "desc": "ecc521 should NOT be accepted when only 'ecc' is in accepted list", -+ }, -+ { -+ "algorithm": "ecc192", -+ "accepted": ["ecc"], -+ "expected": False, -+ "desc": "ecc192 should NOT be accepted when only 'ecc' is in accepted list", -+ }, -+ ] -+ -+ for c in test_cases: -+ result = is_accepted(c["algorithm"], c["accepted"]) -+ self.assertEqual( -+ result, c["expected"], msg=f"{c['desc']} - algorithm='{c['algorithm']}', accepted={c['accepted']}" -+ ) -+ -+ def test_forward_compatibility_ecc_normalization(self): -+ """Test forward compatibility: specific ECC in accepted list should accept 'ecc' algorithm""" -+ test_cases = [ -+ { -+ "algorithm": "ecc", -+ "accepted": ["ecc256"], -+ "expected": True, -+ "desc": "ecc should be accepted when 'ecc256' is in accepted list (both normalize to ecc256)", -+ }, -+ { -+ "algorithm": "ecc", -+ "accepted": ["ecc384"], -+ "expected": False, -+ "desc": "ecc should NOT be accepted when only 'ecc384' is in accepted list", -+ }, -+ { -+ "algorithm": "ecc", -+ "accepted": ["ecc521"], -+ "expected": False, -+ "desc": "ecc should NOT be accepted when only 'ecc521' is in accepted list", -+ }, -+ ] -+ -+ for c in test_cases: -+ result = is_accepted(c["algorithm"], c["accepted"]) -+ self.assertEqual( -+ result, c["expected"], msg=f"{c['desc']} - algorithm='{c['algorithm']}', accepted={c['accepted']}" -+ ) -+ -+ def test_bidirectional_algorithm_matching(self): -+ """Test bidirectional matching scenarios that happen in real usage""" -+ test_cases = [ -+ { -+ "algorithm": "ecc256", -+ "accepted": ["rsa", "ecc"], -+ "expected": True, -+ "desc": "Agent reports ecc256, tenant config has generic 'ecc'", -+ }, -+ { -+ "algorithm": "ecc", -+ "accepted": ["rsa", "ecc256"], -+ "expected": True, -+ "desc": "Agent reports generic 'ecc', tenant config has specific 'ecc256'", -+ }, -+ { -+ "algorithm": "ecc384", -+ "accepted": ["rsa", "ecc"], -+ "expected": False, -+ "desc": "Agent reports ecc384, tenant has generic 'ecc' (should not match)", -+ }, -+ { -+ "algorithm": "ecc", -+ "accepted": ["rsa", "ecc384"], -+ "expected": False, -+ "desc": "Agent reports generic 'ecc', tenant has ecc384 (should not match)", -+ }, -+ ] -+ -+ for c in test_cases: -+ result = is_accepted(c["algorithm"], c["accepted"]) -+ self.assertEqual( -+ result, c["expected"], msg=f"{c['desc']} - algorithm='{c['algorithm']}', accepted={c['accepted']}" -+ ) -+ -+ def test_mixed_algorithm_types(self): -+ """Test mixing different algorithm types in accepted list""" -+ test_cases = [ -+ { -+ "algorithm": "rsa", -+ "accepted": ["ecc", "rsa"], -+ "expected": True, -+ }, -+ { -+ "algorithm": "ecc256", -+ "accepted": ["rsa", "ecc"], -+ "expected": True, -+ }, -+ { -+ "algorithm": "ecc384", -+ "accepted": ["rsa", "ecc256", "ecc384"], -+ "expected": True, -+ }, -+ { -+ "algorithm": "unknown", -+ "accepted": ["rsa", "ecc", "ecc384"], -+ "expected": False, -+ }, -+ ] -+ -+ for c in test_cases: -+ result = is_accepted(c["algorithm"], c["accepted"]) -+ self.assertEqual(result, c["expected"], msg=f"algorithm='{c['algorithm']}', accepted={c['accepted']}") -+ -+ def test_edge_cases(self): -+ """Test edge cases and boundary conditions""" -+ test_cases = [ -+ {"algorithm": "", "accepted": ["ecc"], "expected": False, "desc": "Empty algorithm string"}, -+ {"algorithm": "ecc256", "accepted": [], "expected": False, "desc": "Empty accepted list"}, -+ {"algorithm": "ecc256", "accepted": [""], "expected": False, "desc": "Accepted list with empty string"}, -+ { -+ "algorithm": "ECC256", -+ "accepted": ["ecc256"], -+ "expected": False, -+ "desc": "Case sensitivity - uppercase should not match", -+ }, -+ { -+ "algorithm": "ecc256", -+ "accepted": ["ecc"], -+ "expected": True, -+ "desc": "ecc256 algorithm should match ecc in accepted list", -+ }, -+ { -+ "algorithm": "ecc", -+ "accepted": ["ecc256"], -+ "expected": True, -+ "desc": "ecc algorithm should match ecc256 in accepted list", -+ }, -+ ] -+ -+ for c in test_cases: -+ result = is_accepted(c["algorithm"], c["accepted"]) -+ self.assertEqual( -+ result, c["expected"], msg=f"{c['desc']} - algorithm='{c['algorithm']}', accepted={c['accepted']}" -+ ) --- -2.47.3 - diff --git a/0014-algorithms-add-support-for-specific-RSA-algorithms.patch b/0014-algorithms-add-support-for-specific-RSA-algorithms.patch deleted file mode 100644 index b081517..0000000 --- a/0014-algorithms-add-support-for-specific-RSA-algorithms.patch +++ /dev/null @@ -1,87 +0,0 @@ -From eecd2f73642f784b19cb1bb9c78c6d0b1e486dda Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Fri, 26 Sep 2025 00:03:49 +0100 -Subject: [PATCH 14/18] algorithms: add support for specific RSA algorithms - -Similar to the previous change for ECC, now we extend the Encrypt enum -to support the following specific RSA algorithms: -- RSA1024 -- RSA2048 -- RSA3072 -- RSA4096 - -Map also 'rsa' to 'rsa2048' for backwards compatibility. - -Signed-off-by: Sergio Correia ---- - keylime/common/algorithms.py | 8 ++++++++ - test/test_algorithms.py | 13 ++++++++++++- - 2 files changed, 20 insertions(+), 1 deletion(-) - -diff --git a/keylime/common/algorithms.py b/keylime/common/algorithms.py -index bb22fb6..32a1ec1 100644 ---- a/keylime/common/algorithms.py -+++ b/keylime/common/algorithms.py -@@ -84,6 +84,10 @@ class Hash(str, enum.Enum): - - class Encrypt(str, enum.Enum): - RSA = "rsa" -+ RSA1024 = "rsa1024" -+ RSA2048 = "rsa2048" -+ RSA3072 = "rsa3072" -+ RSA4096 = "rsa4096" - ECC = "ecc" - ECC192 = "ecc192" - ECC224 = "ecc224" -@@ -96,6 +100,8 @@ class Encrypt(str, enum.Enum): - # Handle aliases to match agent behavior - if algorithm == "ecc": - algorithm = "ecc256" # Default ECC alias maps to P-256, same as the agent. -+ if algorithm == "rsa": -+ algorithm = "rsa2048" # Default RSA alias maps to RSA-2048, same as the agent. - return algorithm in list(Encrypt) - - @staticmethod -@@ -103,6 +109,8 @@ class Encrypt(str, enum.Enum): - """Normalize algorithm string to handle aliases, matching the agent behavior""" - if algorithm == "ecc": - return "ecc256" # Default ECC alias maps to P-256. -+ if algorithm == "rsa": -+ return "rsa2048" # Default RSA alias maps to RSA-2048. - return algorithm - - -diff --git a/test/test_algorithms.py b/test/test_algorithms.py -index 8a31fa9..5542c0f 100644 ---- a/test/test_algorithms.py -+++ b/test/test_algorithms.py -@@ -181,7 +181,7 @@ class TestEncrypt(unittest.TestCase): - }, - { - "input": "rsa", -- "expected": "rsa", -+ "expected": "rsa2048", - }, - ] - -@@ -199,6 +199,17 @@ class TestEncrypt(unittest.TestCase): - # Test that direct ecc256 works - self.assertTrue(Encrypt.is_recognized("ecc256")) - -+ def test_normalize_rsa_alias_behavior(self): -+ """Test that RSA alias normalization matches agent behavior""" -+ # Test that "rsa" is recognized through alias handling -+ self.assertTrue(Encrypt.is_recognized("rsa")) -+ -+ # Test that normalize converts "rsa" to "rsa2048" -+ self.assertEqual(Encrypt.normalize("rsa"), "rsa2048") -+ -+ # Test that direct rsa2048 works -+ self.assertTrue(Encrypt.is_recognized("rsa2048")) -+ - - class TestSign(unittest.TestCase): - def test_is_recognized(self): --- -2.47.3 - diff --git a/0015-tpm_util-fix-quote-signature-extraction-for-ECDSA.patch b/0015-tpm_util-fix-quote-signature-extraction-for-ECDSA.patch deleted file mode 100644 index 9a5480a..0000000 --- a/0015-tpm_util-fix-quote-signature-extraction-for-ECDSA.patch +++ /dev/null @@ -1,43 +0,0 @@ -From 690a2059be01993f5e7f65a01d994e53b82211e4 Mon Sep 17 00:00:00 2001 -From: Thore Sommer -Date: Mon, 3 Mar 2025 15:44:37 +0100 -Subject: [PATCH 15/18] tpm_util: fix quote signature extraction for ECDSA - -Signed-off-by: Thore Sommer ---- - keylime/tpm/tpm_util.py | 12 +++++++++--- - 1 file changed, 9 insertions(+), 3 deletions(-) - -diff --git a/keylime/tpm/tpm_util.py b/keylime/tpm/tpm_util.py -index cdecd32..25c40e0 100644 ---- a/keylime/tpm/tpm_util.py -+++ b/keylime/tpm/tpm_util.py -@@ -223,9 +223,7 @@ def checkquote( - pcrblob: The state of the PCRs that were quoted; Intel tpm2-tools specific format - exp_hash_alg: The hash that was expected to have been used for quoting - """ -- sig_alg, hash_alg, sig_size = struct.unpack_from(">HHH", sigblob, 0) -- -- (signature,) = struct.unpack_from(f"{sig_size}s", sigblob, 6) -+ sig_alg, hash_alg = struct.unpack_from(">HH", sigblob, 0) - - pubkey = serialization.load_pem_public_key(aikblob, backend=backends.default_backend()) - if not isinstance(pubkey, (RSAPublicKey, EllipticCurvePublicKey)): -@@ -236,6 +234,14 @@ def checkquote( - if isinstance(pubkey, EllipticCurvePublicKey) and sig_alg not in [tpm2_objects.TPM_ALG_ECDSA]: - raise ValueError(f"Unsupported quote signature algorithm '{sig_alg:#x}' for EC keys") - -+ if sig_alg in [tpm2_objects.TPM_ALG_RSASSA]: -+ (sig_size,) = struct.unpack_from(">H", sigblob, 4) -+ (signature,) = struct.unpack_from(f"{sig_size}s", sigblob, 6) -+ elif sig_alg in [tpm2_objects.TPM_ALG_ECDSA]: -+ signature = ecdsa_der_from_tpm(sigblob) -+ else: -+ raise ValueError(f"Unsupported quote signature algorithm '{sig_alg:#x}'") -+ - hashfunc = tpm2_objects.HASH_FUNCS.get(hash_alg) - if not hashfunc: - raise ValueError(f"Unsupported hash with id {hash_alg:#x} in signature blob") --- -2.47.3 - diff --git a/0016-tpm-fix-ECC-P-521-coordinate-validation.patch b/0016-tpm-fix-ECC-P-521-coordinate-validation.patch deleted file mode 100644 index 231a38e..0000000 --- a/0016-tpm-fix-ECC-P-521-coordinate-validation.patch +++ /dev/null @@ -1,515 +0,0 @@ -From 7c3d81879dba00dcfe917c73b10ca47e8ca7028a Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Sun, 21 Sep 2025 17:49:56 +0100 -Subject: [PATCH 16/18] tpm: fix ECC P-521 coordinate validation -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -P-521 coordinates can vary from 65-66 bytes due to TPM implementations -padding 521-bit values to byte boundaries or stripping leading zeros. -The previous validation was too strict, rejecting valid coordinates. - -Enhanced validation: -- Accepts P-521 coordinate range 65-66 bytes (520-528 bits) -- Validates against actual NIST prime moduli per SEC1 §2.3.5 and - FIPS 186-4 App D (coordinates must be < field prime p) -- Strict rejection of unknown curves for security - -The enhanced approach prevents false validation of coordinates that are -the correct byte length but exceed the curve's field prime. - -Assisted-by: Claude 4 Sonnet -Signed-off-by: Sergio Correia ---- - keylime/tpm/tpm2_objects.py | 42 +++- - test/test_tpm2_objects.py | 417 ++++++++++++++++++++++++++++++++++++ - 2 files changed, 455 insertions(+), 4 deletions(-) - create mode 100644 test/test_tpm2_objects.py - -diff --git a/keylime/tpm/tpm2_objects.py b/keylime/tpm/tpm2_objects.py -index fcc5bb5..9170628 100644 ---- a/keylime/tpm/tpm2_objects.py -+++ b/keylime/tpm/tpm2_objects.py -@@ -31,6 +31,16 @@ TPM_ECC_NIST_P256 = 0x0003 - TPM_ECC_NIST_P384 = 0x0004 - TPM_ECC_NIST_P521 = 0x0005 - -+# ECC curve prime moduli lookup table (coordinates must be < p) -+# This structure supports NIST curves and can be extended for other curves. -+ECC_CURVE_PRIMES = { -+ TPM_ECC_NIST_P192: 2**192 - 2**64 - 1, # P-192 prime -+ TPM_ECC_NIST_P224: 2**224 - 2**96 + 1, # P-224 prime -+ TPM_ECC_NIST_P256: 2**256 - 2**224 + 2**192 + 2**96 - 1, # P-256 prime -+ TPM_ECC_NIST_P384: 2**384 - 2**128 - 2**96 + 2**32 - 1, # P-384 prime -+ TPM_ECC_NIST_P521: 2**521 - 1, # P-521 prime -+} -+ - TPM_ALG_RSA = 0x0001 - TPM_ALG_ECC = 0x0023 - -@@ -318,10 +328,34 @@ def pubkey_parms_from_tpm2b_public( - if len(rest) != 0: - raise ValueError("Misparsed: more contents after X and Y") - -- if (len(x) * 8) != curve.key_size: -- raise ValueError(f"Misparsed either X or curve: {len(x)}*8 != {curve.key_size}") -- if (len(y) * 8) != curve.key_size: -- raise ValueError(f"Misparsed either Y or curve curve: {len(y)}*8 != {curve.key_size}") -+ # ECC coordinates can vary in byte length due to: -+ # 1. Padding to byte boundaries (most common) -+ # 2. Leading zero stripping in some encodings -+ # Validate both byte length and actual coordinate value. -+ max_bytes = (curve.key_size + 7) // 8 -+ min_bytes = max_bytes - 1 if curve.key_size % 8 != 0 else max_bytes -+ -+ # Get the actual prime modulus for the curve -+ prime_p = ECC_CURVE_PRIMES.get(curve_id) -+ if prime_p is None: -+ raise ValueError(f"Unsupported curve ID {curve_id:#x}: prime modulus not known") -+ -+ for label, coord in (("X", x), ("Y", y)): -+ coord_len = len(coord) -+ if coord_len < min_bytes or coord_len > max_bytes: -+ raise ValueError( -+ f"Misparsed {label} coordinate: got {coord_len} bytes, " -+ f"expected {min_bytes}-{max_bytes} for {curve.key_size}-bit curve" -+ ) -+ -+ coord_int = int.from_bytes(coord, "big") -+ # Coordinates must be reduced modulo the field prime p -+ # (SEC1 §2.3.5, FIPS 186-4 App D). Reject values >= p. -+ if coord_int >= prime_p: -+ raise ValueError( -+ f"{label} coordinate too large: {coord_int.bit_length()} bits, " -+ f"must be < {prime_p.bit_length()}-bit prime modulus" -+ ) - - bx = int.from_bytes(x, byteorder="big") - by = int.from_bytes(y, byteorder="big") -diff --git a/test/test_tpm2_objects.py b/test/test_tpm2_objects.py -new file mode 100644 -index 0000000..c880770 ---- /dev/null -+++ b/test/test_tpm2_objects.py -@@ -0,0 +1,417 @@ -+import struct -+import unittest -+ -+from cryptography.hazmat.primitives.asymmetric import ec -+ -+from keylime.tpm.tpm2_objects import ( -+ ECC_CURVE_PRIMES, -+ TPM_ECC_NIST_P192, -+ TPM_ECC_NIST_P224, -+ TPM_ECC_NIST_P256, -+ TPM_ECC_NIST_P384, -+ TPM_ECC_NIST_P521, -+ _curve_from_curve_id, -+ _pack_in_tpm2b, -+ pubkey_parms_from_tpm2b_public, -+) -+ -+ -+class TestTpm2Objects(unittest.TestCase): -+ def test_p521_coordinate_validation_logic(self): -+ """Test the specific coordinate validation logic for P-521""" -+ curve = _curve_from_curve_id(TPM_ECC_NIST_P521) -+ -+ # Test the updated validation logic -+ max_bytes = (curve.key_size + 7) // 8 # Should be 66 bytes for P-521 -+ min_bytes = max_bytes - 1 if curve.key_size % 8 != 0 else max_bytes # Should be 65 bytes for P-521 -+ -+ self.assertEqual(max_bytes, 66) -+ self.assertEqual(min_bytes, 65) # P-521 is not byte-aligned, so allows 65-66 bytes -+ -+ # Test coordinate sizes that should be accepted (65-66 bytes for P-521) -+ valid_sizes = [65, 66] -+ -+ for size in valid_sizes: -+ # Check that the validation logic would accept this size -+ should_pass = min_bytes <= size <= max_bytes -+ self.assertTrue(should_pass, f"Size {size} bytes should be valid for P-521") -+ -+ # Test coordinate sizes that should be rejected -+ invalid_sizes = [64, 67, 32, 68] -+ -+ for size in invalid_sizes: -+ # This should fail: not in the valid range -+ should_fail = size < min_bytes or size > max_bytes -+ self.assertTrue(should_fail, f"Size {size} bytes should be invalid for P-521") -+ -+ def test_p256_coordinate_validation_logic(self): -+ """Test the coordinate validation logic for P-256 to ensure no regression""" -+ curve = _curve_from_curve_id(TPM_ECC_NIST_P256) -+ -+ max_bytes = (curve.key_size + 7) // 8 # Should be 32 bytes for P-256 -+ min_bytes = ( -+ max_bytes - 1 if curve.key_size % 8 != 0 else max_bytes -+ ) # Should be 32 bytes for P-256 (byte-aligned) -+ -+ self.assertEqual(max_bytes, 32) -+ self.assertEqual(min_bytes, 32) # P-256 is byte-aligned, so only accepts 32 bytes -+ -+ # 32 bytes should be accepted -+ size = 32 -+ should_pass = min_bytes <= size <= max_bytes -+ self.assertTrue(should_pass, f"P-256 should accept {size} bytes") -+ -+ # Other sizes should be rejected -+ invalid_sizes = [31, 33, 64] -+ for size in invalid_sizes: -+ should_fail = size < min_bytes or size > max_bytes -+ self.assertTrue(should_fail, f"P-256 should reject {size} bytes") -+ -+ def test_p384_coordinate_validation_logic(self): -+ """Test the coordinate validation logic for P-384 to ensure no regression""" -+ curve = _curve_from_curve_id(TPM_ECC_NIST_P384) -+ -+ max_bytes = (curve.key_size + 7) // 8 # Should be 48 bytes for P-384 -+ min_bytes = ( -+ max_bytes - 1 if curve.key_size % 8 != 0 else max_bytes -+ ) # Should be 48 bytes for P-384 (byte-aligned) -+ -+ self.assertEqual(max_bytes, 48) -+ self.assertEqual(min_bytes, 48) # P-384 is byte-aligned, so only accepts 48 bytes -+ -+ # 48 bytes should be accepted -+ size = 48 -+ should_pass = min_bytes <= size <= max_bytes -+ self.assertTrue(should_pass, f"P-384 should accept {size} bytes") -+ -+ def test_coordinate_size_calculation(self): -+ """Test that coordinate size calculations are correct for different curves""" -+ # P-256: 256 bits -> (256 + 7) // 8 = 32 bytes -+ curve_p256 = _curve_from_curve_id(TPM_ECC_NIST_P256) -+ expected_p256 = (curve_p256.key_size + 7) // 8 -+ self.assertEqual(expected_p256, 32) -+ self.assertEqual(curve_p256.key_size, 256) -+ -+ # P-384: 384 bits -> (384 + 7) // 8 = 48 bytes -+ curve_p384 = _curve_from_curve_id(TPM_ECC_NIST_P384) -+ expected_p384 = (curve_p384.key_size + 7) // 8 -+ self.assertEqual(expected_p384, 48) -+ self.assertEqual(curve_p384.key_size, 384) -+ -+ # P-521: 521 bits -> (521 + 7) // 8 = 66 bytes -+ curve_p521 = _curve_from_curve_id(TPM_ECC_NIST_P521) -+ expected_p521 = (curve_p521.key_size + 7) // 8 -+ self.assertEqual(expected_p521, 66) -+ self.assertEqual(curve_p521.key_size, 521) -+ -+ def test_p521_specific_fix(self): -+ """Test the specific scenario that was fixed: P-521 with 66-byte coordinates""" -+ curve = _curve_from_curve_id(TPM_ECC_NIST_P521) -+ -+ # The key issue: P-521 has 521 bits -+ self.assertEqual(curve.key_size, 521) -+ -+ # TPMs pad to 66 bytes (528 bits) -+ tpm_padded_size = 66 -+ tpm_padded_bits = tpm_padded_size * 8 -+ self.assertEqual(tpm_padded_bits, 528) -+ -+ # The old validation would reject: (66 * 8) != 521 -+ old_validation_fails = tpm_padded_bits != curve.key_size -+ self.assertTrue(old_validation_fails, "Old validation would incorrectly reject 66-byte coordinates") -+ -+ # The new validation should accept: len(x) == expected_bytes OR (len(x) * 8) == curve.key_size -+ expected_bytes = (curve.key_size + 7) // 8 -+ new_validation_passes = (tpm_padded_size == expected_bytes) or (tpm_padded_bits == curve.key_size) -+ self.assertTrue(new_validation_passes, "New validation should accept 66-byte coordinates") -+ -+ def test_validation_before_and_after_fix(self): -+ """Test that demonstrates the fix by comparing old vs new validation logic""" -+ curve = _curve_from_curve_id(TPM_ECC_NIST_P521) -+ -+ # Test multiple coordinate sizes that P-521 can have -+ test_sizes = [65, 66] # 65 bytes (leading zero stripped), 66 bytes (padded) -+ -+ max_bytes = (curve.key_size + 7) // 8 # 66 bytes -+ min_bytes = max_bytes - 1 if curve.key_size % 8 != 0 else max_bytes # 65 bytes for P-521 -+ -+ for coordinate_size in test_sizes: -+ # Old validation logic (strict bit size match) - would require exactly 65.125 bytes -+ # which is impossible since we can't have fractional bytes -+ -+ # New validation logic (accept range for non-byte-aligned curves) -+ new_logic_passes = min_bytes <= coordinate_size <= max_bytes -+ self.assertTrue(new_logic_passes, f"New logic should accept {coordinate_size}-byte coordinates for P-521") -+ -+ # Verify the calculations -+ self.assertEqual(max_bytes, 66) -+ self.assertEqual(min_bytes, 65) -+ -+ def test_p521_coordinate_range_validation(self): -+ """Test that P-521 accepts coordinates in the range 65-66 bytes (520-528 bits)""" -+ curve = _curve_from_curve_id(TPM_ECC_NIST_P521) -+ -+ # P-521: 521 bits, padded to 66 bytes (528 bits), or 65 bytes with leading zero stripped -+ max_bytes = (curve.key_size + 7) // 8 # 66 bytes -+ min_bytes = max_bytes - 1 # 65 bytes (since 521 % 8 != 0) -+ -+ # Test all valid sizes -+ valid_sizes = [65, 66] -+ for size in valid_sizes: -+ is_valid = min_bytes <= size <= max_bytes -+ self.assertTrue(is_valid, f"P-521 should accept {size} bytes ({size * 8} bits)") -+ -+ # Test invalid sizes -+ invalid_sizes = [64, 67, 68, 32] -+ for size in invalid_sizes: -+ is_invalid = size < min_bytes or size > max_bytes -+ self.assertTrue(is_invalid, f"P-521 should reject {size} bytes ({size * 8} bits)") -+ -+ def test_coordinate_value_validation(self): -+ """Test that coordinate values are validated against actual prime moduli""" -+ # Test P-521 with actual prime -+ # curve_p521 = _curve_from_curve_id(TPM_ECC_NIST_P521) # Not needed for this test -+ p521_prime = ECC_CURVE_PRIMES[TPM_ECC_NIST_P521] -+ -+ # Test valid coordinate value (within range) -+ valid_coord_int = p521_prime - 1 # Largest valid value -+ valid_coord_bytes = valid_coord_int.to_bytes(66, "big") # 66 bytes, padded -+ -+ # Test the validation logic -+ coord_int = int.from_bytes(valid_coord_bytes, "big") -+ is_valid_value = coord_int < p521_prime -+ self.assertTrue(is_valid_value, "Coordinate value should be valid for P-521") -+ -+ # Test invalid coordinate value (>= prime) -+ invalid_coord_int = p521_prime # Equal to prime (invalid) -+ invalid_coord_bytes = invalid_coord_int.to_bytes(66, "big") # 66 bytes, but value too large -+ -+ coord_int = int.from_bytes(invalid_coord_bytes, "big") -+ is_invalid_value = coord_int >= p521_prime -+ self.assertTrue(is_invalid_value, "Coordinate value >= prime should be invalid for P-521") -+ -+ def test_prime_constants_accuracy(self): -+ """Test that our hardcoded prime constants are correct""" -+ # Verify the NIST prime values -+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P192], 2**192 - 2**64 - 1) -+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P224], 2**224 - 2**96 + 1) -+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P256], 2**256 - 2**224 + 2**192 + 2**96 - 1) -+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P384], 2**384 - 2**128 - 2**96 + 2**32 - 1) -+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P521], 2**521 - 1) -+ -+ # Verify they are actually less than 2^m for all curves except P-521 -+ self.assertLess(ECC_CURVE_PRIMES[TPM_ECC_NIST_P192], 2**192) -+ self.assertLess(ECC_CURVE_PRIMES[TPM_ECC_NIST_P224], 2**224) -+ self.assertLess(ECC_CURVE_PRIMES[TPM_ECC_NIST_P256], 2**256) -+ self.assertLess(ECC_CURVE_PRIMES[TPM_ECC_NIST_P384], 2**384) -+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P521], 2**521 - 1) # P-521 is special case -+ -+ def test_prime_lookup_table(self): -+ """Test that the prime lookup table works correctly""" -+ # Test known curves -+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P192], 2**192 - 2**64 - 1) -+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P224], 2**224 - 2**96 + 1) -+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P256], 2**256 - 2**224 + 2**192 + 2**96 - 1) -+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P384], 2**384 - 2**128 - 2**96 + 2**32 - 1) -+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P521], 2**521 - 1) -+ -+ # Test rejection of unknown curve -+ unknown_curve_id = 0x9999 -+ unknown_prime = ECC_CURVE_PRIMES.get(unknown_curve_id) -+ self.assertIsNone(unknown_prime, "Unknown curves should not be in ECC_CURVE_PRIMES") -+ -+ def test_error_message_formatting(self): -+ """Test that error messages use bit_length() instead of full integers""" -+ # Create a large coordinate value -+ large_value = ECC_CURVE_PRIMES[TPM_ECC_NIST_P521] # This would be hundreds of digits -+ -+ # Verify bit_length() is much more reasonable than the full number -+ bit_length = large_value.bit_length() -+ self.assertEqual(bit_length, 521) # Much more readable than 150+ digit number -+ -+ # The error message should use bit lengths, not full integers -+ expected_msg_pattern = f"coordinate too large: {bit_length} bits" -+ self.assertIn("521 bits", expected_msg_pattern) -+ -+ def test_unknown_curve_rejection(self): -+ """Test that unknown curves are strictly rejected""" -+ # This tests the design decision to be strict rather than use fallbacks -+ unknown_curve_id = 0x9999 -+ -+ # The strict approach: unknown curves should not have fallback behavior -+ # This ensures we only validate curves we explicitly understand -+ result = ECC_CURVE_PRIMES.get(unknown_curve_id) -+ self.assertIsNone(result, "Unknown curves should be explicitly rejected, not given fallback primes") -+ -+ -+class TestEccPublicKeySecurityValidation(unittest.TestCase): -+ """Test that ECC public key validation includes all required security checks: -+ 1. Point is on the curve -+ 2. Point is not zero or infinity -+ 3. Point is not in a small subgroup (not applicable to NIST curves with cofactor=1) -+ """ -+ -+ def create_ecc_tpm2b_public(self, x: int, y: int, curve_id: int = TPM_ECC_NIST_P256) -> bytes: -+ """Helper to create a TPM2B_PUBLIC structure for ECC key with given coordinates""" -+ # Get coordinate size based on curve -+ curve = _curve_from_curve_id(curve_id) -+ coord_bytes = (curve.key_size + 7) // 8 -+ -+ # Convert coordinates to bytes -+ x_bytes = x.to_bytes(coord_bytes, "big") -+ y_bytes = y.to_bytes(coord_bytes, "big") -+ -+ # Build TPMT_PUBLIC structure -+ # alg_type (TPM_ALG_ECC = 0x0023) -+ tpmt = struct.pack(">H", 0x0023) -+ # name_alg (TPM_ALG_SHA256 = 0x000B) -+ tpmt += struct.pack(">H", 0x000B) -+ # object_attributes (4 bytes) -+ tpmt += struct.pack(">I", 0x00040072) -+ # auth_policy (empty TPM2B) -+ tpmt += struct.pack(">H", 0) -+ # symmetric (TPM_ALG_NULL) -+ tpmt += struct.pack(">H", 0x0010) -+ # scheme (TPM_ALG_NULL) -+ tpmt += struct.pack(">H", 0x0010) -+ # curve_id -+ tpmt += struct.pack(">H", curve_id) -+ # kdf_scheme (TPM_ALG_NULL) -+ tpmt += struct.pack(">H", 0x0010) -+ # x coordinate (TPM2B) -+ tpmt += _pack_in_tpm2b(x_bytes) -+ # y coordinate (TPM2B) -+ tpmt += _pack_in_tpm2b(y_bytes) -+ -+ # Wrap in TPM2B_PUBLIC -+ return _pack_in_tpm2b(tpmt) -+ -+ def test_point_on_curve_validation(self): -+ """Test that points not on the curve are rejected (Security Check #1)""" -+ # For P-256, the curve equation is: y² = x³ - 3x + b (mod p) -+ # Choose coordinates that don't satisfy this equation -+ x = 1 -+ y = 1 # (1, 1) is not on the P-256 curve -+ -+ tpm2b_public = self.create_ecc_tpm2b_public(x, y, TPM_ECC_NIST_P256) -+ -+ # The cryptography library should reject this point as not being on the curve -+ with self.assertRaises(ValueError) as cm: -+ pubkey_parms_from_tpm2b_public(tpm2b_public) -+ self.assertIn("not on the curve", str(cm.exception).lower()) -+ -+ def test_point_at_infinity_validation(self): -+ """Test that the point at infinity (0, 0) is rejected (Security Check #2)""" -+ # The point at infinity should be rejected -+ x = 0 -+ y = 0 -+ -+ tpm2b_public = self.create_ecc_tpm2b_public(x, y, TPM_ECC_NIST_P256) -+ -+ # The cryptography library should reject the point at infinity -+ with self.assertRaises(ValueError) as cm: -+ pubkey_parms_from_tpm2b_public(tpm2b_public) -+ self.assertIn("not on the curve", str(cm.exception).lower()) -+ -+ def test_valid_point_accepted(self): -+ """Test that a valid point on the curve is accepted""" -+ # Generate a valid key and extract its coordinates -+ private_key = ec.generate_private_key(ec.SECP256R1()) -+ public_key = private_key.public_key() -+ numbers = public_key.public_numbers() -+ -+ # Create TPM2B_PUBLIC with valid coordinates -+ tpm2b_public = self.create_ecc_tpm2b_public(numbers.x, numbers.y, TPM_ECC_NIST_P256) -+ -+ # Should parse successfully -+ parsed_key, _ = pubkey_parms_from_tpm2b_public(tpm2b_public) -+ self.assertIsInstance(parsed_key, ec.EllipticCurvePublicKey) -+ -+ # Verify the coordinates match -+ assert isinstance(parsed_key, ec.EllipticCurvePublicKey) # Type narrowing for pyright -+ parsed_numbers = parsed_key.public_numbers() -+ self.assertEqual(parsed_numbers.x, numbers.x) -+ self.assertEqual(parsed_numbers.y, numbers.y) -+ -+ def test_small_subgroup_not_applicable_to_nist_curves(self): -+ """Test documenting that small subgroup checks are not needed for NIST curves (Security Check #3) -+ -+ NIST P-curves (P-192, P-224, P-256, P-384, P-521) all have cofactor h=1, -+ meaning the entire curve has prime order. There are no small subgroups to check. -+ -+ Curves with cofactor > 1 (like Curve25519 with h=8) require additional validation -+ to ensure the point is not in a small subgroup, but this is not applicable to -+ the NIST curves used by TPMs. -+ """ -+ # This test documents the cofactor=1 property for all supported NIST curves -+ # The cryptography library's point validation is sufficient for these curves -+ -+ test_curves = [ -+ (TPM_ECC_NIST_P192, ec.SECP192R1()), -+ (TPM_ECC_NIST_P224, ec.SECP224R1()), -+ (TPM_ECC_NIST_P256, ec.SECP256R1()), -+ (TPM_ECC_NIST_P384, ec.SECP384R1()), -+ (TPM_ECC_NIST_P521, ec.SECP521R1()), -+ ] -+ -+ for curve_id, curve_obj in test_curves: -+ with self.subTest(curve=curve_obj.name): -+ try: -+ # Generate a valid key for this curve -+ # Note: P-192 may not be supported in newer OpenSSL versions -+ private_key = ec.generate_private_key(curve_obj) -+ except Exception: # pylint: disable=broad-except -+ # Skip this specific curve if not supported by OpenSSL (e.g., P-192) -+ self.skipTest(f"Curve {curve_obj.name} not supported by OpenSSL") -+ -+ public_key = private_key.public_key() -+ numbers = public_key.public_numbers() -+ -+ # Create TPM2B_PUBLIC and verify it parses successfully -+ tpm2b_public = self.create_ecc_tpm2b_public(numbers.x, numbers.y, curve_id) -+ parsed_key, _ = pubkey_parms_from_tpm2b_public(tpm2b_public) -+ -+ # All NIST curves have cofactor = 1, so no small subgroup attacks possible -+ # The point validation by the cryptography library is sufficient -+ self.assertIsInstance(parsed_key, ec.EllipticCurvePublicKey) -+ -+ def test_coordinate_exceeds_field_prime_rejected(self): -+ """Test that coordinates >= field prime are rejected""" -+ # Use a coordinate value that's >= the field prime for P-256 -+ p256_prime = ECC_CURVE_PRIMES[TPM_ECC_NIST_P256] -+ -+ # x coordinate exceeds the field prime -+ x = p256_prime + 1 -+ y = 1 -+ -+ tpm2b_public = self.create_ecc_tpm2b_public(x, y, TPM_ECC_NIST_P256) -+ -+ # Should be rejected during coordinate validation -+ with self.assertRaises(ValueError) as cm: -+ pubkey_parms_from_tpm2b_public(tpm2b_public) -+ # Will fail either at coordinate validation or curve validation -+ self.assertTrue( -+ "coordinate too large" in str(cm.exception).lower() or "not on the curve" in str(cm.exception).lower() -+ ) -+ -+ def test_p521_point_validation(self): -+ """Test point validation works correctly for P-521 (non-byte-aligned curve)""" -+ # Generate a valid P-521 key -+ private_key = ec.generate_private_key(ec.SECP521R1()) -+ public_key = private_key.public_key() -+ numbers = public_key.public_numbers() -+ -+ # Valid point should be accepted -+ tpm2b_public = self.create_ecc_tpm2b_public(numbers.x, numbers.y, TPM_ECC_NIST_P521) -+ parsed_key, _ = pubkey_parms_from_tpm2b_public(tpm2b_public) -+ self.assertIsInstance(parsed_key, ec.EllipticCurvePublicKey) -+ -+ # Invalid point should be rejected -+ tpm2b_public_invalid = self.create_ecc_tpm2b_public(1, 1, TPM_ECC_NIST_P521) -+ with self.assertRaises(ValueError) as cm: -+ pubkey_parms_from_tpm2b_public(tpm2b_public_invalid) -+ self.assertIn("not on the curve", str(cm.exception).lower()) -+ -+ -+if __name__ == "__main__": -+ unittest.main() --- -2.47.3 - diff --git a/0017-tpm-fix-ECC-P-521-credential-activation-with-consist.patch b/0017-tpm-fix-ECC-P-521-credential-activation-with-consist.patch deleted file mode 100644 index f32c723..0000000 --- a/0017-tpm-fix-ECC-P-521-credential-activation-with-consist.patch +++ /dev/null @@ -1,221 +0,0 @@ -From 2acb35cb5b203f08aa281c571d341406ff1602c2 Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Thu, 25 Sep 2025 14:25:18 +0100 -Subject: [PATCH 17/18] tpm: fix ECC P-521 credential activation with - consistent marshaling - -The TPM credential activation was failing for P-521 curves due to -inconsistent ECC point marshaling in tpms_ecc_point_marshal(). - -The function used bit_length() which varies for P-521 coordinates -(520-521 bits), producing different blob sizes and causing TPM -integrity check failures during ActivateCredential operations. - -Assisted-by: Claude 4 Sonnet -Signed-off-by: Sergio Correia ---- - keylime/tpm/tpm2_objects.py | 11 +-- - keylime/tpm/tpm_util.py | 7 +- - test/test_tpm2_objects.py | 130 ++++++++++++++++++++++++++++++++++++ - 3 files changed, 142 insertions(+), 6 deletions(-) - -diff --git a/keylime/tpm/tpm2_objects.py b/keylime/tpm/tpm2_objects.py -index 9170628..d33ebaa 100644 ---- a/keylime/tpm/tpm2_objects.py -+++ b/keylime/tpm/tpm2_objects.py -@@ -597,9 +597,12 @@ def unmarshal_tpml_pcr_selection(tpml_pcr_selection: bytes) -> Tuple[Dict[int, i - - def tpms_ecc_point_marshal(public_key: EllipticCurvePublicKey) -> bytes: - pn = public_key.public_numbers() -+ curve = public_key.curve - -- sz = (pn.x.bit_length() + 7) // 8 -- secret = struct.pack(">H", sz) + pn.x.to_bytes(sz, "big") -+ # Use fixed coordinate size based on curve to ensure consistent marshaling -+ # This is critical for P-521 where bit_length() can vary (520-521 bits) -+ # leading to credential activation failures due to inconsistent blob sizes -+ coord_size = (curve.key_size + 7) // 8 - -- sz = (pn.y.bit_length() + 7) // 8 -- return secret + struct.pack(">H", sz) + pn.y.to_bytes(sz, "big") -+ secret = struct.pack(">H", coord_size) + pn.x.to_bytes(coord_size, "big") -+ return secret + struct.pack(">H", coord_size) + pn.y.to_bytes(coord_size, "big") -diff --git a/keylime/tpm/tpm_util.py b/keylime/tpm/tpm_util.py -index 25c40e0..fbbe557 100644 ---- a/keylime/tpm/tpm_util.py -+++ b/keylime/tpm/tpm_util.py -@@ -318,11 +318,14 @@ def crypt_secret_encrypt_ecc(public_key: EllipticCurvePublicKey, hashfunc: hashe - - digest_size = hashfunc.digest_size - -+ # Use fixed coordinate size for consistent marshaling -+ coord_size = (public_key.curve.key_size + 7) // 8 -+ - x = my_public_key.public_numbers().x -- party_x = x.to_bytes((x.bit_length() + 7) >> 3, "big") -+ party_x = x.to_bytes(coord_size, "big") - - x = public_key.public_numbers().x -- party_y = x.to_bytes((x.bit_length() + 7) >> 3, "big") -+ party_y = x.to_bytes(coord_size, "big") - - data = crypt_kdfe(hashfunc, ecc_secret_x, "IDENTITY", party_x, party_y, digest_size << 3) - -diff --git a/test/test_tpm2_objects.py b/test/test_tpm2_objects.py -index c880770..48d6a43 100644 ---- a/test/test_tpm2_objects.py -+++ b/test/test_tpm2_objects.py -@@ -1,6 +1,7 @@ - import struct - import unittest - -+from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives.asymmetric import ec - - from keylime.tpm.tpm2_objects import ( -@@ -13,7 +14,9 @@ from keylime.tpm.tpm2_objects import ( - _curve_from_curve_id, - _pack_in_tpm2b, - pubkey_parms_from_tpm2b_public, -+ tpms_ecc_point_marshal, - ) -+from keylime.tpm.tpm_util import crypt_secret_encrypt_ecc - - - class TestTpm2Objects(unittest.TestCase): -@@ -413,5 +416,132 @@ class TestEccPublicKeySecurityValidation(unittest.TestCase): - self.assertIn("not on the curve", str(cm.exception).lower()) - - -+class TestEccMarshaling(unittest.TestCase): -+ """Test ECC point marshaling consistency fixes""" -+ -+ def test_p521_marshaling_consistency(self): -+ """Test that P-521 marshaling produces consistent blob sizes regardless of coordinate values""" -+ # Generate multiple P-521 keys to test with different coordinate values -+ keys = [] -+ for _ in range(10): -+ private_key = ec.generate_private_key(ec.SECP521R1()) -+ keys.append(private_key.public_key()) -+ -+ # Marshal all keys and check that blob sizes are consistent -+ blob_sizes = [] -+ for key in keys: -+ blob = tpms_ecc_point_marshal(key) -+ blob_sizes.append(len(blob)) -+ -+ # All blobs should be the same size for P-521 -+ self.assertEqual(len(set(blob_sizes)), 1, "All P-521 marshaled blobs should have the same size") -+ -+ # Expected size: 2 bytes (x size) + 66 bytes (x coord) + 2 bytes (y size) + 66 bytes (y coord) = 136 bytes -+ expected_size = 2 + 66 + 2 + 66 -+ self.assertEqual(blob_sizes[0], expected_size, f"P-521 marshaled blob should be {expected_size} bytes") -+ -+ def test_marshaling_coordinate_sizes(self): -+ """Test that marshaled coordinates use fixed sizes based on curve key size""" -+ # Test P-521: 521 bits -> (521 + 7) // 8 = 66 bytes per coordinate -+ p521_key = ec.generate_private_key(ec.SECP521R1()).public_key() -+ p521_blob = tpms_ecc_point_marshal(p521_key) -+ -+ # Parse the blob to check coordinate sizes -+ x_size = struct.unpack(">H", p521_blob[:2])[0] -+ y_size = struct.unpack(">H", p521_blob[2 + x_size : 2 + x_size + 2])[0] -+ -+ self.assertEqual(x_size, 66, "P-521 X coordinate should be 66 bytes") -+ self.assertEqual(y_size, 66, "P-521 Y coordinate should be 66 bytes") -+ -+ # Test P-256: 256 bits -> (256 + 7) // 8 = 32 bytes per coordinate -+ p256_key = ec.generate_private_key(ec.SECP256R1()).public_key() -+ p256_blob = tpms_ecc_point_marshal(p256_key) -+ -+ x_size = struct.unpack(">H", p256_blob[:2])[0] -+ y_size = struct.unpack(">H", p256_blob[2 + x_size : 2 + x_size + 2])[0] -+ -+ self.assertEqual(x_size, 32, "P-256 X coordinate should be 32 bytes") -+ self.assertEqual(y_size, 32, "P-256 Y coordinate should be 32 bytes") -+ -+ def test_p521_credential_activation_consistency(self): -+ """Test the specific issue: P-521 credential activation with consistent marshaling""" -+ # This test verifies the fix for credential activation failures -+ # Generate two P-521 keys with potentially different bit lengths for coordinates -+ key1 = ec.generate_private_key(ec.SECP521R1()).public_key() -+ key2 = ec.generate_private_key(ec.SECP521R1()).public_key() -+ -+ # Marshal both keys -+ blob1 = tpms_ecc_point_marshal(key1) -+ blob2 = tpms_ecc_point_marshal(key2) -+ -+ # The critical fix: both blobs should be the same size regardless of coordinate bit lengths -+ self.assertEqual( -+ len(blob1), len(blob2), "P-521 marshaled blobs must be same size regardless of coordinate bit lengths" -+ ) -+ -+ # Both should use the fixed coordinate size (66 bytes) -+ expected_total_size = 2 + 66 + 2 + 66 # size_x + x + size_y + y -+ self.assertEqual(len(blob1), expected_total_size) -+ self.assertEqual(len(blob2), expected_total_size) -+ -+ def test_marshaling_format_correctness(self): -+ """Test that marshaling follows the correct TPM format: size(2) + coord(n) + size(2) + coord(n)""" -+ key = ec.generate_private_key(ec.SECP521R1()).public_key() -+ blob = tpms_ecc_point_marshal(key) -+ -+ # Parse the blob structure -+ if len(blob) < 4: -+ self.fail("Marshaled blob too short") -+ -+ x_size = struct.unpack(">H", blob[:2])[0] -+ self.assertEqual(x_size, 66, "X coordinate size should be 66 for P-521") -+ -+ if len(blob) < 2 + x_size + 2: -+ self.fail("Marshaled blob missing Y coordinate size") -+ -+ y_size = struct.unpack(">H", blob[2 + x_size : 2 + x_size + 2])[0] -+ self.assertEqual(y_size, 66, "Y coordinate size should be 66 for P-521") -+ -+ # Total size should be: 2 + 66 + 2 + 66 = 136 -+ expected_total = 2 + x_size + 2 + y_size -+ self.assertEqual(len(blob), expected_total, "Total marshaled blob size incorrect") -+ -+ def test_crypt_secret_encrypt_ecc_consistency(self): -+ """Test that crypt_secret_encrypt_ecc produces consistent results with fixed coordinate sizes""" -+ # Generate a P-521 key to test with -+ public_key = ec.generate_private_key(ec.SECP521R1()).public_key() -+ hashfunc = hashes.SHA256() -+ -+ # Call the function multiple times and check consistency -+ results = [] -+ for _ in range(5): -+ data, point = crypt_secret_encrypt_ecc(public_key, hashfunc) -+ results.append((data, point)) -+ -+ # Check that all returned points have consistent marshaling -+ # (the data will be different due to random key generation, but point marshaling should be consistent) -+ point_sizes = [len(point) for _, point in results] -+ self.assertEqual(len(set(point_sizes)), 1, "All marshaled points should have the same size") -+ -+ # For P-521, the marshaled point should be 136 bytes (2+66+2+66) -+ expected_point_size = 2 + 66 + 2 + 66 -+ self.assertEqual( -+ point_sizes[0], expected_point_size, f"P-521 marshaled point should be {expected_point_size} bytes" -+ ) -+ -+ # All data results should be different (due to random ephemeral keys) -+ data_results = [data for data, _ in results] -+ self.assertEqual( -+ len(set(data_results)), -+ len(data_results), -+ "All data results should be different due to random ephemeral keys", -+ ) -+ -+ # All data results should have the same length (SHA256 digest size) -+ data_sizes = [len(data) for data, _ in results] -+ self.assertEqual(len(set(data_sizes)), 1, "All data results should have the same size") -+ self.assertEqual(data_sizes[0], hashfunc.digest_size, "Data size should match hash digest size") -+ -+ - if __name__ == "__main__": - unittest.main() --- -2.47.3 - diff --git a/0018-tpm-fix-ECC-signature-parsing-to-support-variable-le.patch b/0018-tpm-fix-ECC-signature-parsing-to-support-variable-le.patch deleted file mode 100644 index fbe8cbb..0000000 --- a/0018-tpm-fix-ECC-signature-parsing-to-support-variable-le.patch +++ /dev/null @@ -1,372 +0,0 @@ -From ee4192df70384fa6b23f359a287e042103ba4ea9 Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Thu, 25 Sep 2025 14:37:10 +0100 -Subject: [PATCH 18/18] tpm: fix ECC signature parsing to support - variable-length coordinates - -The previous ECC signature validation implementation incorrectly assumed -fixed-length coordinate encoding, causing failures with mathematically -correct variable-length coordinates (especially P-521 curves where -coordinates are typically 65-66 bytes). - -This commit fixes the ecdsa_der_from_tpm() function to properly handle -TSS ESAPI signature format where the sig_size field contains only the -r component size, followed by the s component with its own size header. - -This enables proper ECC attestation for the supported NIST curves. - -Assisted-by: Claude 4 Sonnet -Signed-off-by: Sergio Correia ---- - keylime/tpm/tpm_main.py | 2 +- - keylime/tpm/tpm_util.py | 105 +++++++++++++++++++--- - test/test_tpm2_objects.py | 184 +++++++++++++++++++++++++++++++++++++- - 3 files changed, 277 insertions(+), 14 deletions(-) - -diff --git a/keylime/tpm/tpm_main.py b/keylime/tpm/tpm_main.py -index 6f2e89f..ecbacbe 100644 ---- a/keylime/tpm/tpm_main.py -+++ b/keylime/tpm/tpm_main.py -@@ -91,7 +91,7 @@ class Tpm: - if isinstance(iak_pub, EllipticCurvePublicKey): - if sig_alg in [tpm2_objects.TPM_ALG_ECDSA]: - try: -- der_sig = tpm_util.ecdsa_der_from_tpm(iak_sign) -+ der_sig = tpm_util.ecdsa_der_from_tpm(iak_sign, iak_pub) - tpm_util.verify(iak_pub, der_sig, digest, hashfunc) - logger.info("Agent %s AIK verified with IAK", uuid) - return True -diff --git a/keylime/tpm/tpm_util.py b/keylime/tpm/tpm_util.py -index fbbe557..f554f94 100644 ---- a/keylime/tpm/tpm_util.py -+++ b/keylime/tpm/tpm_util.py -@@ -59,6 +59,43 @@ logger = keylime_logging.init_logging("tpm_util") - - SupportedKeyTypes = Union[RSAPublicKey, EllipticCurvePublicKey] - -+# ECC signature parsing constants. -+# Raw signature sizes for different ECC curves (r||s concatenated format). -+# r and s are the two mathematical components of an ECDSA signature. -+ECC_SECP192R1_SIGNATURE_SIZE = 48 # 24 bytes each for r,s. -+ECC_SECP224R1_SIGNATURE_SIZE = 56 # 28 bytes each for r,s. -+ECC_SECP256R1_SIGNATURE_SIZE = 64 # 32 bytes each for r,s. -+ECC_SECP384R1_SIGNATURE_SIZE = 96 # 48 bytes each for r,s. -+ECC_SECP521R1_SIGNATURE_SIZE = 132 # 66 bytes each for r,s. -+ -+# TPM2B_ECDSA_SIGNATURE format constants. -+TPM2B_SIZE_FIELD_LENGTH = 2 # 2 bytes for size field. -+TPM2B_MIN_HEADER_SIZE = 4 # Minimum: 2 bytes r_size + 2 bytes s_size. -+ -+# DER encoding constants. -+DER_SEQUENCE_TAG = 0x30 -+ -+# Signature blob header offset (skip alg, hash_alg, sig_size headers). -+SIGNATURE_BLOB_HEADER_SIZE = 6 -+ -+# ECC curve to signature size mapping for raw r||s format. -+ECC_RAW_SIGNATURE_SIZES = { -+ "secp192r1": ECC_SECP192R1_SIGNATURE_SIZE, -+ "secp224r1": ECC_SECP224R1_SIGNATURE_SIZE, -+ "secp256r1": ECC_SECP256R1_SIGNATURE_SIZE, -+ "secp384r1": ECC_SECP384R1_SIGNATURE_SIZE, -+ "secp521r1": ECC_SECP521R1_SIGNATURE_SIZE, -+} -+ -+# ECC curve coordinate size ranges (min, max) for validation. -+ECC_COORDINATE_SIZE_RANGES = { -+ "secp192r1": (1, 24), # 192 bits = 24 bytes max -+ "secp224r1": (1, 28), # 224 bits = 28 bytes max -+ "secp256r1": (1, 32), # 256 bits = 32 bytes max -+ "secp384r1": (1, 48), # 384 bits = 48 bytes max -+ "secp521r1": (1, 66), # 521 bits = 66 bytes max (521 bits, not 512) -+} -+ - - def verify( - pubkey: SupportedKeyTypes, -@@ -106,17 +143,59 @@ def der_len(encoded_int_len: int) -> bytes: - return bytes((0x80 | len(bin_str),)) + bin_str - - --def ecdsa_der_from_tpm(sigblob: bytes) -> bytes: -- _, _, sig_size_r = struct.unpack_from(">HHH", sigblob, 0) -- sig_r = sigblob[6 : 6 + sig_size_r] -- encoded_sig_r = der_int(sig_r) -- sigblob = sigblob[6 + sig_size_r :] -- sig_size_s = struct.unpack_from(">H", sigblob, 0)[0] -- sig_s = sigblob[2 : 2 + sig_size_s] -- encoded_sig_s = der_int(sig_s) -- total_size = len(encoded_sig_r) + len(encoded_sig_s) -- der_sig = bytes.fromhex(f"30{total_size:x}") + encoded_sig_r + encoded_sig_s -- return der_sig -+def ecdsa_der_from_tpm(sigblob: bytes, pubkey: EllipticCurvePublicKey) -> bytes: -+ """Convert ECC signature from TPM format to DER format for cryptographic verification. -+ -+ This function handles TSS ESAPI signature format where the signature header's -+ sig_size field contains the size of the r component, followed by the s component -+ with its own size header. -+ -+ Parameters -+ ---------- -+ sigblob: TPM signature blob containing signature headers and signature data -+ pubkey: ECC public key to determine expected signature size -+ -+ Returns -+ ------- -+ DER-encoded ECDSA signature suitable for cryptographic library verification -+ -+ Raises -+ ------ -+ ValueError: If signature format cannot be parsed or is invalid -+ """ -+ # Extract signature header information. -+ _sig_alg, _hash_alg, sig_size_r = struct.unpack_from(">HHH", sigblob, 0) -+ -+ # Extract the r component (size is in sig_size_r field). -+ sig_r = sigblob[SIGNATURE_BLOB_HEADER_SIZE : SIGNATURE_BLOB_HEADER_SIZE + sig_size_r] -+ -+ # The s component follows immediately after r, with its own size header. -+ s_offset = SIGNATURE_BLOB_HEADER_SIZE + sig_size_r -+ if s_offset + 2 <= len(sigblob): -+ sig_size_s = struct.unpack_from(">H", sigblob, s_offset)[0] -+ s_start = s_offset + 2 -+ if s_start + sig_size_s <= len(sigblob): -+ sig_s = sigblob[s_start : s_start + sig_size_s] -+ -+ # Validate coordinate sizes against curve requirements. -+ curve_name = pubkey.curve.name -+ coordinate_range = ECC_COORDINATE_SIZE_RANGES.get(curve_name) -+ if coordinate_range: -+ min_size, max_size = coordinate_range -+ if not min_size <= len(sig_r) <= max_size: -+ raise ValueError(f"Invalid r coordinate size {len(sig_r)} for curve {curve_name}") -+ if not min_size <= len(sig_s) <= max_size: -+ raise ValueError(f"Invalid s coordinate size {len(sig_s)} for curve {curve_name}") -+ -+ # Convert to DER format. -+ encoded_sig_r = der_int(sig_r) -+ encoded_sig_s = der_int(sig_s) -+ total_size = len(encoded_sig_r) + len(encoded_sig_s) -+ der_length = der_len(total_size) -+ der_sig = bytes([DER_SEQUENCE_TAG]) + der_length + encoded_sig_r + encoded_sig_s -+ return der_sig -+ -+ raise ValueError("Unable to parse ECC signature from TPM format") - - - def __get_pcrs_from_blob(pcrblob: bytes) -> Tuple[int, Dict[int, int], List[bytes]]: -@@ -238,7 +317,9 @@ def checkquote( - (sig_size,) = struct.unpack_from(">H", sigblob, 4) - (signature,) = struct.unpack_from(f"{sig_size}s", sigblob, 6) - elif sig_alg in [tpm2_objects.TPM_ALG_ECDSA]: -- signature = ecdsa_der_from_tpm(sigblob) -+ if not isinstance(pubkey, EllipticCurvePublicKey): -+ raise ValueError(f"ECDSA signature algorithm requires EllipticCurvePublicKey, got {type(pubkey)}") -+ signature = ecdsa_der_from_tpm(sigblob, pubkey) - else: - raise ValueError(f"Unsupported quote signature algorithm '{sig_alg:#x}'") - -diff --git a/test/test_tpm2_objects.py b/test/test_tpm2_objects.py -index 48d6a43..c0e4c0a 100644 ---- a/test/test_tpm2_objects.py -+++ b/test/test_tpm2_objects.py -@@ -16,7 +16,7 @@ from keylime.tpm.tpm2_objects import ( - pubkey_parms_from_tpm2b_public, - tpms_ecc_point_marshal, - ) --from keylime.tpm.tpm_util import crypt_secret_encrypt_ecc -+from keylime.tpm.tpm_util import crypt_secret_encrypt_ecc, der_int, der_len, ecdsa_der_from_tpm - - - class TestTpm2Objects(unittest.TestCase): -@@ -543,5 +543,187 @@ class TestEccMarshaling(unittest.TestCase): - self.assertEqual(data_sizes[0], hashfunc.digest_size, "Data size should match hash digest size") - - -+class TestEccSignatureParsing(unittest.TestCase): -+ """Test ECC signature parsing improvements for variable-length coordinates""" -+ -+ def create_test_signature_blob(self, sig_r: bytes, sig_s: bytes) -> bytes: -+ """Create a test TPM signature blob with given r and s components""" -+ # TPM signature format: sig_alg(2) + hash_alg(2) + sig_size_r(2) + r_data + sig_size_s(2) + s_data -+ sig_alg = 0x0018 # TPM_ALG_ECDSA -+ hash_alg = 0x000B # TPM_ALG_SHA256 -+ -+ blob = struct.pack(">HHH", sig_alg, hash_alg, len(sig_r)) -+ blob += sig_r -+ blob += struct.pack(">H", len(sig_s)) -+ blob += sig_s -+ -+ return blob -+ -+ def test_p521_variable_length_coordinates(self): -+ """Test that P-521 signatures with variable-length coordinates are parsed correctly""" -+ # Generate a P-521 key for testing -+ private_key = ec.generate_private_key(ec.SECP521R1()) -+ public_key = private_key.public_key() -+ -+ # Test with 65-byte coordinates (leading zero stripped) -+ sig_r_65 = b"\x00" * 1 + b"\x01" * 64 # 65 bytes -+ sig_s_65 = b"\x00" * 1 + b"\x02" * 64 # 65 bytes -+ -+ blob_65 = self.create_test_signature_blob(sig_r_65, sig_s_65) -+ -+ # Should parse successfully -+ der_sig_65 = ecdsa_der_from_tpm(blob_65, public_key) -+ self.assertIsInstance(der_sig_65, bytes) -+ self.assertTrue(len(der_sig_65) > 0) -+ -+ # Test with 66-byte coordinates (full padding) -+ sig_r_66 = b"\x00" * 2 + b"\x01" * 64 # 66 bytes -+ sig_s_66 = b"\x00" * 2 + b"\x02" * 64 # 66 bytes -+ -+ blob_66 = self.create_test_signature_blob(sig_r_66, sig_s_66) -+ -+ # Should parse successfully -+ der_sig_66 = ecdsa_der_from_tpm(blob_66, public_key) -+ self.assertIsInstance(der_sig_66, bytes) -+ self.assertTrue(len(der_sig_66) > 0) -+ -+ def test_coordinate_size_validation(self): -+ """Test that coordinate size validation works for different curves""" -+ # Test P-256 with valid coordinates -+ p256_key = ec.generate_private_key(ec.SECP256R1()).public_key() -+ -+ # Valid P-256 coordinates (32 bytes each) -+ sig_r_32 = b"\x01" * 32 -+ sig_s_32 = b"\x02" * 32 -+ blob_p256_valid = self.create_test_signature_blob(sig_r_32, sig_s_32) -+ -+ # Should parse successfully -+ der_sig = ecdsa_der_from_tpm(blob_p256_valid, p256_key) -+ self.assertIsInstance(der_sig, bytes) -+ -+ # Test P-256 with invalid coordinates (too large) -+ sig_r_invalid = b"\x01" * 50 # Too large for P-256 -+ sig_s_invalid = b"\x02" * 50 # Too large for P-256 -+ blob_p256_invalid = self.create_test_signature_blob(sig_r_invalid, sig_s_invalid) -+ -+ # Should raise ValueError -+ with self.assertRaises(ValueError) as cm: -+ ecdsa_der_from_tpm(blob_p256_invalid, p256_key) -+ self.assertIn("Invalid r coordinate size", str(cm.exception)) -+ -+ def test_signature_parsing_edge_cases(self): -+ """Test edge cases in signature parsing""" -+ p256_key = ec.generate_private_key(ec.SECP256R1()).public_key() -+ -+ # Test with truncated blob (missing s component) -+ truncated_blob = struct.pack(">HHH", 0x0018, 0x000B, 32) + b"\x01" * 32 -+ # Missing s component -+ -+ with self.assertRaises(ValueError) as cm: -+ ecdsa_der_from_tpm(truncated_blob, p256_key) -+ self.assertIn("Unable to parse ECC signature", str(cm.exception)) -+ -+ # Test with blob too short for s size header -+ short_blob = struct.pack(">HHH", 0x0018, 0x000B, 32) + b"\x01" * 32 + b"\x00" # Only 1 byte for s size -+ -+ with self.assertRaises(ValueError) as cm: -+ ecdsa_der_from_tpm(short_blob, p256_key) -+ self.assertIn("Unable to parse ECC signature", str(cm.exception)) -+ -+ def test_der_encoding_correctness(self): -+ """Test that DER encoding produces correctly formatted output""" -+ p256_key = ec.generate_private_key(ec.SECP256R1()).public_key() -+ -+ # Create test coordinates -+ sig_r = b"\x01" * 32 -+ sig_s = b"\x02" * 32 -+ blob = self.create_test_signature_blob(sig_r, sig_s) -+ -+ der_sig = ecdsa_der_from_tpm(blob, p256_key) -+ -+ # DER signature should start with SEQUENCE tag (0x30) -+ self.assertEqual(der_sig[0], 0x30, "DER signature should start with SEQUENCE tag") -+ -+ # Should be parseable as DER format -+ # The structure should be: 0x30 + length + INTEGER(r) + INTEGER(s) -+ self.assertTrue(len(der_sig) >= 6, "DER signature should have minimum length") -+ -+ def test_multiple_curve_support(self): -+ """Test that signature parsing works for multiple curve types""" -+ test_cases = [ -+ (ec.SECP256R1(), 32), -+ (ec.SECP384R1(), 48), -+ (ec.SECP521R1(), 66), -+ ] -+ -+ for curve, coord_size in test_cases: -+ with self.subTest(curve=curve.name): -+ private_key = ec.generate_private_key(curve) -+ public_key = private_key.public_key() -+ -+ # Create test signature with appropriate coordinate size -+ sig_r = b"\x01" * coord_size -+ sig_s = b"\x02" * coord_size -+ blob = self.create_test_signature_blob(sig_r, sig_s) -+ -+ # Should parse successfully -+ der_sig = ecdsa_der_from_tpm(blob, public_key) -+ self.assertIsInstance(der_sig, bytes) -+ self.assertTrue(len(der_sig) > 0) -+ self.assertEqual(der_sig[0], 0x30) # DER SEQUENCE tag -+ -+ def test_der_int_encoding(self): -+ """Test DER integer encoding helper function""" -+ # Test positive number that doesn't need padding -+ test_bytes = b"\x7F" # 127, no padding needed -+ der_encoded = der_int(test_bytes) -+ expected = b"\x02\x01\x7F" # INTEGER tag + length + value -+ self.assertEqual(der_encoded, expected) -+ -+ # Test positive number that needs zero padding (high bit set) -+ test_bytes = b"\xFF" # 255, needs zero padding -+ der_encoded = der_int(test_bytes) -+ expected = b"\x02\x02\x00\xFF" # INTEGER tag + length + zero padding + value -+ self.assertEqual(der_encoded, expected) -+ -+ def test_der_len_encoding(self): -+ """Test DER length encoding helper function""" -+ # Test short form (< 128) -+ short_len = der_len(50) -+ self.assertEqual(short_len, b"\x32") # 50 in hex -+ -+ # Test long form (>= 128) -+ long_len = der_len(300) # 0x012C -+ expected = b"\x82\x01\x2C" # Long form: 0x80 | 2 bytes, then 0x012C -+ self.assertEqual(long_len, expected) -+ -+ def test_signature_format_validation_comprehensive(self): -+ """Comprehensive test of signature format validation""" -+ p521_key = ec.generate_private_key(ec.SECP521R1()).public_key() -+ -+ # Test minimum valid coordinate sizes for P-521 -+ valid_sizes = [65, 66] -+ for size in valid_sizes: -+ sig_r = b"\x01" * size -+ sig_s = b"\x02" * size -+ blob = self.create_test_signature_blob(sig_r, sig_s) -+ -+ # Should not raise exception -+ der_sig = ecdsa_der_from_tpm(blob, p521_key) -+ self.assertIsInstance(der_sig, bytes) -+ -+ # Test invalid coordinate sizes for P-521 (outside the 1-66 range) -+ invalid_sizes = [0, 67, 100] # 0 is too small, 67+ is too large -+ for size in invalid_sizes: -+ with self.subTest(size=size): -+ sig_r = b"\x01" * size if size > 0 else b"" -+ sig_s = b"\x02" * size if size > 0 else b"" -+ blob = self.create_test_signature_blob(sig_r, sig_s) -+ -+ with self.assertRaises(ValueError) as cm: -+ ecdsa_der_from_tpm(blob, p521_key) -+ self.assertIn("coordinate size", str(cm.exception)) -+ -+ - if __name__ == "__main__": - unittest.main() --- -2.47.3 - diff --git a/0019-Added-manpages-for-keylime-services-and-the-tenant.patch b/0019-Added-manpages-for-keylime-services-and-the-tenant.patch deleted file mode 100644 index ca0d572..0000000 --- a/0019-Added-manpages-for-keylime-services-and-the-tenant.patch +++ /dev/null @@ -1,974 +0,0 @@ -From af74d817bd7c8c2a2e014e1728b46f41049ac066 Mon Sep 17 00:00:00 2001 -From: Marek Safarik -Date: Tue, 14 Oct 2025 15:14:26 +0200 -Subject: [PATCH] Added manpages for keylime services and the tenant - -Here I am adding manpages for keylime tenant, verifier, registrar, policy. -The RST formatting has been fixed for rst2man compatibility. - -Co-authored-by: Marek Safarik -Co-authored-by: Sergio Correia ---- - docs/man/keylime-policy.1.rst | 179 +++++++++++++++++++++ - docs/man/keylime_agent.8.rst | 218 +++++++++++++++++++++++++ - docs/man/keylime_registrar.8.rst | 142 +++++++++++++++++ - docs/man/keylime_tenant.1.rst | 263 +++++++++++++++++++++++++++++++ - docs/man/keylime_verifier.8.rst | 116 ++++++++++++++ - 5 files changed, 918 insertions(+) - create mode 100644 docs/man/keylime-policy.1.rst - create mode 100644 docs/man/keylime_agent.8.rst - create mode 100644 docs/man/keylime_registrar.8.rst - create mode 100644 docs/man/keylime_tenant.1.rst - create mode 100644 docs/man/keylime_verifier.8.rst - -diff --git a/docs/man/keylime-policy.1.rst b/docs/man/keylime-policy.1.rst -new file mode 100644 -index 0000000..b24c372 ---- /dev/null -+++ b/docs/man/keylime-policy.1.rst -@@ -0,0 +1,179 @@ -+============== -+keylime-policy -+============== -+ -+------------------------------------------ -+Keylime policy creation and signing tool -+------------------------------------------ -+ -+:Manual section: 1 -+:Author: Keylime Developers -+:Date: September 2025 -+ -+SYNOPSIS -+======== -+ -+**keylime-policy** {create,sign} [*OPTIONS*] -+ -+(Requires root privileges, use with sudo) -+ -+DESCRIPTION -+=========== -+ -+keylime-policy is a utility for creating and signing Keylime policies. It supports creating -+runtime policies (for IMA/filesystem attestation) and measured boot policies (for boot-time -+attestation), as well as signing runtime policies using DSSE (Dead Simple Signing Envelope). -+ -+COMMANDS -+======== -+ -+**keylime-policy create runtime** [*OPTIONS*] -+ -+ Create runtime policies from filesystem, allowlists, RPM repositories, or IMA measurement lists. -+ -+ Options: -+ -+ **-o, --output** *OUTPUT* -+ Output file (defaults to stdout) -+ -+ **-p, --base-policy** *BASE_POLICY* -+ Merge new data into existing JSON runtime policy -+ -+ **-k, --keyrings** -+ Create keyrings policy entries -+ -+ **-b, --ima-buf** -+ Process ima-buf entries other than keyrings -+ -+ **-a, --allowlist** *ALLOWLIST* -+ Read checksums from plain-text allowlist -+ -+ **-e, --excludelist** *EXCLUDE_LIST_FILE* -+ Add IMA exclude list to policy -+ -+ **-m, --ima-measurement-list** *[IMA_MEASUREMENT_LIST]* -+ Use IMA measurement list for hash/keyring extraction -+ -+ **--ignored-keyrings** *IGNORED_KEYRINGS* -+ Ignore specified keyring (repeatable) -+ -+ **--add-ima-signature-verification-key** *IMA_SIGNATURE_KEYS* -+ Add x509/key to tenant_keyring (repeatable) -+ -+ **--show-legacy-allowlist** -+ Display digests in legacy allowlist format -+ -+ **-v, --verbose** -+ Set log level to DEBUG -+ -+ Filesystem scanning: -+ -+ **--algo** *{sha1,sha256,sha384,sha512,sm3_256}* -+ Checksum algorithm -+ -+ **--ramdisk-dir** *RAMDISK_DIR* -+ Path to initrds (e.g., /boot) -+ -+ **--rootfs** *ROOTFS* -+ Path to root filesystem (e.g., /) -+ -+ **-s, --skip-path** *SKIP_PATH* -+ Comma-separated directories to skip -+ -+ Repository scanning: -+ -+ **--local-rpm-repo** *LOCAL_RPM_REPO* -+ Local RPM repository directory -+ -+ **--remote-rpm-repo** *REMOTE_RPM_REPO* -+ Remote RPM repository URL -+ -+**keylime-policy create measured-boot** [*OPTIONS*] -+ -+ Create measured boot reference state policies from UEFI event logs. -+ -+ Options: -+ -+ **-e, --eventlog-file** *EVENTLOG_FILE* -+ Binary UEFI eventlog (required) -+ -+ **--without-secureboot, -i** -+ Create policy without SecureBoot (MeasuredBoot only) -+ -+ **-o, --output** *OUTPUT* -+ Output path for generated measured boot policy -+ -+**keylime-policy sign runtime** [*OPTIONS*] -+ -+ Sign runtime policies using DSSE. -+ -+ Options: -+ -+ **-o, --output** *OUTPUT_FILE* -+ Output file for DSSE-signed policy -+ -+ **-r, --runtime-policy** *POLICY* -+ Runtime policy file to sign (required) -+ -+ **-k, --keyfile** *KEYFILE* -+ EC private key for signing -+ -+ **-p, --keypath** *KEYPATH* -+ Output filename for created private key -+ -+ **-b, --backend** *{ecdsa,x509}* -+ DSSE backend (ecdsa or x509) -+ -+ **-c, --cert-outfile** *CERT_OUTFILE* -+ Output file for x509 certificate (x509 backend) -+ -+EXAMPLES -+======== -+ -+**Create runtime policy from filesystem:** -+ -+.. code-block:: bash -+ -+ sudo keylime-policy create runtime --rootfs / --output my-policy.json -+ -+**Create runtime policy from allowlist:** -+ -+.. code-block:: bash -+ -+ sudo keylime-policy create runtime --allowlist my-allowlist.txt --output policy.json -+ -+**Create measured boot policy:** -+ -+.. code-block:: bash -+ -+ sudo keylime-policy create measured-boot -e /sys/kernel/security/tpm0/binary_bios_measurements -o mb-policy.json -+ -+**Sign runtime policy:** -+ -+.. code-block:: bash -+ -+ sudo keylime-policy sign runtime -r policy.json -k signing-key.pem -o signed-policy.json -+ -+ENVIRONMENT -+=========== -+ -+**KEYLIME_LOGGING_CONFIG** -+ Path to logging.conf -+ -+NOTES -+===== -+ -+- All operations require root privileges -+- Runtime policies use JSON format -+- Measured boot policies require binary UEFI event logs -+- DSSE signing supports both ECDSA and x509 backends -+ -+SEE ALSO -+======== -+ -+**keylime_tenant**\(1), **keylime_verifier**\(8), **keylime_registrar**\(8) -+ -+BUGS -+==== -+ -+Report bugs at https://github.com/keylime/keylime/issues -diff --git a/docs/man/keylime_agent.8.rst b/docs/man/keylime_agent.8.rst -new file mode 100644 -index 0000000..dffcc09 ---- /dev/null -+++ b/docs/man/keylime_agent.8.rst -@@ -0,0 +1,218 @@ -+============= -+keylime_agent -+============= -+ -+----------------------------------------------- -+Keylime agent service for TPM-based attestation -+----------------------------------------------- -+ -+:Manual section: 8 -+:Author: Keylime Developers -+:Date: September 2025 -+ -+SYNOPSIS -+======== -+ -+**keylime_agent** -+ -+(Most operations require root privileges, use with sudo) -+ -+DESCRIPTION -+=========== -+ -+The agent is a long-running service that runs on systems to be attested. It communicates with -+the TPM to generate quotes, collects IMA and measured boot event logs, and provides secure -+payload functionality. The service does not accept command-line options; behavior is configured -+via TOML configuration files. -+ -+CONFIGURATION -+============= -+ -+Primary configuration is read from ``/etc/keylime/agent.conf`` (or an override via env). -+Configuration uses TOML format. All options are under the ``[agent]`` section. -+ -+Drop-in overrides: files in ``/etc/keylime/agent.conf.d/`` are applied in lexicographic order. -+ -+Essential configuration options: -+ -+**uuid** -+ Agent identifier (``generate``, ``hash_ek``, ``environment``, ``dmidecode``, ``hostname``, or explicit UUID) -+ -+**ip**, **port** -+ Bind address and port (default: 9002) -+ -+**contact_ip**, **contact_port** -+ External contact address (optional) -+ -+**registrar_ip**, **registrar_port** -+ Registrar endpoint -+ -+**enable_agent_mtls** -+ Enable mTLS communication -+ -+**tls_dir** -+ TLS material location (``generate`` for auto-generate under ``$KEYLIME_DIR/cv_ca``, ``default`` for ``$KEYLIME_DIR/secure``) -+ -+**server_key**, **server_key_password**, **server_cert** -+ TLS files (self-signed cert) -+ -+**trusted_client_ca** -+ Trusted client CA list -+ -+**enc_keyname** -+ Payload encryption key file name -+ -+**dec_payload_file** -+ Decrypted payload file name -+ -+**secure_size** -+ tmpfs partition size for secure storage -+ -+**tpm_ownerpassword** -+ TPM owner password (``generate`` for random) -+ -+**extract_payload_zip** -+ Auto-extract zip payloads (bool) -+ -+**enable_revocation_notifications** -+ Listen for revocation via ZeroMQ (bool) -+ -+**revocation_notification_ip**, **revocation_notification_port** -+ ZeroMQ endpoint -+ -+**revocation_cert** -+ Certificate to verify revocation messages -+ -+**revocation_actions** -+ Python scripts to run on revocation -+ -+**payload_script** -+ Script to run after payload extraction -+ -+**enable_insecure_payload** -+ Allow payloads without mTLS (insecure) -+ -+**measure_payload_pcr** -+ PCR to extend with payload (-1 to disable) -+ -+**exponential_backoff**, **retry_interval**, **max_retries** -+ TPM communication retry -+ -+**tpm_hash_alg**, **tpm_encryption_alg**, **tpm_signing_alg** -+ TPM algorithms -+ -+**ek_handle** -+ EK handle (``generate`` or explicit handle like ``0x81000000``) -+ -+**enable_iak_idevid** -+ Enable IAK/IDevID usage (bool) -+ -+**iak_idevid_template**, **iak_idevid_asymmetric_alg**, **iak_idevid_name_alg** -+ IAK/IDevID config -+ -+**idevid_password**, **idevid_handle**, **iak_password**, **iak_handle** -+ Persistent key handles -+ -+**iak_cert**, **idevid_cert** -+ Certificate file names -+ -+**run_as** -+ User:group to drop privileges to -+ -+**ima_ml_path** -+ IMA measurement log path (default: ``/sys/kernel/security/ima/ascii_runtime_measurements``) -+ -+**measuredboot_ml_path** -+ Measured boot log path (default: ``/sys/kernel/security/tpm0/binary_bios_measurements``) -+ -+ENVIRONMENT -+=========== -+ -+**KEYLIME_AGENT_CONFIG** -+ Path to agent.conf (highest priority) -+ -+**KEYLIME_LOGGING_CONFIG** -+ Path to logging.conf -+ -+**KEYLIME_DIR** -+ Working directory (default: ``/var/lib/keylime``) -+ -+**KEYLIME_AGENT_UUID** -+ UUID when ``uuid = environment`` -+ -+**KEYLIME_AGENT_IAK_CERT** -+ Override iak_cert path -+ -+**KEYLIME_AGENT_IDEVID_CERT** -+ Override idevid_cert path -+ -+**KEYLIME_TEST** -+ ``on/true/1`` enables testing mode -+ -+FILES -+===== -+ -+``/etc/keylime/agent.conf`` -+ TOML format configuration file -+ -+``/etc/keylime/agent.conf.d/`` -+ Drop-in snippets; read in lexicographic order -+ -+``/etc/keylime/logging.conf`` -+ Logging configuration -+ -+``$KEYLIME_DIR/secure/`` -+ Secure tmpfs mount for keys/payloads -+ -+``$KEYLIME_DIR/cv_ca/`` -+ TLS certificates when ``tls_dir = generate`` -+ -+``$KEYLIME_DIR/tpmdata.yml`` -+ TPM state persistence -+ -+RUNTIME -+======= -+ -+Start from system install: -+ -+.. code-block:: bash -+ -+ sudo keylime_agent -+ -+Start as a systemd service: -+ -+.. code-block:: bash -+ -+ sudo systemctl enable --now keylime_agent -+ -+Open firewall port: -+ -+.. code-block:: bash -+ -+ sudo firewall-cmd --add-port=9002/tcp -+ sudo firewall-cmd --runtime-to-permanent -+ -+PREREQUISITES -+============= -+ -+- Root privileges (use sudo) -+- TPM 2.0 available (verify with ``tpm2_pcrread``) -+- IMA enabled in kernel -+- Network connectivity to registrar -+ -+NOTES -+===== -+ -+- Agent uses TOML configuration format (unlike other Keylime components). -+- The Rust agent is the current implementation; Python agent is deprecated. -+- Agent generates self-signed certificates for mTLS if not provided. -+ -+SEE ALSO -+======== -+ -+**keylime_verifier**\(8), **keylime_registrar**\(8), **keylime_tenant**\(1) -+ -+BUGS -+==== -+ -+Report bugs at https://github.com/keylime/rust-keylime/issues -diff --git a/docs/man/keylime_registrar.8.rst b/docs/man/keylime_registrar.8.rst -new file mode 100644 -index 0000000..9841c34 ---- /dev/null -+++ b/docs/man/keylime_registrar.8.rst -@@ -0,0 +1,142 @@ -+================= -+keylime_registrar -+================= -+ -+------------------------------------------------ -+Keylime registrar service for agent registration -+------------------------------------------------ -+ -+:Manual section: 8 -+:Author: Keylime Developers -+:Date: September 2025 -+ -+SYNOPSIS -+======== -+ -+**keylime_registrar** -+ -+(Most operations require root privileges, use with sudo) -+ -+DESCRIPTION -+=========== -+ -+The registrar is a long-running service used by agents. It maintains its own database where it stores data -+of registered agents. The service does not accept command-line options; behavior is -+configured via configuration files and environment variables, and is managed by keylime tenant. -+ -+CONFIGURATION -+============= -+ -+Primary configuration is read from ``/etc/keylime/registrar.conf`` (or an override via env). -+All options are under the ``[registrar]`` section. -+ -+Essential configuration options: -+ -+**ip** -+ Bind address -+ -+**port** -+ HTTP port -+ -+**tls_port** -+ HTTPS port -+ -+**tls_dir** -+ TLS material location (``generate`` for auto-generate CA, keys, certs under ``$KEYLIME_DIR/reg_ca``, ``default`` for shared verifier CA under ``$KEYLIME_DIR/cv_ca``) -+ -+**server_key**, **server_key_password**, **server_cert**, **trusted_client_ca** -+ TLS files -+ -+**database_url** -+ SQLAlchemy URL; value ``sqlite`` maps to ``$KEYLIME_DIR/reg_data.sqlite`` -+ -+**database_pool_sz_ovfl** -+ Pool size, overflow (non-sqlite) -+ -+**auto_migrate_db** -+ Apply DB migrations on startup -+ -+**max_upload_size** -+ Request body limit (bytes) -+ -+**tpm_identity** -+ Allowed identity (``default``, ``ek_cert_or_iak_idevid``, ``ek_cert``, ``iak_idevid``) -+ -+**malformed_cert_action** -+ ``warn`` (default), ``reject``, or ``ignore`` -+ -+**durable_attestation_import** (optional) -+ Python import path to enable Durable Attestation -+ -+ENVIRONMENT -+=========== -+ -+**KEYLIME_REGISTRAR_CONFIG** -+ Path to registrar.conf (highest priority) -+ -+**KEYLIME_LOGGING_CONFIG** -+ Path to logging.conf -+ -+**KEYLIME_DIR** -+ Working directory (default: ``/var/lib/keylime``) -+ -+**KEYLIME_TEST** -+ ``on/true/1`` enables testing mode (looser checks; WORK_DIR becomes CWD) -+ -+FILES -+===== -+ -+``/etc/keylime/registrar.conf`` -+ Registrar configuration file -+ -+``/etc/keylime/logging.conf`` -+ Logging configuration -+ -+``$KEYLIME_DIR/reg_data.sqlite`` -+ Database file when ``database_url = sqlite`` -+ -+``$KEYLIME_DIR/reg_ca`` -+ TLS certificates when ``tls_dir = generate`` -+ -+``$KEYLIME_DIR/cv_ca`` -+ Shared verifier certificates when ``tls_dir = default`` -+ -+RUNTIME -+======= -+ -+Start from system install: -+ -+.. code-block:: bash -+ -+ sudo keylime_registrar -+ -+Start as a systemd service: -+ -+.. code-block:: bash -+ -+ systemctl enable --now keylime_registrar -+ -+Open firewall ports (adjust if you changed ports): -+ -+.. code-block:: bash -+ -+ firewall-cmd --add-port=8890/tcp --add-port=8891/tcp -+ firewall-cmd --runtime-to-permanent -+ -+NOTES -+===== -+ -+- HTTPS is required for routes unless explicitly allowed insecure by the service. -+- With ``tls_dir = default``, start the verifier before the registrar so the shared CA/certs exist in ``$KEYLIME_DIR/cv_ca``. -+- The service forks worker processes (default: CPU count). -+- Registrar and verifier may run on the same host or on separate hosts. -+ -+SEE ALSO -+======== -+ -+**keylime_verifier**\(8), **keylime_tenant**\(1), **keylime_agent**\(8) -+ -+BUGS -+==== -+ -+Report bugs at https://github.com/keylime/keylime/issues -diff --git a/docs/man/keylime_tenant.1.rst b/docs/man/keylime_tenant.1.rst -new file mode 100644 -index 0000000..7be1319 ---- /dev/null -+++ b/docs/man/keylime_tenant.1.rst -@@ -0,0 +1,263 @@ -+============== -+keylime_tenant -+============== -+ -+--------------------------------------------------------------------------- -+Keylime tenant management tool for agent provisioning and policy management -+--------------------------------------------------------------------------- -+ -+:Manual section: 1 -+:Author: Keylime Developers -+:Date: July 2025 -+ -+SYNOPSIS -+======== -+ -+**keylime_tenant** [*OPTIONS*] [*COMMAND*] -+ -+(Most operations require root privileges, use with sudo) -+ -+DESCRIPTION -+=========== -+ -+keylime_tenant is the primary command-line interface for managing Keylime agents and policies. -+It allows users to provision agents with TPM-based attestation, manage runtime policies, -+measured boot policies, and interact with Keylime registrar and verifier services. -+ -+The tenant can add, delete, update, and monitor agents, as well as manage various types of -+policies including runtime policies (for IMA/EVM attestation) and measured boot policies -+(for boot-time attestation). It supports both push and pull models for agent communication. -+ -+You can run keylime_tenant on the same system as the Keylime registrar or verifier, or on a separate system. -+ -+COMMANDS -+======== -+ -+**-c, --command** *COMMAND* -+ Specify the command to execute. Valid commands are: -+ -+ - **add**: Add a new agent to the system (default) -+ - **delete**: Remove an agent from the system -+ - **update**: Update an existing agent's configuration -+ - **regstatus**: Show agent status from registrar -+ - **cvstatus**: Show agent status from cloud verifier -+ - **status**: Show combined agent status -+ - **reglist**: List all agents in registrar -+ - **cvlist**: List all agents in cloud verifier -+ - **reactivate**: Reactivate a failed agent -+ - **regdelete**: Delete agent from registrar only -+ - **bulkinfo**: Get bulk information about agents -+ - **addruntimepolicy**: Add a runtime policy (requires --runtime-policy or --allowlist) -+ - **showruntimepolicy**: Display a runtime policy (requires --runtime-policy-name) -+ - **deleteruntimepolicy**: Remove a runtime policy (requires --runtime-policy-name) -+ - **updateruntimepolicy**: Update a runtime policy (requires --runtime-policy-name) -+ - **listruntimepolicy**: List all runtime policies -+ - **addmbpolicy**: Add a measured boot policy (requires --mb-policy-name) -+ - **showmbpolicy**: Display a measured boot policy (requires --mb-policy-name) -+ - **deletembpolicy**: Remove a measured boot policy (requires --mb-policy-name) -+ - **updatembpolicy**: Update a measured boot policy (requires --mb-policy-name) -+ - **listmbpolicy**: List all measured boot policies -+ -+OPTIONS -+======= -+ -+**-h, --help** -+ Show help message and exit -+ -+**--push-model** -+ Enable push model (avoid requests to keylime-agent) -+ -+**-t, --targethost** *AGENT_IP* -+ The IP address of the host to provision -+ -+**-tp, --targetport** *AGENT_PORT* -+ The port of the host to provision -+ -+**-r, --registrarhost** *REGISTRAR_IP* -+ The IP address of the registrar where to retrieve the agents data from -+ -+**-rp, --registrarport** *REGISTRAR_PORT* -+ The port of the registrar -+ -+**--cv_targethost** *CV_AGENT_IP* -+ The IP address of the host to provision that the verifier will use (optional). -+ Use only if different than argument to option -t/--targethost -+ -+**-v, --cv** *VERIFIER_IP* -+ The IP address of the cloud verifier -+ -+**-vp, --cvport** *VERIFIER_PORT* -+ The port of the cloud verifier -+ -+**-vi, --cvid** *VERIFIER_ID* -+ The unique identifier of a cloud verifier -+ -+**-nvc, --no-verifier-check** -+ Disable the check to confirm if the agent is being processed by the specified verifier. -+ Use only with -c/--command delete or reactivate -+ -+**-u, --uuid** *AGENT_UUID* -+ UUID for the agent to provision -+ -+**-f, --file** *FILE* -+ Deliver the specified plaintext file to the provisioned agent -+ -+**--cert** *CA_DIR* -+ Create and deliver a certificate using a CA created by ca-util. -+ Pass in the CA directory or use "default" to use the standard directory -+ -+**-k, --key** *KEYFILE* -+ An intermediate key file produced by user_data_encrypt -+ -+**-p, --payload** *PAYLOAD* -+ Specify the encrypted payload to deliver with encrypted keys specified by -k -+ -+**--include** *INCL_DIR* -+ Include additional files in provided directory in certificate zip file. -+ Must be specified with --cert -+ -+**--runtime-policy** *RUNTIME_POLICY* -+ Specify the file path of a runtime policy -+ -+**--runtime-policy-checksum** *RUNTIME_POLICY_CHECKSUM* -+ Specify the SHA-256 checksum of a runtime policy -+ -+**--runtime-policy-sig-key** *RUNTIME_POLICY_SIG_KEY* -+ Specify the public key file used to validate the runtime policy signature -+ -+**--runtime-policy-url** *RUNTIME_POLICY_URL* -+ Specify the URL of a remote runtime policy -+ -+**--runtime-policy-name** *RUNTIME_POLICY_NAME* -+ The name of the runtime policy to operate with -+ -+**--mb-policy** *MB_POLICY* -+ The measured boot policy to operate with -+ -+**--mb-policy-name** *MB_POLICY_NAME* -+ The name of the measured boot policy to operate with -+ -+**--tpm_policy** *TPM_POLICY* -+ Specify a TPM policy in JSON format. -+ Example: {"15":"0000000000000000000000000000000000000000"} -+ -+**--verify** -+ Block on cryptographically checked key derivation confirmation from the agent -+ once it has been provisioned -+ -+**--supported-version** *SUPPORTED_VERSION* -+ API version that is supported by the agent. Detected automatically by default -+ -+DEPRECATED OPTIONS -+================== -+ -+The following options are deprecated and may be removed in future versions: -+ -+**--allowlist** *ALLOWLIST* -+ **DEPRECATED**: Migrate to runtime policies for continued functionality. -+ Specify the file path of an allowlist -+ -+**--allowlist-url** *ALLOWLIST_URL* -+ **DEPRECATED**: Migrate to runtime policies for continued functionality. -+ Specify the URL of a remote allowlist -+ -+**--allowlist-name** *ALLOWLIST_NAME* -+ **DEPRECATED**: Migrate to runtime policies for continued functionality. -+ The name of allowlist to operate with -+ -+**--exclude** *IMA_EXCLUDE* -+ **DEPRECATED**: Migrate to runtime policies for continued functionality. -+ Specify the location of an IMA exclude list -+ -+**--mb_refstate** *MB_POLICY* -+ **DEPRECATED**: Use --mb-policy instead. -+ Specify the location of a measured boot reference state -+ -+**--signature-verification-key** *IMA_SIGN_VERIFICATION_KEYS* -+ **DEPRECATED**: Provide verification keys as part of a runtime policy for continued functionality. -+ Specify an IMA file signature verification key -+ -+EXAMPLES -+======== -+ -+**Add a new agent:** -+ -+.. code-block:: bash -+ -+ sudo keylime_tenant -c add -t 192.168.1.100 -u agent-001 -+ -+**Add an agent with runtime policy:** -+ -+.. code-block:: bash -+ -+ sudo keylime_tenant -c add -t 192.168.1.100 -u agent-001 --runtime-policy /path/to/policy.json -+ -+**Check agent status:** -+ -+.. code-block:: bash -+ -+ sudo keylime_tenant -c status -u agent-001 -+ -+**Delete an agent:** -+ -+.. code-block:: bash -+ -+ sudo keylime_tenant -c delete -u agent-001 -+ -+**List all agents:** -+ -+.. code-block:: bash -+ -+ sudo keylime_tenant -c cvlist -+ -+**Add a runtime policy:** -+ -+.. code-block:: bash -+ -+ sudo keylime_tenant -c addruntimepolicy --runtime-policy-name my-policy --runtime-policy /path/to/policy.json -+ -+**Add a measured boot policy:** -+ -+.. code-block:: bash -+ -+ sudo keylime_tenant -c addmbpolicy --mb-policy-name my-mb-policy --mb-policy /path/to/mb-policy.json -+ -+**Provision agent with certificate delivery:** -+ -+.. code-block:: bash -+ -+ sudo keylime_tenant -c add -t 192.168.1.100 -u agent-001 --cert default -+ -+**Provision agent with custom verifier:** -+ -+.. code-block:: bash -+ -+ sudo keylime_tenant -c add -t 192.168.1.100 -u agent-001 -v 192.168.1.200 -vp 8881 -+ -+FILES -+===== -+ -+/etc/keylime/tenant.conf -+ Default configuration file for keylime_tenant. Contains all tenant related settings. -+ -+ -+PREREQUISITES -+============= -+ -+- Keylime verifier service running (default: 127.0.0.1:8881) -+- Keylime registrar service running (default: 127.0.0.1:8891) -+- Root privileges (use sudo) -+- Network connectivity to registrar and verifier services -+- Valid TLS configuration in /etc/keylime/tenant.conf -+ -+SEE ALSO -+======== -+ -+**keylime_verifier**\(8), **keylime_registrar**\(8), **keylime_agent**\(8) -+ -+For more information about Keylime, visit: https://keylime.dev -+ -+BUGS -+==== -+ -+Report bugs to the Keylime project at: https://github.com/keylime/keylime/issues -diff --git a/docs/man/keylime_verifier.8.rst b/docs/man/keylime_verifier.8.rst -new file mode 100644 -index 0000000..fd7cfb9 ---- /dev/null -+++ b/docs/man/keylime_verifier.8.rst -@@ -0,0 +1,116 @@ -+================ -+keylime_verifier -+================ -+ -+---------------------------------------------- -+Keylime verifier service for agent attestation -+---------------------------------------------- -+ -+:Manual section: 8 -+:Author: Keylime Developers -+:Date: September 2025 -+ -+SYNOPSIS -+======== -+ -+**keylime_verifier** -+ -+(Most operations require root privileges, use with sudo) -+ -+DESCRIPTION -+=========== -+ -+The verifier is a long-running service that attests registered agents. It accesses -+the registrar database to obtain agent data, and optionally performs measured boot evaluation and durable -+attestation. The service does not accept command-line options; its behavior is configured via -+configuration files and environment variables, and it is managed by keylime tenant. -+ -+CONFIGURATION -+============= -+ -+Primary configuration is read from ``/etc/keylime/verifier.conf`` (or an override via env). -+All options are under the ``[verifier]`` section. -+ -+Essentials: -+- **uuid**: Unique identifier for this verifier instance -+- **ip**, **port**: Bind address and HTTP port -+- **registrar_ip**, **registrar_port**: Registrar endpoint -+- **enable_agent_mtls**: Enable mTLS with agents and tenant -+- **tls_dir**: TLS material location -+ -+ - ``generate``: auto-generate CA, client and server keys/certs under ``$KEYLIME_DIR/cv_ca`` -+ - ``default``: use existing materials under ``$KEYLIME_DIR/cv_ca`` -+ -+- **server_key**, **server_key_password**, **server_cert**: Server TLS files -+- **client_key**, **client_key_password**, **client_cert**: Client TLS files -+- **trusted_client_ca**, **trusted_server_ca**: CA lists -+- **database_url**: SQLAlchemy URL; value ``sqlite`` maps to ``$KEYLIME_DIR/cv_data.sqlite`` -+- **database_pool_sz_ovfl**: Pool size, overflow (non-sqlite) -+- **auto_migrate_db**: Apply DB migrations on startup -+- **num_workers**: Number of worker processes (``0`` = CPU count) -+- **exponential_backoff**, **retry_interval**, **max_retries**: Retry behavior for agent comm -+- **quote_interval**: Time between integrity checks (seconds) -+- **max_upload_size**: Upload size limit (bytes) -+- **request_timeout**: Agent request timeout (seconds) -+- **measured_boot_policy_name**, **measured_boot_imports**, **measured_boot_evaluate**: measured boot policy settings -+- **severity_labels**, **severity_policy**: revocation severity config -+- **ignore_tomtou_errors**: handle ToMToU IMA entries (bool) -+- **durable_attestation_import** and related **persistent_store_url**, **transparency_log_url**, -+ **time_stamp_authority_url**, **time_stamp_authority_certs_path**, **persistent_store_format**, -+ **persistent_store_encoding**, **transparency_log_sign_algo**, **signed_attributes**: durable attestation -+- **require_allow_list_signatures**: require signed allowlists (bool) -+ -+ENVIRONMENT -+=========== -+ -+- **KEYLIME_VERIFIER_CONFIG**: Path to verifier.conf (highest priority) -+- **KEYLIME_LOGGING_CONFIG**: Path to logging.conf -+- **KEYLIME_DIR**: Working directory (default: ``/var/lib/keylime``) -+- **KEYLIME_TEST**: ``on/true/1`` enables testing mode (looser checks; WORK_DIR becomes CWD) -+ -+FILES -+===== -+ -+- ``/etc/keylime/verifier.conf`` -+- ``/etc/keylime/logging.conf`` -+- ``$KEYLIME_DIR/cv_data.sqlite`` (when ``database_url = sqlite``) -+- ``$KEYLIME_DIR/cv_ca`` (when ``tls_dir = default`` or ``generate``) -+- systemd unit: ``keylime_verifier.service`` -+ -+RUNTIME -+======= -+ -+Start from system install: -+ -+.. code-block:: bash -+ -+ sudo keylime_verifier -+ -+Start as a systemd service: -+ -+.. code-block:: bash -+ -+ systemctl enable --now keylime_verifier -+ -+Open firewall ports (adjust if you changed ports): -+ -+.. code-block:: bash -+ -+ firewall-cmd --add-port 8881/tcp -+ firewall-cmd --runtime-to-permanent -+ -+NOTES -+===== -+ -+- Verifier initializes measured boot components on startup. -+- With ``tls_dir = generate``, the verifier creates CA/keys/certs in ``$KEYLIME_DIR/cv_ca`` used by other components. -+ -+SEE ALSO -+======== -+ -+**keylime_registrar**\(8), **keylime_tenant**\(1), **keylime_agent**\(8) -+ -+BUGS -+==== -+ -+Report bugs at https://github.com/keylime/keylime/issues --- -2.51.0 - diff --git a/keylime.spec b/keylime.spec index e455b19..5ef2255 100644 --- a/keylime.spec +++ b/keylime.spec @@ -1,5 +1,5 @@ %global srcname keylime -%global policy_version 42.1.2 +%global policy_version 43.1.1 # Package is actually noarch, but it has an optional dependency that is # arch-specific. @@ -8,8 +8,8 @@ %global selinuxtype targeted Name: keylime -Version: 7.12.1 -Release: 14%{?dist} +Version: 7.14.1 +Release: 1%{?dist} Summary: Open source TPM software for Bootstrapping and Maintaining Trust URL: https://github.com/keylime/keylime @@ -19,51 +19,8 @@ Source1: https://github.com/RedHat-SP-Security/%{name}-selinux/archive/v% Source2: %{srcname}.sysusers Source3: %{srcname}.tmpfiles -# Backported from https://github.com/keylime/keylime/pull/1782 -# Fixes DB connections leaks (https://issues.redhat.com/browse/RHEL-102995) -Patch: keylime-fix-db-connection-leaks.patch - -# Backported from https://github.com/keylime/keylime/pull/1791 -Patch: 0002-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch -Patch: 0003-mb-support-vendor_db-as-logged-by-newer-shim-version.patch - -# Backported from https://github.com/keylime/keylime/pull/1784 -# and https://github.com/keylime/keylime/pull/1785 -Patch: 0004-verifier-Gracefully-shutdown-on-signal.patch -Patch: 0005-revocations-Try-to-send-notifications-on-shutdown.patch -Patch: 0006-requests_client-close-the-session-at-the-end-of-the-.patch - -# Backported from https://github.com/keylime/keylime/pull/1736, -# https://github.com/keylime/keylime/commit/11c6b7f and -# https://github.com/keylime/keylime/commit/dd63459 -Patch: 0007-tests-change-test_mba_parsing-to-not-need-keylime-in.patch -Patch: 0008-tests-skip-measured-boot-related-tests-for-s390x-and.patch -Patch: 0009-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch - -# Backported from https://github.com/keylime/keylime/pull/1793 -Patch: 0010-mba-normalize-vendor_db-in-EV_EFI_VARIABLE_AUTHORITY.patch - -# Backported from https://github.com/keylime/keylime/pull/1794 -Patch: 0011-fix-malformed-certs-workaround.patch -# Backported from https://github.com/keylime/keylime/pull/1795 -Patch: 0012-keylime-policy-avoid-opening-dev-stdout.patch - -# Backported from: -# - https://github.com/keylime/keylime/pull/1746 -# - https://github.com/keylime/keylime/pull/1803 -# - https://github.com/keylime/keylime/pull/1808 -# ECC attestation support. -Patch: 0013-algorithms-add-support-for-specific-ECC-curve-algori.patch -Patch: 0014-algorithms-add-support-for-specific-RSA-algorithms.patch -Patch: 0015-tpm_util-fix-quote-signature-extraction-for-ECDSA.patch -Patch: 0016-tpm-fix-ECC-P-521-coordinate-validation.patch -Patch: 0017-tpm-fix-ECC-P-521-credential-activation-with-consist.patch -Patch: 0018-tpm-fix-ECC-signature-parsing-to-support-variable-le.patch - -# Backported from: -# - https://github.com/keylime/keylime/pull/1802 -# - https://github.com/keylime/keylime/pull/1813 -Patch: 0019-Added-manpages-for-keylime-services-and-the-tenant.patch +Patch: 0001-Fix-timestamp-conversion-to-use-UTC-timezone.patch +Patch: 0002-Fix-efivar-availability-check-in-test_create_mb_poli.patch # Main program: Apache-2.0 # Icons: MIT @@ -76,9 +33,11 @@ BuildRequires: python3-devel BuildRequires: python3-dbus BuildRequires: python3-jinja2 BuildRequires: python3-cryptography +BuildRequires: python3-docutils BuildRequires: python3-gpg BuildRequires: python3-pyasn1 BuildRequires: python3-pyasn1-modules +BuildRequires: python3-requests BuildRequires: python3-tornado BuildRequires: python3-sqlalchemy BuildRequires: python3-lark @@ -90,7 +49,6 @@ BuildRequires: systemd-rpm-macros BuildRequires: rpm-sign BuildRequires: createrepo_c BuildRequires: tpm2-tools -BuildRequires: python3-docutils Requires: python3-%{srcname} = %{version}-%{release} Requires: %{srcname}-base = %{version}-%{release} @@ -261,7 +219,7 @@ bzip2 -9 %{srcname}.pp mkdir -p manpages rst2man --syntax-highlight=none docs/man/keylime_tenant.1.rst manpages/keylime_tenant.1 -rst2man --syntax-highlight=none docs/man/keylime-policy.1.rst manpages/keylime-policy.1 +rst2man --syntax-highlight=none docs/man/keylime-policy.1.rst manpages/keylime-policy.1 rst2man --syntax-highlight=none docs/man/keylime_registrar.8.rst manpages/keylime_registrar.8 rst2man --syntax-highlight=none docs/man/keylime_verifier.8.rst manpages/keylime_verifier.8 @@ -286,8 +244,10 @@ done # Ship the ek-openssl-verify script. mkdir -p %{buildroot}/%{_datadir}/%{srcname}/scripts -install -Dpm 755 scripts/ek-openssl-verify \ - %{buildroot}/%{_datadir}/%{srcname}/scripts/ek-openssl-verify +for s in ek-openssl-verify keylime_oneshot_attestation; do + install -Dpm 755 scripts/"${s}" \ + %{buildroot}/%{_datadir}/%{srcname}/scripts/"${s}" +done # Ship configuration templates. cp -r ./templates %{buildroot}%{_datadir}/%{srcname}/templates/ @@ -493,6 +453,7 @@ fi %{_tmpfilesdir}/%{srcname}.conf %{_sysusersdir}/%{srcname}.conf %{_datadir}/%{srcname}/scripts/ek-openssl-verify +%{_datadir}/%{srcname}/scripts/keylime_oneshot_attestation %{_datadir}/%{srcname}/templates %{_bindir}/keylime_upgrade_config diff --git a/sources b/sources index 9b71be8..9e0d7e5 100644 --- a/sources +++ b/sources @@ -1,2 +1,2 @@ -SHA512 (v7.12.1.tar.gz) = c1297ebfc659102d73283255cfda4a977dfbff9bdd3748e05de405dadb70f752ad39aa5848edda9143d8ec620d07c21f1551fa4a914c99397620ab1682e58458 -SHA512 (keylime-selinux-42.1.2.tar.gz) = cb7b7b10d1d81af628a7ffdadc1be5af6d75851a44f58cff04edc575cbba1613447e56bfa1fb86660ec7c15e5fcf16ba51f2984094550ba3e08f8095b800b741 +SHA512 (v7.14.1.tar.gz) = d94cd1e25ec31e43fea05d0c404dd25c05b6b28435db2f8ca34546f6ff8bfd5da12d2dcd3b5cf4772c44688ae8968468dc2470da23596714e7615dbf6dfbe841 +SHA512 (keylime-selinux-43.1.1.tar.gz) = 1b0a850f68321e4872bb01eb99f5b000f1b5cbe3f1882e781bff519868ba5f4ca50f25b328b3662895969833add5c30d00e2a2361d2d626e7cffd95c0243ec39