From 1bbfd75e553d56eb4f812e8fae24ee65a5a7b305 Mon Sep 17 00:00:00 2001 From: eabdullin Date: Fri, 14 Nov 2025 14:06:37 +0000 Subject: [PATCH] import CS keylime-7.12.1-11.el10.2 --- .gitignore | 4 +- .keylime.metadata | 2 - ...EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch | 4 +- ...r_db-as-logged-by-newer-shim-version.patch | 8 +- ...rifier-Gracefully-shutdown-on-signal.patch | 6 +- ...ry-to-send-notifications-on-shutdown.patch | 4 +- ...close-the-session-at-the-end-of-the-.patch | 4 +- ...t_mba_parsing-to-not-need-keylime-in.patch | 91 ++ ...red-boot-related-tests-for-s390x-and.patch | 21 +- ...epo-tests-from-create-runtime-policy.patch | 6 +- ...ndor_db-in-EV_EFI_VARIABLE_AUTHORITY.patch | 281 ++++ 0011-fix-malformed-certs-workaround.patch | 1304 +++++++++++++++++ ...lime-policy-avoid-opening-dev-stdout.patch | 37 + ...e-keylime-compatible-with-python-3.9.patch | 628 -------- ...ate-str_to_version-in-the-adjust-scr.patch | 52 - ...HEL-9-version-of-create_allowlist.sh.patch | 404 ----- ...erver_key_password-for-verifier-regi.patch | 66 - ...h => keylime-fix-db-connection-leaks.patch | 0 SPECS/keylime.spec => keylime.spec | 512 ++++--- SOURCES/keylime.sysusers => keylime.sysusers | 0 SOURCES/keylime.tmpfiles => keylime.tmpfiles | 0 sources | 2 + 22 files changed, 2049 insertions(+), 1387 deletions(-) delete mode 100644 .keylime.metadata rename SOURCES/0008-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch => 0002-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch (90%) rename SOURCES/0009-mb-support-vendor_db-as-logged-by-newer-shim-version.patch => 0003-mb-support-vendor_db-as-logged-by-newer-shim-version.patch (98%) rename SOURCES/0010-verifier-Gracefully-shutdown-on-signal.patch => 0004-verifier-Gracefully-shutdown-on-signal.patch (88%) rename SOURCES/0011-revocations-Try-to-send-notifications-on-shutdown.patch => 0005-revocations-Try-to-send-notifications-on-shutdown.patch (98%) rename SOURCES/0012-requests_client-close-the-session-at-the-end-of-the-.patch => 0006-requests_client-close-the-session-at-the-end-of-the-.patch (91%) create mode 100644 0007-tests-change-test_mba_parsing-to-not-need-keylime-in.patch rename SOURCES/0003-tests-skip-measured-boot-related-tests-for-s390x-and.patch => 0008-tests-skip-measured-boot-related-tests-for-s390x-and.patch (74%) rename SOURCES/0002-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch => 0009-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch (94%) create mode 100644 0010-mba-normalize-vendor_db-in-EV_EFI_VARIABLE_AUTHORITY.patch create mode 100644 0011-fix-malformed-certs-workaround.patch create mode 100644 0012-keylime-policy-avoid-opening-dev-stdout.patch delete mode 100644 SOURCES/0001-Make-keylime-compatible-with-python-3.9.patch delete mode 100644 SOURCES/0004-templates-duplicate-str_to_version-in-the-adjust-scr.patch delete mode 100644 SOURCES/0005-Restore-RHEL-9-version-of-create_allowlist.sh.patch delete mode 100644 SOURCES/0006-Revert-default-server_key_password-for-verifier-regi.patch rename SOURCES/0007-fix_db_connection_leaks.patch => keylime-fix-db-connection-leaks.patch (100%) rename SPECS/keylime.spec => keylime.spec (51%) rename SOURCES/keylime.sysusers => keylime.sysusers (100%) rename SOURCES/keylime.tmpfiles => keylime.tmpfiles (100%) create mode 100644 sources diff --git a/.gitignore b/.gitignore index aaf9c11..50bf933 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -SOURCES/keylime-selinux-42.1.2.tar.gz -SOURCES/v7.12.1.tar.gz +keylime-selinux-42.1.2.tar.gz +v7.12.1.tar.gz diff --git a/.keylime.metadata b/.keylime.metadata deleted file mode 100644 index 8274479..0000000 --- a/.keylime.metadata +++ /dev/null @@ -1,2 +0,0 @@ -36672155770ce6690e59d97764072f9629af716d SOURCES/keylime-selinux-42.1.2.tar.gz -3db2aa10ee0a005bf5d0a1214cd08e2604da0429 SOURCES/v7.12.1.tar.gz diff --git a/SOURCES/0008-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch b/0002-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch similarity index 90% rename from SOURCES/0008-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch rename to 0002-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch index 80fdde9..e6f5bef 100644 --- a/SOURCES/0008-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch +++ b/0002-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch @@ -1,7 +1,7 @@ -From d14e0a132cfedd081bffa7a990b9401d5e257cac Mon Sep 17 00:00:00 2001 +From 52944972182639a625599e29ebe65b91714a3a41 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Fri, 8 Aug 2025 16:40:01 +0100 -Subject: [PATCH 8/9] mb: support EV_EFI_HANDOFF_TABLES events on PCR1 +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 diff --git a/SOURCES/0009-mb-support-vendor_db-as-logged-by-newer-shim-version.patch b/0003-mb-support-vendor_db-as-logged-by-newer-shim-version.patch similarity index 98% rename from SOURCES/0009-mb-support-vendor_db-as-logged-by-newer-shim-version.patch rename to 0003-mb-support-vendor_db-as-logged-by-newer-shim-version.patch index bcc024e..c079994 100644 --- a/SOURCES/0009-mb-support-vendor_db-as-logged-by-newer-shim-version.patch +++ b/0003-mb-support-vendor_db-as-logged-by-newer-shim-version.patch @@ -1,7 +1,7 @@ -From 607b97ac8d414cb57b1ca89925631d41bd7ac04c Mon Sep 17 00:00:00 2001 +From 34bd283113f13c251114507315c647975beede2f Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Fri, 8 Aug 2025 16:41:54 +0100 -Subject: [PATCH 9/9] mb: support vendor_db as logged by newer shim versions +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: @@ -199,10 +199,10 @@ index 23cafb9..c98e61d 100755 **get_kernel(events, has_secureboot), } diff --git a/test/test_create_mb_policy.py b/test/test_create_mb_policy.py -index b00d8e7..cd32bda 100644 +index eaed0e3..aa7a4b9 100644 --- a/test/test_create_mb_policy.py +++ b/test/test_create_mb_policy.py -@@ -364,6 +364,148 @@ class CreateMeasuredBootPolicy_Test(unittest.TestCase): +@@ -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"]) diff --git a/SOURCES/0010-verifier-Gracefully-shutdown-on-signal.patch b/0004-verifier-Gracefully-shutdown-on-signal.patch similarity index 88% rename from SOURCES/0010-verifier-Gracefully-shutdown-on-signal.patch rename to 0004-verifier-Gracefully-shutdown-on-signal.patch index df7bb23..39f6327 100644 --- a/SOURCES/0010-verifier-Gracefully-shutdown-on-signal.patch +++ b/0004-verifier-Gracefully-shutdown-on-signal.patch @@ -1,7 +1,7 @@ -From 1b7191098ca3f6d72c6ad218564ae0938a87efd4 Mon Sep 17 00:00:00 2001 +From c530c332321c1daffa5bfcd08754179012dd21cc Mon Sep 17 00:00:00 2001 From: Anderson Toshiyuki Sasaki -Date: Mon, 18 Aug 2025 12:22:55 +0000 -Subject: [PATCH 10/13] verifier: Gracefully shutdown on signal +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. diff --git a/SOURCES/0011-revocations-Try-to-send-notifications-on-shutdown.patch b/0005-revocations-Try-to-send-notifications-on-shutdown.patch similarity index 98% rename from SOURCES/0011-revocations-Try-to-send-notifications-on-shutdown.patch rename to 0005-revocations-Try-to-send-notifications-on-shutdown.patch index b36836e..dea95ca 100644 --- a/SOURCES/0011-revocations-Try-to-send-notifications-on-shutdown.patch +++ b/0005-revocations-Try-to-send-notifications-on-shutdown.patch @@ -1,7 +1,7 @@ -From af9ac50f5acf1a7d4ad285956b60e60c3c4416b7 Mon Sep 17 00:00:00 2001 +From 565889ab6c90823a5096e39a58e9599fa49072f6 Mon Sep 17 00:00:00 2001 From: Anderson Toshiyuki Sasaki Date: Wed, 23 Jul 2025 15:39:49 +0200 -Subject: [PATCH 11/13] revocations: Try to send notifications on shutdown +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 diff --git a/SOURCES/0012-requests_client-close-the-session-at-the-end-of-the-.patch b/0006-requests_client-close-the-session-at-the-end-of-the-.patch similarity index 91% rename from SOURCES/0012-requests_client-close-the-session-at-the-end-of-the-.patch rename to 0006-requests_client-close-the-session-at-the-end-of-the-.patch index ba24d60..7fb869a 100644 --- a/SOURCES/0012-requests_client-close-the-session-at-the-end-of-the-.patch +++ b/0006-requests_client-close-the-session-at-the-end-of-the-.patch @@ -1,7 +1,7 @@ -From 5fb4484b07a7ba3fcdf451bf816b5f07a40d6d97 Mon Sep 17 00:00:00 2001 +From e6fb5090df3e35c7d44bc8f7f37d420d7ee8a05c Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Wed, 4 Jun 2025 19:52:37 +0100 -Subject: [PATCH 12/13] requests_client: close the session at the end of the +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 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 new file mode 100644 index 0000000..3937579 --- /dev/null +++ b/0007-tests-change-test_mba_parsing-to-not-need-keylime-in.patch @@ -0,0 +1,91 @@ +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/SOURCES/0003-tests-skip-measured-boot-related-tests-for-s390x-and.patch b/0008-tests-skip-measured-boot-related-tests-for-s390x-and.patch similarity index 74% rename from SOURCES/0003-tests-skip-measured-boot-related-tests-for-s390x-and.patch rename to 0008-tests-skip-measured-boot-related-tests-for-s390x-and.patch index 8cf9b37..14e7247 100644 --- a/SOURCES/0003-tests-skip-measured-boot-related-tests-for-s390x-and.patch +++ b/0008-tests-skip-measured-boot-related-tests-for-s390x-and.patch @@ -1,7 +1,7 @@ -From 4e7cd6b75de27897ecc8e7329732cd945f7adfd0 Mon Sep 17 00:00:00 2001 +From 1496567e4b06f7a8eff9f758ea2e4e00ffa89f9b Mon Sep 17 00:00:00 2001 From: Sergio Correia -Date: Thu, 22 May 2025 18:27:04 +0100 -Subject: [PATCH 3/6] tests: skip measured-boot related tests for s390x and +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 @@ -11,7 +11,7 @@ Signed-off-by: Sergio Correia 2 files changed, 4 insertions(+) diff --git a/test/test_create_mb_policy.py b/test/test_create_mb_policy.py -index eaed0e3..b00d8e7 100644 +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. @@ -31,16 +31,17 @@ index eaed0e3..b00d8e7 100644 def test_event_to_sha256(self): test_cases = [ diff --git a/test/test_mba_parsing.py b/test/test_mba_parsing.py -index 670a602..e157116 100644 +index 4ee4e3b..82e6086 100644 --- a/test/test_mba_parsing.py +++ b/test/test_mba_parsing.py -@@ -1,10 +1,12 @@ +@@ -1,4 +1,5 @@ import os +import platform + import tempfile import unittest - - from keylime.common.algorithms import Hash - from keylime.mba import mba + 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") @@ -48,5 +49,5 @@ index 670a602..e157116 100644 def test_parse_bootlog(self): """Test parsing binary measured boot event log""" -- -2.47.1 +2.47.3 diff --git a/SOURCES/0002-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch b/0009-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch similarity index 94% rename from SOURCES/0002-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch rename to 0009-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch index 5735f6c..9643ec5 100644 --- a/SOURCES/0002-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch +++ b/0009-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch @@ -1,7 +1,7 @@ -From 5c5c7f7f7180111485b24061af4c0395476958b5 Mon Sep 17 00:00:00 2001 +From be968fd54198042d2014ad63368b78e9d4609169 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Thu, 22 May 2025 11:25:15 -0400 -Subject: [PATCH 2/6] tests: fix rpm repo tests from create-runtime-policy +Subject: [PATCH 09/10] tests: fix rpm repo tests from create-runtime-policy Signed-off-by: Sergio Correia --- @@ -54,5 +54,5 @@ index 708438c..b62729b 100755 } -- -2.47.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 new file mode 100644 index 0000000..59cf28c --- /dev/null +++ b/0010-mba-normalize-vendor_db-in-EV_EFI_VARIABLE_AUTHORITY.patch @@ -0,0 +1,281 @@ +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 new file mode 100644 index 0000000..1891260 --- /dev/null +++ b/0011-fix-malformed-certs-workaround.patch @@ -0,0 +1,1304 @@ +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 new file mode 100644 index 0000000..eea629d --- /dev/null +++ b/0012-keylime-policy-avoid-opening-dev-stdout.patch @@ -0,0 +1,37 @@ +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/SOURCES/0001-Make-keylime-compatible-with-python-3.9.patch b/SOURCES/0001-Make-keylime-compatible-with-python-3.9.patch deleted file mode 100644 index 7239692..0000000 --- a/SOURCES/0001-Make-keylime-compatible-with-python-3.9.patch +++ /dev/null @@ -1,628 +0,0 @@ -From f7c32aec9c44a176124d982d942391ed3d50e846 Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Tue, 3 Jun 2025 21:23:09 +0100 -Subject: [PATCH 1/6] Make keylime compatible with python 3.9 - -Signed-off-by: Sergio Correia ---- - keylime/ima/types.py | 33 ++++---- - keylime/models/base/basic_model.py | 4 +- - keylime/models/base/basic_model_meta.py | 4 +- - keylime/models/base/field.py | 4 +- - keylime/models/base/persistable_model.py | 4 +- - keylime/models/base/type.py | 4 +- - keylime/models/base/types/base64_bytes.py | 4 +- - keylime/models/base/types/certificate.py | 92 +++++++++++---------- - keylime/models/base/types/dictionary.py | 4 +- - keylime/models/base/types/one_of.py | 6 +- - keylime/models/registrar/registrar_agent.py | 31 +++---- - keylime/policy/create_runtime_policy.py | 2 +- - keylime/registrar_client.py | 8 +- - keylime/web/base/action_handler.py | 7 +- - keylime/web/base/controller.py | 78 ++++++++--------- - tox.ini | 10 +++ - 16 files changed, 154 insertions(+), 141 deletions(-) - -diff --git a/keylime/ima/types.py b/keylime/ima/types.py -index 99f0aa7..a0fffdf 100644 ---- a/keylime/ima/types.py -+++ b/keylime/ima/types.py -@@ -6,11 +6,6 @@ if sys.version_info >= (3, 8): - else: - from typing_extensions import Literal, TypedDict - --if sys.version_info >= (3, 11): -- from typing import NotRequired, Required --else: -- from typing_extensions import NotRequired, Required -- - ### Types for tpm_dm.py - - RuleAttributeType = Optional[Union[int, str, bool]] -@@ -51,7 +46,7 @@ class Rule(TypedDict): - - - class Policies(TypedDict): -- version: Required[int] -+ version: int - match_on: MatchKeyType - rules: Dict[str, Rule] - -@@ -60,27 +55,27 @@ class Policies(TypedDict): - - - class RPMetaType(TypedDict): -- version: Required[int] -- generator: NotRequired[int] -- timestamp: NotRequired[str] -+ version: int -+ generator: int -+ timestamp: str - - - class RPImaType(TypedDict): -- ignored_keyrings: Required[List[str]] -- log_hash_alg: Required[Literal["sha1", "sha256", "sha384", "sha512"]] -+ ignored_keyrings: List[str] -+ log_hash_alg: Literal["sha1", "sha256", "sha384", "sha512"] - dm_policy: Optional[Policies] - - - RuntimePolicyType = TypedDict( - "RuntimePolicyType", - { -- "meta": Required[RPMetaType], -- "release": NotRequired[int], -- "digests": Required[Dict[str, List[str]]], -- "excludes": Required[List[str]], -- "keyrings": Required[Dict[str, List[str]]], -- "ima": Required[RPImaType], -- "ima-buf": Required[Dict[str, List[str]]], -- "verification-keys": Required[str], -+ "meta": RPMetaType, -+ "release": int, -+ "digests": Dict[str, List[str]], -+ "excludes": List[str], -+ "keyrings": Dict[str, List[str]], -+ "ima": RPImaType, -+ "ima-buf": Dict[str, List[str]], -+ "verification-keys": str, - }, - ) -diff --git a/keylime/models/base/basic_model.py b/keylime/models/base/basic_model.py -index 68a126e..6f5de83 100644 ---- a/keylime/models/base/basic_model.py -+++ b/keylime/models/base/basic_model.py -@@ -407,7 +407,9 @@ class BasicModel(ABC, metaclass=BasicModelMeta): - if max and length > max: - self._add_error(field, msg or f"should be at most {length} {element_type}(s)") - -- def validate_number(self, field: str, *expressions: tuple[str, int | float], msg: Optional[str] = None) -> None: -+ def validate_number( -+ self, field: str, *expressions: tuple[str, Union[int, float]], msg: Optional[str] = None -+ ) -> None: - value = self.values.get(field) - - if not value: -diff --git a/keylime/models/base/basic_model_meta.py b/keylime/models/base/basic_model_meta.py -index 353e004..84617d4 100644 ---- a/keylime/models/base/basic_model_meta.py -+++ b/keylime/models/base/basic_model_meta.py -@@ -1,6 +1,6 @@ - from abc import ABCMeta - from types import MappingProxyType --from typing import Any, Callable, Mapping, TypeAlias, Union -+from typing import Any, Callable, Mapping, Union - - from sqlalchemy.types import TypeEngine - -@@ -40,7 +40,7 @@ class BasicModelMeta(ABCMeta): - - # pylint: disable=bad-staticmethod-argument, no-value-for-parameter, using-constant-test - -- DeclaredFieldType: TypeAlias = Union[ModelType, TypeEngine, type[ModelType], type[TypeEngine]] -+ DeclaredFieldType = Union[ModelType, TypeEngine, type[ModelType], type[TypeEngine]] - - @classmethod - def _is_model_class(mcs, cls: type) -> bool: # type: ignore[reportSelfClassParameterName] -diff --git a/keylime/models/base/field.py b/keylime/models/base/field.py -index 7fb3dcb..d1e3bc3 100644 ---- a/keylime/models/base/field.py -+++ b/keylime/models/base/field.py -@@ -1,6 +1,6 @@ - import re - from inspect import isclass --from typing import TYPE_CHECKING, Any, Optional, TypeAlias, Union -+from typing import TYPE_CHECKING, Any, Optional, Union - - from sqlalchemy.types import TypeEngine - -@@ -23,7 +23,7 @@ class ModelField: - [2] https://docs.python.org/3/library/functions.html#property - """ - -- DeclaredFieldType: TypeAlias = Union[ModelType, TypeEngine, type[ModelType], type[TypeEngine]] -+ DeclaredFieldType = Union[ModelType, TypeEngine, type[ModelType], type[TypeEngine]] - - FIELD_NAME_REGEX = re.compile(r"^[A-Za-z_]+[A-Za-z0-9_]*$") - -diff --git a/keylime/models/base/persistable_model.py b/keylime/models/base/persistable_model.py -index 18f7d0d..015d661 100644 ---- a/keylime/models/base/persistable_model.py -+++ b/keylime/models/base/persistable_model.py -@@ -1,4 +1,4 @@ --from typing import Any, Mapping, Optional, Sequence -+from typing import Any, Mapping, Optional, Sequence, Union - - from keylime.models.base.basic_model import BasicModel - from keylime.models.base.db import db_manager -@@ -165,7 +165,7 @@ class PersistableModel(BasicModel, metaclass=PersistableModelMeta): - else: - return None - -- def __init__(self, data: Optional[dict | object] = None, process_associations: bool = True) -> None: -+ def __init__(self, data: Optional[Union[dict, object]] = None, process_associations: bool = True) -> None: - if isinstance(data, type(self).db_mapping): - super().__init__({}, process_associations) - self._init_from_mapping(data, process_associations) -diff --git a/keylime/models/base/type.py b/keylime/models/base/type.py -index 2520f72..e4d924c 100644 ---- a/keylime/models/base/type.py -+++ b/keylime/models/base/type.py -@@ -1,7 +1,7 @@ - from decimal import Decimal - from inspect import isclass - from numbers import Real --from typing import Any, TypeAlias, Union -+from typing import Any, Union - - from sqlalchemy.engine.interfaces import Dialect - from sqlalchemy.types import TypeEngine -@@ -99,7 +99,7 @@ class ModelType: - you should instead set ``_type_engine`` to ``None`` and override the ``get_db_type`` method. - """ - -- DeclaredTypeEngine: TypeAlias = Union[TypeEngine, type[TypeEngine]] -+ DeclaredTypeEngine = Union[TypeEngine, type[TypeEngine]] - - def __init__(self, type_engine: DeclaredTypeEngine) -> None: - if isclass(type_engine) and issubclass(type_engine, TypeEngine): -diff --git a/keylime/models/base/types/base64_bytes.py b/keylime/models/base/types/base64_bytes.py -index b9b4b13..a1eeced 100644 ---- a/keylime/models/base/types/base64_bytes.py -+++ b/keylime/models/base/types/base64_bytes.py -@@ -1,6 +1,6 @@ - import base64 - import binascii --from typing import Optional, TypeAlias, Union -+from typing import Optional, Union - - from sqlalchemy.types import Text - -@@ -62,7 +62,7 @@ class Base64Bytes(ModelType): - b64_str = Base64Bytes().cast("MIIE...") - """ - -- IncomingValue: TypeAlias = Union[bytes, str, None] -+ IncomingValue = Union[bytes, str, None] - - def __init__(self) -> None: - super().__init__(Text) -diff --git a/keylime/models/base/types/certificate.py b/keylime/models/base/types/certificate.py -index 2c27603..0f03169 100644 ---- a/keylime/models/base/types/certificate.py -+++ b/keylime/models/base/types/certificate.py -@@ -1,7 +1,7 @@ - import base64 - import binascii - import io --from typing import Optional, TypeAlias, Union -+from typing import Optional, Union - - import cryptography.x509 - from cryptography.hazmat.primitives.serialization import Encoding -@@ -78,7 +78,7 @@ class Certificate(ModelType): - cert = Certificate().cast("-----BEGIN CERTIFICATE-----\nMIIE...") - """ - -- IncomingValue: TypeAlias = Union[cryptography.x509.Certificate, bytes, str, None] -+ IncomingValue = Union[cryptography.x509.Certificate, bytes, str, None] - - def __init__(self) -> None: - super().__init__(Text) -@@ -195,18 +195,19 @@ class Certificate(ModelType): - """ - - try: -- match self.infer_encoding(value): -- case "decoded": -- return None -- 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] -- case "base64": -- der_value = base64.b64decode(value, validate=True) # type: ignore[reportArgumentType, arg-type] -- cryptography.x509.load_der_x509_certificate(der_value) -- case _: -- raise Exception -+ encoding_inf = self.infer_encoding(value) -+ if encoding_inf == "decoded": -+ return None -+ -+ if encoding_inf == "der": -+ cryptography.x509.load_der_x509_certificate(value) # type: ignore[reportArgumentType, arg-type] -+ elif encoding_inf == "pem": -+ cryptography.x509.load_pem_x509_certificate(value) # type: ignore[reportArgumentType, arg-type] -+ elif encoding_inf == "base64": -+ der_value = base64.b64decode(value, validate=True) # type: ignore[reportArgumentType, arg-type] -+ cryptography.x509.load_der_x509_certificate(der_value) -+ else: -+ raise Exception - except Exception: - return False - -@@ -227,37 +228,38 @@ class Certificate(ModelType): - if not value: - return None - -- match self.infer_encoding(value): -- case "decoded": -- return value # type: ignore[reportReturnType, return-value] -- case "der": -- try: -- return self._load_der_cert(value) # type: ignore[reportArgumentType, arg-type] -- except PyAsn1Error as err: -- raise ValueError( -- f"value cast to certificate appears DER encoded but cannot be deserialized as such: {value!r}" -- ) from err -- case "pem": -- try: -- return self._load_pem_cert(value) # type: ignore[reportArgumentType, arg-type] -- except PyAsn1Error as err: -- raise ValueError( -- f"value cast to certificate appears PEM encoded but cannot be deserialized as such: " -- f"'{str(value)}'" -- ) from err -- case "base64": -- try: -- return self._load_der_cert(base64.b64decode(value, validate=True)) # type: ignore[reportArgumentType, arg-type] -- except (binascii.Error, PyAsn1Error) as err: -- raise ValueError( -- f"value cast to certificate appears Base64 encoded but cannot be deserialized as such: " -- f"'{str(value)}'" -- ) from err -- case _: -- raise TypeError( -- f"value cast to certificate is of type '{value.__class__.__name__}' but should be one of 'str', " -- f"'bytes' or 'cryptography.x509.Certificate': '{str(value)}'" -- ) -+ encoding_inf = self.infer_encoding(value) -+ if encoding_inf == "decoded": -+ return value # type: ignore[reportReturnType, return-value] -+ -+ if encoding_inf == "der": -+ try: -+ return self._load_der_cert(value) # type: ignore[reportArgumentType, arg-type] -+ except PyAsn1Error as err: -+ raise ValueError( -+ f"value cast to certificate appears DER encoded but cannot be deserialized as such: {value!r}" -+ ) from err -+ elif encoding_inf == "pem": -+ try: -+ return self._load_pem_cert(value) # type: ignore[reportArgumentType, arg-type] -+ except PyAsn1Error as err: -+ raise ValueError( -+ f"value cast to certificate appears PEM encoded but cannot be deserialized as such: " -+ f"'{str(value)}'" -+ ) from err -+ elif encoding_inf == "base64": -+ try: -+ return self._load_der_cert(base64.b64decode(value, validate=True)) # type: ignore[reportArgumentType, arg-type] -+ except (binascii.Error, PyAsn1Error) as err: -+ raise ValueError( -+ f"value cast to certificate appears Base64 encoded but cannot be deserialized as such: " -+ f"'{str(value)}'" -+ ) from err -+ else: -+ raise TypeError( -+ f"value cast to certificate is of type '{value.__class__.__name__}' but should be one of 'str', " -+ f"'bytes' or 'cryptography.x509.Certificate': '{str(value)}'" -+ ) - - def generate_error_msg(self, _value: IncomingValue) -> str: - return "must be a valid X.509 certificate in PEM format or otherwise encoded using Base64" -diff --git a/keylime/models/base/types/dictionary.py b/keylime/models/base/types/dictionary.py -index 7d9e811..d9ffec3 100644 ---- a/keylime/models/base/types/dictionary.py -+++ b/keylime/models/base/types/dictionary.py -@@ -1,5 +1,5 @@ - import json --from typing import Optional, TypeAlias, Union -+from typing import Optional, Union - - from sqlalchemy.types import Text - -@@ -50,7 +50,7 @@ class Dictionary(ModelType): - kv_pairs = Dictionary().cast('{"key": "value"}') - """ - -- IncomingValue: TypeAlias = Union[dict, str, None] -+ IncomingValue = Union[dict, str, None] - - def __init__(self) -> None: - super().__init__(Text) -diff --git a/keylime/models/base/types/one_of.py b/keylime/models/base/types/one_of.py -index 479d417..faf097d 100644 ---- a/keylime/models/base/types/one_of.py -+++ b/keylime/models/base/types/one_of.py -@@ -1,6 +1,6 @@ - from collections import Counter - from inspect import isclass --from typing import Any, Optional, TypeAlias, Union -+from typing import Any, Optional, Union - - from sqlalchemy.engine.interfaces import Dialect - from sqlalchemy.types import Float, Integer, String, TypeEngine -@@ -65,8 +65,8 @@ class OneOf(ModelType): - incoming PEM value would not be cast to a certificate object and remain a string. - """ - -- Declaration: TypeAlias = Union[str, int, float, ModelType, TypeEngine, type[ModelType], type[TypeEngine]] -- PermittedList: TypeAlias = list[Union[str, int, float, ModelType]] -+ Declaration = Union[str, int, float, ModelType, TypeEngine, type[ModelType], type[TypeEngine]] -+ PermittedList = list[Union[str, int, float, ModelType]] - - def __init__(self, *args: Declaration) -> None: - # pylint: disable=super-init-not-called -diff --git a/keylime/models/registrar/registrar_agent.py b/keylime/models/registrar/registrar_agent.py -index 560c188..b232049 100644 ---- a/keylime/models/registrar/registrar_agent.py -+++ b/keylime/models/registrar/registrar_agent.py -@@ -153,21 +153,22 @@ class RegistrarAgent(PersistableModel): - names = ", ".join(non_compliant_certs) - names = " and".join(names.rsplit(",", 1)) - -- match config.get("registrar", "malformed_cert_action"): -- case "ignore": -- return -- case "reject": -- logger.error( -- "Certificate(s) %s may not conform to strict ASN.1 DER encoding rules and were rejected due to " -- "config ('malformed_cert_action = reject')", -- names, -- ) -- case _: -- logger.warning( -- "Certificate(s) %s may not conform to strict ASN.1 DER encoding rules and were re-encoded before " -- "parsing by python-cryptography", -- names, -- ) -+ cfg = config.get("registrar", "malformed_cert_action") -+ if cfg == "ignore": -+ return -+ -+ if cfg == "reject": -+ logger.error( -+ "Certificate(s) %s may not conform to strict ASN.1 DER encoding rules and were rejected due to " -+ "config ('malformed_cert_action = reject')", -+ names, -+ ) -+ else: -+ logger.warning( -+ "Certificate(s) %s may not conform to strict ASN.1 DER encoding rules and were re-encoded before " -+ "parsing by python-cryptography", -+ names, -+ ) - - def _bind_ak_to_iak(self, iak_attest, iak_sign): - # The ak-iak binding should only be verified when either aik_tpm or iak_tpm is changed -diff --git a/keylime/policy/create_runtime_policy.py b/keylime/policy/create_runtime_policy.py -index 6a412c4..8e1c687 100644 ---- a/keylime/policy/create_runtime_policy.py -+++ b/keylime/policy/create_runtime_policy.py -@@ -972,7 +972,7 @@ def create_runtime_policy(args: argparse.Namespace) -> Optional[RuntimePolicyTyp - ) - abort = True - else: -- if a not in algorithms.Hash: -+ if a not in set(algorithms.Hash): - if a == SHA256_OR_SM3: - algo = a - else: -diff --git a/keylime/registrar_client.py b/keylime/registrar_client.py -index 705ff12..97fbc2a 100644 ---- a/keylime/registrar_client.py -+++ b/keylime/registrar_client.py -@@ -13,12 +13,6 @@ if sys.version_info >= (3, 8): - else: - from typing_extensions import TypedDict - --if sys.version_info >= (3, 11): -- from typing import NotRequired --else: -- from typing_extensions import NotRequired -- -- - class RegistrarData(TypedDict): - ip: Optional[str] - port: Optional[str] -@@ -27,7 +21,7 @@ class RegistrarData(TypedDict): - aik_tpm: str - ek_tpm: str - ekcert: Optional[str] -- provider_keys: NotRequired[Dict[str, str]] -+ provider_keys: Dict[str, str] - - - logger = keylime_logging.init_logging("registrar_client") -diff --git a/keylime/web/base/action_handler.py b/keylime/web/base/action_handler.py -index b20de89..e7b5888 100644 ---- a/keylime/web/base/action_handler.py -+++ b/keylime/web/base/action_handler.py -@@ -1,4 +1,5 @@ - import re -+import sys - import time - import traceback - from inspect import iscoroutinefunction -@@ -48,7 +49,11 @@ class ActionHandler(RequestHandler): - - # Take the list of strings returned by format_exception, where each string ends in a newline and may contain - # internal newlines, and split the concatenation of all the strings by newline -- message = "".join(traceback.format_exception(err)) -+ if sys.version_info < (3, 10): -+ message = "".join(traceback.format_exception(err, None, None)) -+ else: -+ message = "".join(traceback.format_exception(err)) -+ - lines = message.split("\n") - - for line in lines: -diff --git a/keylime/web/base/controller.py b/keylime/web/base/controller.py -index f1ac3c5..153535e 100644 ---- a/keylime/web/base/controller.py -+++ b/keylime/web/base/controller.py -@@ -2,7 +2,7 @@ import http.client - import json - import re - from types import MappingProxyType --from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence, TypeAlias, Union -+from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence, Union - - from tornado.escape import parse_qs_bytes - from tornado.httputil import parse_body_arguments -@@ -15,14 +15,16 @@ if TYPE_CHECKING: - from keylime.models.base.basic_model import BasicModel - from keylime.web.base.action_handler import ActionHandler - --PathParams: TypeAlias = Mapping[str, str] --QueryParams: TypeAlias = Mapping[str, str | Sequence[str]] --MultipartParams: TypeAlias = Mapping[str, Union[str, bytes, Sequence[str | bytes]]] --FormParams: TypeAlias = Union[QueryParams, MultipartParams] --JSONConvertible: TypeAlias = Union[str, int, float, bool, None, "JSONObjectConvertible", "JSONArrayConvertible"] --JSONObjectConvertible: TypeAlias = Mapping[str, JSONConvertible] --JSONArrayConvertible: TypeAlias = Sequence[JSONConvertible] # pyright: ignore[reportInvalidTypeForm] --Params: TypeAlias = Mapping[str, Union[str, bytes, Sequence[str | bytes], JSONObjectConvertible, JSONArrayConvertible]] -+PathParams = Mapping[str, str] -+QueryParams = Mapping[str, Union[str, Sequence[str]]] -+MultipartParams = Mapping[str, Union[str, bytes, Union[Sequence[str], Sequence[bytes]]]] -+FormParams = Union[QueryParams, MultipartParams] -+JSONConvertible = Union[str, int, float, bool, None, "JSONObjectConvertible", "JSONArrayConvertible"] -+JSONObjectConvertible = Mapping[str, JSONConvertible] -+JSONArrayConvertible = Sequence[JSONConvertible] # pyright: ignore[reportInvalidTypeForm] -+Params = Mapping[ -+ str, Union[str, bytes, Union[Sequence[str], Sequence[bytes]], JSONObjectConvertible, JSONArrayConvertible] -+] - - - class Controller: -@@ -77,7 +79,7 @@ class Controller: - VERSION_REGEX = re.compile("^\\/v(\\d+)(?:\\.(\\d+))*") - - @staticmethod -- def decode_url_query(query: str | bytes) -> QueryParams: -+ def decode_url_query(query: Union[str, bytes]) -> QueryParams: - """Parses a binary query string (whether from a URL or HTTP body) into a dict of Unicode strings. If multiple - instances of the same key are present in the string, their values are collected into a list. - -@@ -135,8 +137,8 @@ class Controller: - - @staticmethod - def prepare_http_body( -- body: Union[str, JSONObjectConvertible | JSONArrayConvertible, Any], content_type: Optional[str] = None -- ) -> tuple[Optional[bytes | Any], Optional[str]]: -+ body: Union[str, Union[JSONObjectConvertible, JSONArrayConvertible], Any], content_type: Optional[str] = None -+ ) -> tuple[Optional[Union[bytes, Any]], Optional[str]]: - """Prepares an object to be included in the body of an HTTP request or response and infers the appropriate - media type unless provided. ``body`` will be serialised into JSON if it contains a ``dict`` or ``list`` which is - serialisable unless a ``content_type`` other than ``"application/json"`` is provided. -@@ -155,32 +157,34 @@ class Controller: - if content_type: - content_type = content_type.lower().strip() - -- body_out: Optional[bytes | Any] -- content_type_out: Optional[str] -- -- match (body, content_type): -- case (None, _): -- body_out = None -- content_type_out = content_type -- case ("", _): -- body_out = b"" -- content_type_out = "text/plain; charset=utf-8" -- case (_, "text/plain"): -+ body_out: Optional[bytes | Any] = None -+ content_type_out: Optional[str] = None -+ -+ if body is None: -+ body_out = None -+ content_type_out = content_type -+ elif body == "": -+ body_out = b"" -+ content_type_out = "text/plain; charset=utf-8" -+ else: -+ if content_type == "text/plain": - body_out = str(body).encode("utf-8") - content_type_out = "text/plain; charset=utf-8" -- case (_, "application/json") if isinstance(body, str): -- body_out = body.encode("utf-8") -- content_type_out = "application/json" -- case (_, "application/json"): -- body_out = json.dumps(body, allow_nan=False, indent=4).encode("utf-8") -- content_type_out = "application/json" -- case (_, None) if isinstance(body, str): -- body_out = body.encode("utf-8") -- content_type_out = "text/plain; charset=utf-8" -- case (_, None) if isinstance(body, (dict, list)): -- body_out = json.dumps(body, allow_nan=False, indent=4).encode("utf-8") -- content_type_out = "application/json" -- case (_, _): -+ elif content_type == "application/json": -+ if isinstance(body, str): -+ body_out = body.encode("utf-8") -+ content_type_out = "application/json" -+ else: -+ body_out = json.dumps(body, allow_nan=False, indent=4).encode("utf-8") -+ content_type_out = "application/json" -+ elif content_type is None: -+ if isinstance(body, str): -+ body_out = body.encode("utf-8") -+ content_type_out = "text/plain; charset=utf-8" -+ elif isinstance(body, (dict, list)): -+ body_out = json.dumps(body, allow_nan=False, indent=4).encode("utf-8") -+ content_type_out = "application/json" -+ else: - body_out = body - content_type_out = content_type - -@@ -248,7 +252,7 @@ class Controller: - self, - code: int = 200, - status: Optional[str] = None, -- data: Optional[JSONObjectConvertible | JSONArrayConvertible] = None, -+ data: Optional[Union[JSONObjectConvertible, JSONArrayConvertible]] = None, - ) -> None: - """Converts a Python data structure to JSON and wraps it in the following boilerplate JSON object which is - returned by all v2 endpoints: -diff --git a/tox.ini b/tox.ini -index 031ac54..ce3974c 100644 ---- a/tox.ini -+++ b/tox.ini -@@ -51,3 +51,13 @@ commands = black --diff ./keylime ./test - deps = - isort - commands = isort --diff --check ./keylime ./test -+ -+ -+[testenv:pylint39] -+basepython = python3.9 -+deps = -+ -r{toxinidir}/requirements.txt -+ -r{toxinidir}/test-requirements.txt -+ pylint -+commands = bash scripts/check_codestyle.sh -+allowlist_externals = bash --- -2.47.1 - diff --git a/SOURCES/0004-templates-duplicate-str_to_version-in-the-adjust-scr.patch b/SOURCES/0004-templates-duplicate-str_to_version-in-the-adjust-scr.patch deleted file mode 100644 index 3432ee9..0000000 --- a/SOURCES/0004-templates-duplicate-str_to_version-in-the-adjust-scr.patch +++ /dev/null @@ -1,52 +0,0 @@ -From 7ca86e1c0d68f45915d9f583ffaf149285905005 Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Tue, 3 Jun 2025 10:50:48 +0100 -Subject: [PATCH 4/6] templates: duplicate str_to_version() in the adjust - script - -As a follow-up of upstream PR#1486, duplicate the str_to_version() -method in adjust.py so that we do not need the keylime modules in -order for the configuration upgrade script to run. - -Signed-off-by: Sergio Correia ---- - templates/2.0/adjust.py | 22 ++++++++++++++++++++-- - 1 file changed, 20 insertions(+), 2 deletions(-) - -diff --git a/templates/2.0/adjust.py b/templates/2.0/adjust.py -index 6008e4c..24ba898 100644 ---- a/templates/2.0/adjust.py -+++ b/templates/2.0/adjust.py -@@ -4,9 +4,27 @@ import logging - import re - from configparser import RawConfigParser - from logging import Logger --from typing import Dict, List, Optional, Tuple -+from typing import Dict, Tuple, Union - --from keylime.common.version import str_to_version -+ -+def str_to_version(v_str: str) -> Union[Tuple[int, int], None]: -+ """ -+ Validates the string format and converts the provided string to a tuple of -+ ints which can be sorted and compared. -+ -+ :returns: Tuple with version number parts converted to int. In case of -+ invalid version string, returns None -+ """ -+ -+ # Strip to remove eventual quotes and spaces -+ v_str = v_str.strip('" ') -+ -+ m = re.match(r"^(\d+)\.(\d+)$", v_str) -+ -+ if not m: -+ return None -+ -+ return (int(m.group(1)), int(m.group(2))) - - - def adjust( --- -2.47.1 - diff --git a/SOURCES/0005-Restore-RHEL-9-version-of-create_allowlist.sh.patch b/SOURCES/0005-Restore-RHEL-9-version-of-create_allowlist.sh.patch deleted file mode 100644 index bebd40f..0000000 --- a/SOURCES/0005-Restore-RHEL-9-version-of-create_allowlist.sh.patch +++ /dev/null @@ -1,404 +0,0 @@ -From c60460eccab93863dbd1fd0b748e5a275c8e6737 Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Tue, 3 Jun 2025 21:29:15 +0100 -Subject: [PATCH 5/6] Restore RHEL-9 version of create_allowlist.sh - -Signed-off-by: Sergio Correia ---- - scripts/create_runtime_policy.sh | 335 ++++++++++--------------------- - 1 file changed, 104 insertions(+), 231 deletions(-) - -diff --git a/scripts/create_runtime_policy.sh b/scripts/create_runtime_policy.sh -index 90ba50b..c0b641d 100755 ---- a/scripts/create_runtime_policy.sh -+++ b/scripts/create_runtime_policy.sh -@@ -1,282 +1,155 @@ --#!/usr/bin/env bash -+#!/usr/bin/bash - ################################################################################ - # SPDX-License-Identifier: Apache-2.0 - # Copyright 2017 Massachusetts Institute of Technology. - ################################################################################ - -- --if [ $0 != "-bash" ] ; then -- pushd `dirname "$0"` > /dev/null 2>&1 --fi --KCRP_BASE_DIR=$(pwd) --if [ $0 != "-bash" ] ; then -- popd 2>&1 > /dev/null --fi --KCRP_BASE_DIR=$KCRP_BASE_DIR/.. -- --function detect_hash { -- local hashstr=$1 -- -- case "${#hashstr}" in -- 32) hashalgo=md5sum ;; -- 40) hashalgo=sha1sum ;; -- 64) hashalgo=sha256sum ;; -- 128) hashalgo=sha512sum ;; -- *) hashalgo="na";; -- esac -- -- echo $hashalgo --} -- --function announce { -- # 1 - MESSAGE -- -- MESSAGE=$(echo "${1}" | tr '\n' ' ') -- MESSAGE=$(echo $MESSAGE | sed "s/\t\t*/ /g") -- -- echo "==> $(date) - ${0} - $MESSAGE" --} -- --function valid_algo { -- local algo=$1 -- -- [[ " ${ALGO_LIST[@]} " =~ " ${algo} " ]] --} -- - # Configure the installer here - INITRAMFS_TOOLS_GIT=https://salsa.debian.org/kernel-team/initramfs-tools.git - INITRAMFS_TOOLS_VER="master" - --# All defaults --ALGO=sha1sum --WORK_DIR=/tmp/kcrp --OUTPUT_DIR=${WORK_DIR}/output --ALLOWLIST_DIR=${WORK_DIR}/allowlist --INITRAMFS_LOC="/boot/" --INITRAMFS_STAGING_DIR=${WORK_DIR}/ima_ramfs/ --INITRAMFS_TOOLS_DIR=${WORK_DIR}/initramfs-tools --BOOT_AGGREGATE_LOC="/sys/kernel/security/ima/ascii_runtime_measurements" --ROOTFS_LOC="/" --EXCLUDE_LIST="none" --SKIP_PATH="none" --ALGO_LIST=("sha1sum" "sha256sum" "sha512sum") -+WORKING_DIR=$(readlink -f "$0") -+WORKING_DIR=$(dirname "$WORKING_DIR") - - # Grabs Debian's initramfs_tools from Git repo if no other options exist - if [[ ! `command -v unmkinitramfs` && ! -x "/usr/lib/dracut/skipcpio" ]] ; then - # Create temp dir for pulling in initramfs-tools -- announce "INFO: Downloading initramfs-tools: $INITRAMFS_TOOLS_DIR" -+ TMPDIR=`mktemp -d` || exit 1 -+ echo "INFO: Downloading initramfs-tools: $TMPDIR" - -- mkdir -p $INITRAMFS_TOOLS_DIR - # Clone initramfs-tools repo -- pushd $INITRAMFS_TOOLS_DIR > /dev/null 2>&1 -- git clone $INITRAMFS_TOOLS_GIT initramfs-tools > /dev/null 2>&1 -- pushd initramfs-tools > /dev/null 2>&1 -- git checkout $INITRAMFS_TOOLS_VER > /dev/null 2>&1 -- popd > /dev/null 2>&1 -- popd > /dev/null 2>&1 -+ pushd $TMPDIR -+ git clone $INITRAMFS_TOOLS_GIT initramfs-tools -+ pushd initramfs-tools -+ git checkout $INITRAMFS_TOOLS_VER -+ popd # $TMPDIR -+ popd - - shopt -s expand_aliases -- alias unmkinitramfs=$INITRAMFS_TOOLS_DIR/initramfs-tools/unmkinitramfs -- -- which unmkinitramfs > /dev/null 2>&1 || exit 1 -+ alias unmkinitramfs=$TMPDIR/initramfs-tools/unmkinitramfs - fi - -+ - if [[ $EUID -ne 0 ]]; then - echo "This script must be run as root" 1>&2 - exit 1 - fi - --USAGE=$(cat <<-END -- Usage: $0 -o/--output_file FILENAME [-a/--algo ALGO] [-x/--ramdisk-location PATH] [-y/--boot_aggregate-location PATH] [-z/--rootfs-location PATH] [-e/--exclude_list FILENAME] [-s/--skip-path PATH] [-h/--help] -+if [ $# -lt 1 ] -+then -+ echo "No arguments provided" >&2 -+ echo "Usage: `basename $0` -o [filename] -h [hash-algo]" >&2 -+ exit $NOARGS; -+fi - -- optional arguments: -- -a/--algo (checksum algorithm to be used, default: $ALGO) -- -x/--ramdisk-location (path to initramdisk, default: $INITRAMFS_LOC, set to "none" to skip) -- -y/--boot_aggregate-location (path for IMA log, used for boot aggregate extraction, default: $BOOT_AGGREGATE_LOC, set to "none" to skip) -- -z/--rootfs-location (path to root filesystem, default: $ROOTFS_LOC, cannot be skipped) -- -e/--exclude_list (filename containing a list of paths to be excluded (i.e., verifier will not try to match checksums, default: $EXCLUDE_LIST) -- -s/--skip-path (comma-separated path list, files found there will not have checksums calculated, default: $SKIP_PATH) -- -h/--help (show this message and exit) --END --) -+ALGO=sha256sum - --while [[ $# -gt 0 ]] --do -- key="$1" -+ALGO_LIST=("sha1sum" "sha256sum" "sha512sum") -+ -+valid_algo() { -+ local algo=$1 -+ -+ [[ " ${ALGO_LIST[@]} " =~ " ${algo} " ]] -+} - -- case $key in -- -a|--algo) -- ALGO="$2" -- shift -- ;; -- -a=*|--algo=*) -- ALGO=$(echo $key | cut -d '=' -f 2) -- ;; -- -x|--ramdisk-location) -- INITRAMFS_LOC="$2" -- shift -- ;; -- -x=*|--ramdisk-location=*) -- INITRAMFS_LOC=$(echo $key | cut -d '=' -f 2) -- ;; -- -y|--boot_aggregate-location) -- BOOT_AGGREGATE_LOC=$2 -- shift -- ;; -- -y=*|--boot_aggregate-location=*) -- BOOT_AGGREGATE_LOC=$(echo $key | cut -d '=' -f 2) -- ;; -- -z|--rootfs-location) -- ROOTFS_LOC=$2 -- shift -- ;; -- -z=*|--rootfs-location=*) -- ROOTFS_LOC=$(echo $key | cut -d '=' -f 2) -- ;; -- -e|--exclude_list) -- EXCLUDE_LIST=$2 -- shift -- ;; -- -e=*|--exclude_list=*) -- EXCLUDE_LIST=$(echo $key | cut -d '=' -f 2) -- ;; -- -o=*|--output_file=*) -- OUTPUT=$(echo $key | cut -d '=' -f 2) -- ;; -- -o|--output_file) -- OUTPUT=$2 -- shift -- ;; -- -s=*|--skip-path=*) -- SKIP_PATH=$(echo $key | cut -d '=' -f 2) -- ;; -- -s|--skip-path) -- SKIP_PATH=$2 -- shift -- ;; -- -h|--help) -- printf "%s\n" "$USAGE" -- exit 0 -- shift -- ;; -- *) -- # unknown option -- ;; -- esac -- shift -+while getopts ":o:h:" opt; do -+ case $opt in -+ o) -+ OUTPUT=$(readlink -f $OPTARG) -+ rm -f $OUTPUT -+ ;; -+ h) -+ if valid_algo $OPTARG; then -+ ALGO=$OPTARG -+ else -+ echo "Invalid hash function argument: use sha1sum, sha256sum, or sha512sum" -+ exit 1 -+ fi -+ ;; -+ esac - done - --if ! valid_algo $ALGO -+if [ ! "$OUTPUT" ] - then -- echo "Invalid hash function argument: pick from \"${ALGO_LIST[@]}\"" -+ echo "Missing argument for -o" >&2; -+ echo "Usage: $0 -o [filename] -h [hash-algo]" >&2; - exit 1 - fi - --if [[ -z $OUTPUT ]] --then -- printf "%s\n" "$USAGE" -- exit 1 -+ -+# Where to look for initramfs image -+INITRAMFS_LOC="/boot" -+if [ -d "/ostree" ]; then -+ # If we are on an ostree system change where we look for initramfs image -+ loc=$(grep -E "/ostree/[^/]([^/]*)" -o /proc/cmdline | head -n 1 | cut -d / -f 3) -+ INITRAMFS_LOC="/boot/ostree/${loc}/" - fi - --rm -rf $ALLOWLIST_DIR --rm -rf $INITRAMFS_STAGING_DIR --rm -rf $OUTPUT_DIR - --announce "Writing allowlist $ALLOWLIST_DIR/${OUTPUT} with $ALGO..." --mkdir -p $ALLOWLIST_DIR -+echo "Writing allowlist to $OUTPUT with $ALGO..." - --if [[ $BOOT_AGGREGATE_LOC != "none" ]] --then -- announce "--- Adding boot agregate from $BOOT_AGGREGATE_LOC on allowlist $ALLOWLIST_DIR/${OUTPUT} ..." - # Add boot_aggregate from /sys/kernel/security/ima/ascii_runtime_measurements (IMA Log) file. - # The boot_aggregate measurement is always the first line in the IMA Log file. - # The format of the log lines is the following: - # - # File_Digest may start with the digest algorithm specified (e.g "sha1:", "sha256:") depending on the template used. -- head -n 1 $BOOT_AGGREGATE_LOC | awk '{ print $4 " boot_aggregate" }' | sed 's/.*://' >> $ALLOWLIST_DIR/${OUTPUT} -+head -n 1 /sys/kernel/security/ima/ascii_runtime_measurements | awk '{ print $4 " boot_aggregate" }' | sed 's/.*://' >> $OUTPUT - -- bagghash=$(detect_hash $(cat $ALLOWLIST_DIR/${OUTPUT} | cut -d ' ' -f 1)) -- if [[ $ALGO != $bagghash ]] -- then -- announce "ERROR: \"boot aggregate\" has was calculated with $bagghash, but files will be calculated with $ALGO. Use option -a $bagghash" -- exit 1 -- fi --else -- announce "--- Skipping boot aggregate..." --fi -- --announce "--- Adding all appropriate files from $ROOTFS_LOC on allowlist $ALLOWLIST_DIR/${OUTPUT} ..." - # Add all appropriate files under root FS to allowlist --pushd $ROOTFS_LOC > /dev/null 2>&1 --BASE_EXCLUDE_DIRS="\bsys\b\|\brun\b\|\bproc\b\|\blost+found\b\|\bdev\b\|\bmedia\b\|\bsnap\b\|\bmnt\b\|\bvar\b\|\btmp\b" --ROOTFS_FILE_LIST=$(ls | grep -v $BASE_EXCLUDE_DIRS) --if [[ $SKIP_PATH != "none" ]] --then -- SKIP_PATH=$(echo $SKIP_PATH | sed -e "s#^$ROOTFS_LOC##g" -e "s#,$ROOTFS_LOC##g" -e "s#,#\\\|#g") -- ROOTFS_FILE_LIST=$(echo "$ROOTFS_FILE_LIST" | grep -v "$SKIP_PATH") --fi --find $ROOTFS_FILE_LIST \( -fstype rootfs -o -xtype f -type l -o -type f \) -uid 0 -exec $ALGO "$ROOTFS_LOC/{}" >> $ALLOWLIST_DIR/${OUTPUT} \; --popd > /dev/null 2>&1 -+cd / -+find `ls / | grep -v "\bsys\b\|\brun\b\|\bproc\b\|\blost+found\b\|\bdev\b\|\bmedia\b\|\bsnap\b\|mnt"` \( -fstype rootfs -o -xtype f -type l -o -type f \) -uid 0 -exec $ALGO '/{}' >> $OUTPUT \; - - # Create staging area for init ram images --mkdir -p $INITRAMFS_STAGING_DIR -+rm -rf /tmp/ima/ -+mkdir -p /tmp/ima - --if [[ $INITRAMFS_LOC != "none" ]] --then -- # Where to look for initramfs image -- if [[ -d "/ostree" ]] -- then -- X=$INITRAMFS_LOC -- # If we are on an ostree system change where we look for initramfs image -- loc=$(grep -E "/ostree/[^/]([^/]*)" -o /proc/cmdline | head -n 1 | cut -d / -f 3) -- INITRAMFS_LOC="/boot/ostree/${loc}/" -- announce "--- The location of initramfs was overriden from \"${X}\" to \"$INITRAMFS_LOC\"" -- fi -- -- announce "--- Creating allowlist for init ram disks found under \"$INITRAMFS_LOC\" to $ALLOWLIST_DIR/${OUTPUT} ..." -- for i in $(ls ${INITRAMFS_LOC}/initr* 2> /dev/null) -- do -- announce " extracting $i" -- mkdir -p $INITRAMFS_STAGING_DIR/$i-extracted -- cd $INITRAMFS_STAGING_DIR/$i-extracted -- -- # platform-specific handling of init ram disk images -- if [[ `command -v unmkinitramfs` ]] ; then -- mkdir -p $INITRAMFS_STAGING_DIR/$i-extracted-unmk -- unmkinitramfs $i $INITRAMFS_STAGING_DIR/$i-extracted-unmk -- if [[ -d "$INITRAMFS_STAGING_DIR/$i-extracted-unmk/main/" ]] ; then -- cp -r $INITRAMFS_STAGING_DIR/$i-extracted-unmk/main/. /tmp/ima/$i-extracted -- else -- cp -r $INITRAMFS_STAGING_DIR/$i-extracted-unmk/. /tmp/ima/$i-extracted -- fi -- elif [[ -x "/usr/lib/dracut/skipcpio" ]] ; then -- /usr/lib/dracut/skipcpio $i | gunzip -c | cpio -i -d 2> /dev/null -+# Iterate through init ram disks and add files to allowlist -+echo "Creating allowlist for init ram disk" -+for i in `ls ${INITRAMFS_LOC}/initr*` -+do -+ echo "extracting $i" -+ mkdir -p /tmp/ima/$i-extracted -+ cd /tmp/ima/$i-extracted -+ -+ # platform-specific handling of init ram disk images -+ if [[ `command -v unmkinitramfs` ]] ; then -+ mkdir -p /tmp/ima/$i-extracted-unmk -+ unmkinitramfs $i /tmp/ima/$i-extracted-unmk -+ if [[ -d "/tmp/ima/$i-extracted-unmk/main/" ]] ; then -+ cp -r /tmp/ima/$i-extracted-unmk/main/. /tmp/ima/$i-extracted - else -- announce "ERROR: No tools for initramfs image processing found!" -- exit 1 -+ cp -r /tmp/ima/$i-extracted-unmk/. /tmp/ima/$i-extracted - fi -+ elif [[ -x "/usr/lib/dracut/skipcpio" ]] ; then -+ /usr/lib/dracut/skipcpio $i | gunzip -c 2> /dev/null | cpio -i -d 2> /dev/null -+ else -+ echo "ERROR: No tools for initramfs image processing found!" -+ break -+ fi - -- find -type f -exec $ALGO "./{}" \; | sed "s| \./\./| /|" >> $ALLOWLIST_DIR/${OUTPUT} -- done --fi -- --# Non-critical cleanup on the resulting file (when ROOTFS_LOC = '/', the path starts on allowlist ends up with double '//' ) --sed -i "s^ //^ /^g" $ALLOWLIST_DIR/${OUTPUT} --# A bit of cleanup on the resulting file (among other problems, sha256sum might output a hash with the prefix '\\') --sed -i "s/^\\\//g" $ALLOWLIST_DIR/${OUTPUT} -- --# Convert to runtime policy --mkdir -p $OUTPUT_DIR --announce "Converting created allowlist ($ALLOWLIST_DIR/${OUTPUT}) to Keylime runtime policy ($OUTPUT_DIR/${OUTPUT}) ..." --CONVERT_CMD_OPTS="--allowlist $ALLOWLIST_DIR/${OUTPUT} --output_file $OUTPUT_DIR/${OUTPUT}" --[ -f $EXCLUDE_LIST ] && CONVERT_CMD_OPTS="$CONVERT_CMD_OPTS --excludelist "$(readlink -f -- "${EXCLUDE_LIST}")"" -+ find -type f -exec $ALGO "./{}" \; | sed "s| \./\./| /|" >> $OUTPUT -+done - --pushd $KCRP_BASE_DIR > /dev/null 2>&1 --export PYTHONPATH=$KCRP_BASE_DIR:$PYTHONPATH --# only 3 dependencies required: pip3 install cryptography lark packaging --python3 ./keylime/cmd/convert_runtime_policy.py $CONVERT_CMD_OPTS; echo " " --if [[ $? -eq 0 ]] --then -- announce "Done, new runtime policy file present at ${OUTPUT_DIR}/$OUTPUT. It can be used on the tenant keylime host with \"keylime_tenant -c add --runtime-policy ${OUTPUT_DIR}/$OUTPUT " --fi --popd > /dev/null 2>&1 -+# when ROOTFS_LOC = '/', the path starts on allowlist ends up with double '//' -+# -+# Example: -+# -+# b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c //bar -+# -+# Replace the unwanted '//' with a single '/' -+sed -i 's| /\+| /|g' $ALLOWLIST_DIR/${OUTPUT} -+ -+# When the file name contains newlines or backslashes, the output of sha256sum -+# adds a backslash at the beginning of the line. -+# -+# Example: -+# -+# $ echo foo > ba\\r -+# $ sha256sum ba\\r -+# \b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c ba\\r -+# -+# Remove the unwanted backslash prefix -+sed -i 's/^\\//g' $ALLOWLIST_DIR/${OUTPUT} -+ -+# Clean up -+rm -rf /tmp/ima --- -2.47.1 - diff --git a/SOURCES/0006-Revert-default-server_key_password-for-verifier-regi.patch b/SOURCES/0006-Revert-default-server_key_password-for-verifier-regi.patch deleted file mode 100644 index 48d8420..0000000 --- a/SOURCES/0006-Revert-default-server_key_password-for-verifier-regi.patch +++ /dev/null @@ -1,66 +0,0 @@ -From 733db4036f2142152795fc51b761f05e39594b08 Mon Sep 17 00:00:00 2001 -From: Sergio Correia -Date: Tue, 27 May 2025 09:31:54 +0000 -Subject: [PATCH 6/6] Revert "default" server_key_password for - verifier/registrar - -Signed-off-by: Sergio Correia ---- - templates/2.0/mapping.json | 4 ++-- - templates/2.1/mapping.json | 6 +++--- - 2 files changed, 5 insertions(+), 5 deletions(-) - -diff --git a/templates/2.0/mapping.json b/templates/2.0/mapping.json -index 80dcdde..8fce124 100644 ---- a/templates/2.0/mapping.json -+++ b/templates/2.0/mapping.json -@@ -232,7 +232,7 @@ - "server_key_password": { - "section": "cloud_verifier", - "option": "private_key_pw", -- "default": "" -+ "default": "default" - }, - "enable_agent_mtls": { - "section": "cloud_verifier", -@@ -563,7 +563,7 @@ - "server_key_password": { - "section": "registrar", - "option": "private_key_pw", -- "default": "" -+ "default": "default" - }, - "server_cert": { - "section": "registrar", -diff --git a/templates/2.1/mapping.json b/templates/2.1/mapping.json -index 956a53a..88e3fb6 100644 ---- a/templates/2.1/mapping.json -+++ b/templates/2.1/mapping.json -@@ -262,7 +262,7 @@ - "server_key_password": { - "section": "verifier", - "option": "server_key_password", -- "default": "" -+ "default": "default" - }, - "enable_agent_mtls": { - "section": "verifier", -@@ -593,7 +593,7 @@ - "server_key_password": { - "section": "registrar", - "option": "server_key_password", -- "default": "" -+ "default": "default" - }, - "server_cert": { - "section": "registrar", -@@ -835,4 +835,4 @@ - "handler_consoleHandler": "logging", - "logger_keylime": "logging" - } --} -\ No newline at end of file -+} --- -2.47.1 - diff --git a/SOURCES/0007-fix_db_connection_leaks.patch b/keylime-fix-db-connection-leaks.patch similarity index 100% rename from SOURCES/0007-fix_db_connection_leaks.patch rename to keylime-fix-db-connection-leaks.patch diff --git a/SPECS/keylime.spec b/keylime.spec similarity index 51% rename from SPECS/keylime.spec rename to keylime.spec index a31de45..ad1ae93 100644 --- a/SPECS/keylime.spec +++ b/keylime.spec @@ -1,58 +1,75 @@ +## START: Set by rpmautospec +## (rpmautospec version 0.6.5) +## RPMAUTOSPEC: autochangelog +## END: Set by rpmautospec + %global srcname keylime %global policy_version 42.1.2 -%global with_selinux 1 -%global selinuxtype targeted # Package is actually noarch, but it has an optional dependency that is # arch-specific. %global debug_package %{nil} +%global with_selinux 1 +%global selinuxtype targeted Name: keylime Version: 7.12.1 -Release: 11%{?dist} +Release: 11%{?dist}.2 Summary: Open source TPM software for Bootstrapping and Maintaining Trust URL: https://github.com/keylime/keylime Source0: https://github.com/keylime/keylime/archive/refs/tags/v%{version}.tar.gz +# The selinux policy for keylime is distributed via this repo: https://github.com/RedHat-SP-Security/keylime-selinux Source1: https://github.com/RedHat-SP-Security/%{name}-selinux/archive/v%{policy_version}/keylime-selinux-%{policy_version}.tar.gz Source2: %{srcname}.sysusers Source3: %{srcname}.tmpfiles -Patch: 0001-Make-keylime-compatible-with-python-3.9.patch -Patch: 0002-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch -Patch: 0003-tests-skip-measured-boot-related-tests-for-s390x-and.patch -Patch: 0004-templates-duplicate-str_to_version-in-the-adjust-scr.patch -# RHEL-9 ships a slightly modified version of create_allowlist.sh and -# also a "default" server_key_password for the registrar and verifier. -# DO NOT REMOVE THE FOLLOWING TWO PATCHES IN FOLLOWING RHEL-9.x REBASES. -Patch: 0005-Restore-RHEL-9-version-of-create_allowlist.sh.patch -Patch: 0006-Revert-default-server_key_password-for-verifier-regi.patch # Backported from https://github.com/keylime/keylime/pull/1782 -Patch: 0007-fix_db_connection_leaks.patch +# 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: 0008-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch -Patch: 0009-mb-support-vendor_db-as-logged-by-newer-shim-version.patch +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: 0010-verifier-Gracefully-shutdown-on-signal.patch -Patch: 0011-revocations-Try-to-send-notifications-on-shutdown.patch -Patch: 0012-requests_client-close-the-session-at-the-end-of-the-.patch +# 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 -License: ASL 2.0 and MIT +# 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 + +# Main program: Apache-2.0 +# Icons: MIT +License: Apache-2.0 AND MIT BuildRequires: git-core +BuildRequires: openssl BuildRequires: openssl-devel BuildRequires: python3-devel BuildRequires: python3-dbus BuildRequires: python3-jinja2 BuildRequires: python3-cryptography +BuildRequires: python3-gpg BuildRequires: python3-pyasn1 BuildRequires: python3-pyasn1-modules BuildRequires: python3-tornado BuildRequires: python3-sqlalchemy -BuildRequires: python3-lark-parser +BuildRequires: python3-lark BuildRequires: python3-psutil BuildRequires: python3-pyyaml BuildRequires: python3-jsonschema @@ -67,10 +84,20 @@ Requires: %{srcname}-base = %{version}-%{release} Requires: %{srcname}-verifier = %{version}-%{release} Requires: %{srcname}-registrar = %{version}-%{release} Requires: %{srcname}-tenant = %{version}-%{release} +Requires: %{srcname}-tools = %{version}-%{release} + +# webapp was removed upstream in release 6.4.2. +Obsoletes: %{srcname}-webapp < 6.4.2 + +# python agent was removed upstream in release 7.0.0. +Obsoletes: python3-%{srcname}-agent < 7.0.0 # Agent. Requires: keylime-agent -Suggests: keylime-agent-rust +Suggests: %{srcname}-agent-rust + +# Conflicts with the monolithic versions of the package, before the split. +Conflicts: keylime < 6.3.0-3 %{?python_enable_dependency_generator} %description @@ -81,10 +108,11 @@ and runtime integrity measurement solution. Summary: The base package contains the default configuration License: MIT +# Conflicts with the monolithic versions of the package, before the split. +Conflicts: keylime < 6.3.0-3 Requires(pre): python3-jinja2 Requires(pre): shadow-utils -Requires(pre): util-linux Requires(pre): tpm2-tss Requires: procps-ng Requires: openssl @@ -108,6 +136,9 @@ The base package contains the Keylime default configuration Summary: The Python Keylime module License: MIT +# Conflicts with the monolithic versions of the package, before the split. +Conflicts: keylime < 6.3.0-3 + Requires: %{srcname}-base = %{version}-%{release} %{?python_provide:%python_provide python3-%{srcname}} @@ -122,10 +153,10 @@ Requires: python3-gpg Requires: python3-lark-parser Requires: python3-pyasn1 Requires: python3-pyasn1-modules +requires: python3-psutil Requires: python3-jsonschema -Requires: python3-psutil +Requires: python3-typing-extensions Requires: tpm2-tools -Requires: openssl %description -n python3-%{srcname} The python3-keylime module implements the functionality used @@ -135,6 +166,9 @@ by Keylime components. Summary: The Python Keylime Verifier component License: MIT +# Conflicts with the monolithic versions of the package, before the split. +Conflicts: keylime < 6.3.0-3 + Requires: %{srcname}-base = %{version}-%{release} Requires: python3-%{srcname} = %{version}-%{release} @@ -146,6 +180,9 @@ of the machine that the agent is running on. Summary: The Keylime Registrar component License: MIT +# Conflicts with the monolithic versions of the package, before the split. +Conflicts: keylime < 6.3.0-3 + Requires: %{srcname}-base = %{version}-%{release} Requires: python3-%{srcname} = %{version}-%{release} @@ -171,6 +208,9 @@ Custom SELinux policy module Summary: The Python Keylime Tenant License: MIT +# Conflicts with the monolithic versions of the package, before the split. +Conflicts: keylime < 6.3.0-3 + Requires: %{srcname}-base = %{version}-%{release} Requires: python3-%{srcname} = %{version}-%{release} @@ -178,13 +218,26 @@ Requires: python3-%{srcname} = %{version}-%{release} %description tenant The Keylime Tenant can be used to provision a Keylime Agent. +%package tools +Summary: Keylime tools +License: MIT + +# Conflicts with the monolithic versions of the package, before the split. +Conflicts: keylime < 6.3.0-3 + +Requires: %{srcname}-base = %{version}-%{release} +Requires: python3-%{srcname} = %{version}-%{release} + +%description tools +The keylime tools package includes miscelaneous tools. + + %prep %autosetup -S git -n %{srcname}-%{version} -a1 %if 0%{?with_selinux} # SELinux policy (originally from selinux-policy-contrib) # this policy module will override the production module -mkdir selinux make -f %{_datadir}/selinux/devel/Makefile %{srcname}.pp bzip2 -9 %{srcname}.pp @@ -204,20 +257,18 @@ for comp in "verifier" "tenant" "registrar" "ca" "logging"; do install -Dpm 400 config/${comp}.conf %{buildroot}/%{_sysconfdir}/%{srcname} done -# Ship some scripts. -mkdir -p %{buildroot}/%{_datadir}/%{srcname}/scripts -for s in create_mb_refstate \ - ek-openssl-verify; do - install -Dpm 755 scripts/${s} \ - %{buildroot}/%{_datadir}/%{srcname}/scripts/${s} +# Do not ship a few scripts that are to be obsoleted soon. +# The functionality they provide is now provided by keylime-policy. +for s in keylime_convert_runtime_policy \ + keylime_create_policy \ + keylime_sign_runtime_policy; do + rm -f %{buildroot}/%{_bindir}/"${s}" done -# On RHEL 9.3, install create_runtime_policy.sh as create_allowlist.sh -# The convert_runtime_policy.py script to convert allowlist and excludelist into -# runtime policy is not called anymore. -# See: https://issues.redhat.com/browse/RHEL-11866 -install -Dpm 755 scripts/create_runtime_policy.sh \ - %{buildroot}/%{_datadir}/%{srcname}/scripts/create_allowlist.sh +# 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 # Ship configuration templates. cp -r ./templates %{buildroot}%{_datadir}/%{srcname}/templates/ @@ -265,11 +316,11 @@ export KEYLIME_LOGGING_CONFIG="${CONF_TEMP_DIR}/logging.conf" # Cleanup. [ "${CONF_TEMP_DIR}" ] && rm -rf "${CONF_TEMP_DIR}" -for e in KEYLIME_VERIFIER_CONFIG \ - KEYLIME_TENANT_CONFIG \ - KEYLIME_REGISTRAR_CONFIG \ - KEYLIME_CA_CONFIG \ - KEYLIME_LOGGING_CONFIG; do + for e in KEYLIME_VERIFIER_CONFIG \ + KEYLIME_TENANT_CONFIG \ + KEYLIME_REGISTRAR_CONFIG \ + KEYLIME_CA_CONFIG \ + KEYLIME_LOGGING_CONFIG; do unset "${e}" done exit 0 @@ -279,12 +330,7 @@ exit 0 exit 0 %post base -for c in ca logging; do - [ -e /etc/keylime/"${c}.conf" ] || continue - /usr/bin/keylime_upgrade_config --component "${c}" \ - --input /etc/keylime/"${c}.conf" \ - >/dev/null -done +/usr/bin/keylime_upgrade_config --component ca --component logging >/dev/null exit 0 %posttrans base @@ -304,43 +350,19 @@ fi [ -d %{_sharedstatedir}/%{srcname}/tpm_cert_store ] && \ chmod 400 %{_sharedstatedir}/%{srcname}/tpm_cert_store/*.pem && \ chmod 500 %{_sharedstatedir}/%{srcname}/tpm_cert_store/ -exit 0 %post verifier -[ -e /etc/keylime/verifier.conf ] && \ - /usr/bin/keylime_upgrade_config --component verifier \ - --input /etc/keylime/verifier.conf \ - >/dev/null +/usr/bin/keylime_upgrade_config --component verifier >/dev/null %systemd_post %{srcname}_verifier.service -exit 0 %post registrar -[ -e /etc/keylime/registrar.conf ] && \ - /usr/bin/keylime_upgrade_config --component registrar \ - --input /etc/keylime/registrar.conf / - >/dev/null +/usr/bin/keylime_upgrade_config --component registrar >/dev/null %systemd_post %{srcname}_registrar.service -exit 0 %post tenant -[ -e /etc/keylime/tenant.conf ] && \ - /usr/bin/keylime_upgrade_config --component tenant \ - --input /etc/keylime/tenant.conf \ - >/dev/null +/usr/bin/keylime_upgrade_config --component tenant >/dev/null exit 0 -%preun verifier -%systemd_preun %{srcname}_verifier.service - -%preun registrar -%systemd_preun %{srcname}_registrar.service - -%postun verifier -%systemd_postun_with_restart %{srcname}_verifier.service - -%postun registrar -%systemd_postun_with_restart %{srcname}_registrar.service - %if 0%{?with_selinux} # SELinux contexts are saved so that only affected files can be # relabeled after the policy module installation @@ -355,7 +377,7 @@ if [ "$1" -le "1" ]; then # First install # The services need to be restarted for the custom label to be # applied in case they where already present in the system, # restart fails silently in case they where not. - for svc in agent registrar verifier; do + for svc in registrar verifier; do [ -f "%{_unitdir}/%{srcname}_${svc}".service ] && \ %systemd_postun_with_restart "%{srcname}_${svc}".service done @@ -369,6 +391,21 @@ if [ $1 -eq 0 ]; then fi %endif +%preun verifier +%systemd_preun %{srcname}_verifier.service + +%preun registrar +%systemd_preun %{srcname}_registrar.service + +%preun tenant +%systemd_preun %{srcname}_registrar.service + +%postun verifier +%systemd_postun_with_restart %{srcname}_verifier.service + +%postun registrar +%systemd_postun_with_restart %{srcname}_registrar.service + %files verifier %license LICENSE %attr(500,%{srcname},%{srcname}) %dir %{_sysconfdir}/%{srcname}/verifier.conf.d @@ -401,14 +438,14 @@ fi %license LICENSE %{python3_sitelib}/%{srcname}-*.egg-info/ %{python3_sitelib}/%{srcname} -%{_datadir}/%{srcname}/scripts/create_mb_refstate %{_bindir}/keylime_attest -%{_bindir}/keylime_convert_runtime_policy -%{_bindir}/keylime_create_policy -%{_bindir}/keylime_sign_runtime_policy -%{_bindir}/keylime_userdata_encrypt %{_bindir}/keylime-policy + +%files tools +%license LICENSE +%{_bindir}/%{srcname}_userdata_encrypt + %files base %license LICENSE %doc README.md @@ -424,7 +461,6 @@ fi %attr(400,%{srcname},%{srcname}) %{_sharedstatedir}/%{srcname}/tpm_cert_store/*.pem %{_tmpfilesdir}/%{srcname}.conf %{_sysusersdir}/%{srcname}.conf -%{_datadir}/%{srcname}/scripts/create_allowlist.sh %{_datadir}/%{srcname}/scripts/ek-openssl-verify %{_datadir}/%{srcname}/templates %{_bindir}/keylime_upgrade_config @@ -433,178 +469,240 @@ fi %license LICENSE %changelog -* Mon Aug 18 2025 Sergio Correia - 7.12.1-11 -- Fix for revocation notifier not closing TLS session correctly - Resolves: RHEL-109656 +## START: Generated by rpmautospec +* Mon Sep 15 2025 Anderson Toshiyuki Sasaki - 7.12.1-14 +- Properly fix malformed TPM certificates workaround -* Wed Aug 13 2025 Sergio Correia - 7.12.1-10 -- Support vendor_db: follow-up fix - Related: RHEL-80455 +* Thu Aug 28 2025 Anderson Toshiyuki Sasaki - 7.12.1-13 +- Avoid opening /dev/stdout when printing + +* Wed Aug 27 2025 Anderson Toshiyuki Sasaki - 7.12.1-12 +- Fix malformed TPM certificates workaround + +* Wed Aug 20 2025 Sergio Correia - 7.12.1-11 +- mba: normalize vendor_db in EV_EFI_VARIABLE_AUTHORITY events + +* Mon Aug 18 2025 Sergio Correia - 7.12.1-10 +- Fix for revocation notifier not closing TLS session correctly * Tue Aug 12 2025 Sergio Correia - 7.12.1-9 - Support vendor_db as logged by newer shim versions - Resolves: RHEL-80455 * Fri Aug 08 2025 Anderson Toshiyuki Sasaki - 7.12.1-8 - Fix DB connection leaks - Resolves: RHEL-108263 -* Tue Jul 22 2025 Sergio Correia - 7.12.1-7 +* Thu Jul 24 2025 Sergio Correia - 7.12.1-7 - Fix tmpfiles.d configuration related to the cert store - Resolves: RHEL-104572 * Thu Jul 10 2025 Sergio Correia - 7.12.1-6 - Populate cert_store_dir with tpmfiles.d - Resolves: RHEL-76926 * Thu Jul 10 2025 Sergio Correia - 7.12.1-5 - Use tmpfiles.d for permissions in /var/lib/keylime and /etc/keylime - Resolves: RHEL-77144 -* Tue Jul 08 2025 Patrik Koncity - 7.12.1-4 -- Add new keylime-selinux release - removing keylime_var_log_t label - Resolves: RHEL-388 +* Wed Jul 09 2025 Patrik Koncity - 7.12.1-4 +- Use the newest keylime-selinux release -* Fri Jun 20 2025 Anderson Toshiyuki Sasaki - 7.12.1-3 -- Avoid changing ownership of /var/log/keylime - Resolves: RHEL-388 +* Wed Jul 02 2025 Anderson Toshiyuki Sasaki - 7.12.1-3 +- Avoid changing the ownership of /var/log/keylime -* Tue May 27 2025 Sergio Correia - 7.12.1-2 -- Revert changes to default server_key_password for verifier/registrar - Resolves: RHEL-93678 +* Mon Feb 17 2025 Sergio Correia - 7.12.1-2 +- Drop old keylime policy related scripts -* Thu May 22 2025 Sergio Correia - 7.12.1-1 -- Update to 7.12.1 - Resolves: RHEL-78418 +* Fri Feb 14 2025 Sergio Correia - 7.12.1-1 +- Updating for Keylime release v7.12.1 -* Wed Feb 05 2025 Sergio Correia - 7.3.0-15 +* Tue Oct 29 2024 Troy Dawson - 7.9.0-8 +- Bump release for October 2024 mass rebuild: + +* Mon Aug 19 2024 Anderson Toshiyuki Sasaki - 7.9.0-7 - Use TLS on revocation notification webhook -- Include system installed CA certificates when verifying webhook - server certificate +- Include system installed CA certificates when verifying webhook server + certificate - Include the CA certificates added via configuration file option 'trusted_server_ca' - Resolves: RHEL-78057 - Resolves: RHEL-78313 - Resolves: RHEL-78316 -* Fri Jan 10 2025 Sergio Correia - 7.3.0-14 -- Backport keylime-policy tool - Resolves: RHEL-75797 +* Fri Aug 16 2024 Anderson Toshiyuki Sasaki - 7.9.0-6 +- Restore create_allowlist.sh to be the same as in RHEL-9 -* Fri Jan 05 2024 Sergio Correia - 7.3.0-13 -- Backport fix for CVE-2023-3674 - Resolves: RHEL-21013 +* Mon Jun 24 2024 Karel Srot - 7.9.0-5 +- Add rhel-10 gating.yaml -* Tue Oct 17 2023 Anderson Toshiyuki Sasaki - 7.3.0-12 -- Set the generator and timestamp in create_policy.py - Related: RHEL-11866 +* Mon Jun 24 2024 Troy Dawson - 7.9.0-4 +- Bump release for June 2024 mass rebuild -* Mon Oct 09 2023 Anderson Toshiyuki Sasaki - 7.3.0-11 -- Suppress unnecessary error message - Related: RHEL-11866 +* Thu May 09 2024 Karel Srot - 7.9.0-3 +- tests: Update CI test plan for C10S -* Fri Oct 06 2023 Anderson Toshiyuki Sasaki - 7.3.0-10 -- Restore allowlist generation script - Resolves: RHEL-11866 - Resolves: RHEL-11867 +* Mon Feb 12 2024 Sergio Correia - 7.9.0-2 +- Fixes for rawhide -* Wed Sep 06 2023 Sergio Correia - 7.3.0-9 -- Rebuild for properly tagging the resulting build - Resolves: RHEL-1898 +* Tue Jan 30 2024 Sergio Correia - 7.9.0-1 +- Updating for Keylime release v7.9.0 +- Migrated license to SPDX -* Fri Sep 01 2023 Sergio Correia - 7.3.0-8 -- Add missing dependencies python3-jinja2 and util-linux - Resolves: RHEL-1898 +* Wed Jan 24 2024 Fedora Release Engineering - 7.8.0-3 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild -* Mon Aug 28 2023 Anderson Toshiyuki Sasaki - 7.3.0-7 -- Automatically update agent API version - Resolves: RHEL-1518 +* Sun Jan 21 2024 Fedora Release Engineering - 7.8.0-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild -* Mon Aug 28 2023 Sergio Correia - 7.3.0-6 -- Fix registrar is subject to a DoS against SSL (CVE-2023-38200) - Resolves: rhbz#2222694 +* Tue Dec 05 2023 Sergio Correia - 7.8.0-1 +- Updating for Keylime release v7.8.0 -* Fri Aug 25 2023 Anderson Toshiyuki Sasaki - 7.3.0-5 -- Fix challenge-protocol bypass during agent registration (CVE-2023-38201) - Resolves: rhbz#2222695 +* Thu Nov 02 2023 Sergio Correia - 7.7.0-1 +- Updating for Keylime release v7.7.0 -* Tue Aug 22 2023 Sergio Correia - 7.3.0-4 -- Update spec file to use %verify(not md5 size mode mtime) for files updated in %post scriptlets - Resolves: RHEL-475 +* Thu Aug 24 2023 Sergio Correia - 7.5.0-1 +- Updating for Keylime release v7.5.0 -* Tue Aug 15 2023 Sergio Correia - 7.3.0-3 -- Fix Keylime configuration upgrades issues introduced in last rebase - Resolves: RHEL-475 -- Handle session close using a session manager - Resolves: RHEL-1252 -- Add ignores for EV_PLATFORM_CONFIG_FLAGS - Resolves: RHEL-947 +* Mon Jul 31 2023 Sergio Correia - 7.3.0-1 +- Updating for Keylime release v7.3.0 -* Tue Aug 8 2023 Patrik Koncity - 7.3.0-2 -- Keylime SELinux policy provides more restricted ports. -- New SELinux label for ports used by keylime. -- Adding tabrmd interfaces allow unix stream socket communication and dbus communication. -- Allow the keylime_server_t domain to get the attributes of all filesystems. - Resolves: RHEL-595 - Resolves: RHEL-390 - Resolves: RHEL-948 +* Thu Jul 20 2023 Fedora Release Engineering - 7.2.5-4 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_39_Mass_Rebuild -* Wed Jul 19 2023 Sergio Correia - 7.3.0-1 -- Update to 7.3.0 - Resolves: RHEL-475 +* Thu Jun 15 2023 Python Maint - 7.2.5-3 +- Rebuilt for Python 3.12 -* Fri Jan 13 2023 Sergio Correia - 6.5.2-4 -- Backport upstream PR#1240 - logging: remove option to log into separate file - Resolves: rhbz#2154584 - keylime verifier is not logging to /var/log/keylime +* Tue Jun 06 2023 Sergio Correia - 7.2.5-2 +- Update test plan -* Thu Dec 1 2022 Sergio Correia - 6.5.2-3 -- Remove leftover policy file - Related: rhbz#2152135 +* Mon Jun 05 2023 Sergio Correia - 7.2.5-1 +- Updating for Keylime release v7.2.5 -* Thu Dec 1 2022 Patrik Koncity - 6.5.2-2 -- Use keylime selinux policy from upstream. - Resolves: rhbz#2152135 +* Fri Feb 03 2023 Sergio Correia - 6.6.0-1 +- Updating for Keylime release v6.6.0 -* Mon Nov 14 2022 Sergio Correia - 6.5.2-1 -- Update to 6.5.2 - Resolves: CVE-2022-3500 - Resolves: rhbz#2138167 - agent fails IMA attestation when one scripts is executed quickly after the other - Resolves: rhbz#2140670 - Segmentation fault in /usr/share/keylime/create_mb_refstate script - Resolves: rhbz#142009 - Registrar may crash during EK validation when require_ek_cert is enabled +* Wed Jan 25 2023 Sergio Correia - 6.5.3-2 +- e2e tests: do not change the tpm hash alg to sha256 -* Tue Sep 13 2022 Sergio Correia - 6.5.0-1 -- Update to 6.5.0 - Resolves: rhbz#2120686 - Keylime configuration is too complex +* Wed Jan 25 2023 Sergio Correia - 6.5.3-1 +- Updating for Keylime release v6.5.3 -* Fri Aug 26 2022 Sergio Correia - 6.4.3-1 -- Update to 6.4.3 - Resolves: rhbz#2121044 - Error parsing EK ASN.1 certificate of Nuvoton HW TPM +* Thu Jan 19 2023 Fedora Release Engineering - 6.4.3-8 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_38_Mass_Rebuild -* Fri Aug 26 2022 Patrik Koncity - 6.4.2-6 -- Update keylime SELinux policy -- Resolves: rhbz#2121058 +* Mon Dec 12 2022 Karel Srot - 6.4.3-7 +- Ignore non-keylime AVCs on Fedora Rawhide -* Fri Aug 26 2022 Patrik Koncity - 6.4.2-5 -- Update keylime SELinux policy and removed duplicate rules -- Resolves: rhbz#2121058 +* Fri Dec 09 2022 Sergio Correia - 6.4.3-6 +- Proper exception handling in tornado_requests -* Fri Aug 26 2022 Patrik Koncity - 6.4.2-4 -- Update keylime SELinux policy -- Resolves: rhbz#2121058 +* Fri Dec 09 2022 Sergio Correia - 6.4.3-5 +- Do not remove tag-repository.repo -* Wed Aug 17 2022 Patrik Koncity - 6.4.2-3 -- Add keylime-selinux policy as subpackage -- See https://fedoraproject.org/wiki/SELinux/IndependentPolicy -- Resolves: rhbz#2121058 +* Thu Dec 01 2022 Karel Srot - 6.4.3-4 +- Add dynamic_ref reference to e2e_tests.fmf -* Mon Jul 11 2022 Sergio Correia - 6.4.2-2 +* Tue Oct 25 2022 Patrik Koncity - 6.4.3-3 +- Add keylime selinux policy as subpackage and update CI + +* Wed Sep 14 2022 Sergio Correia - 6.4.3-2 +- Update tests branch to fedora-main + +* Thu Aug 25 2022 Sergio Correia - 6.4.3-1 +- Updating for Keylime release v6.4.3 + +* Thu Jul 21 2022 Fedora Release Engineering - 6.4.2-4 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_37_Mass_Rebuild + +* Mon Jul 11 2022 Sergio Correia - 6.4.2-3 +- Wrap efivar-libs dependency in a "ifarch %%efi" + +* Fri Jul 08 2022 Sergio Correia - 6.4.2-2 - Fix efivar-libs dependency - Related: rhbz#2082989 +- Some arches do not have efivar-libs, so let's require it conditionally. -* Thu Jul 07 2022 Sergio Correia - 6.4.2-1 -- Update to 6.4.2 - Related: rhbz#2082989 +* Fri Jul 08 2022 Sergio Correia - 6.4.2-1 +- Updating for Keylime release v6.4.2 +- Remove keylime-webapp and mark package as obsolete +- Configure tmpfiles.d +- Move common python dependencies to python3-keylime +- Change dependency from python3-gnupg to python3-gpg +- Use sysusers.d for handling user creation -* Tue Jun 21 2022 Sergio Correia - 6.4.1-1 -- Add keylime to RHEL-9 - Resolves: rhbz#2082989 +* Fri Jul 08 2022 Sergio Correia - 6.4.1-4 +- Adjust Fedora CI test plan as per upstream + +* Thu Jul 07 2022 Sergio Correia - 6.4.1-3 +- Opt in to rpmautospec + +* Mon Jun 13 2022 Python Maint - 6.4.1-2 +- Rebuilt for Python 3.11 + +* Mon Jun 06 2022 Sergio Correia - 6.4.1-1 +- Updating for Keylime release v6.4.1 + +* Wed May 04 2022 Sergio Correia - 6.4.0-1 +- Updating for Keylime release v6.4.0 + +* Wed Apr 06 2022 Sergio Correia - 6.3.2-1 +- Updating for Keylime release v6.3.2 + +* Mon Feb 14 2022 Sergio Correia - 6.3.1-1 +- Updating for Keylime release v6.3.1 + +* Tue Feb 08 2022 Sergio Correia - 6.0.3-4 +- Add Conflicts clauses for the subpackages + +* Mon Feb 07 2022 Sergio Correia - 6.3.0-3 +- Split keylime into subpackages + Related: rhbz#2045874 - Keylime subpackaging and agent alternatives + +* Thu Jan 27 2022 Sergio Correia - 6.3.0-2 +- Fix permissions of config file + +* Thu Jan 27 2022 Sergio Correia - 6.3.0-1 +- Updating for Keylime release v6.3.0 + +* Thu Jan 20 2022 Fedora Release Engineering - 6.1.0-5 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_36_Mass_Rebuild + +* Thu Jul 22 2021 Fedora Release Engineering - 6.1.0-4 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild + +* Fri Jun 04 2021 Python Maint - 6.1.0-3 +- Rebuilt for Python 3.10 + +* Thu Mar 25 2021 Luke Hinds 6.0.1-1 +- Updating for Keylime release v6.1.0 + +* Wed Mar 03 2021 Luke Hinds 6.0.1-1 +- Updating for Keylime release v6.0.1 + +* Tue Mar 02 2021 Zbigniew Jędrzejewski-Szmek - 6.0.0-2 +- Rebuilt for updated systemd-rpm-macros + See https://pagure.io/fesco/issue/2583. + +* Wed Feb 24 2021 Luke Hinds 6.0.0-1 +- Updating for Keylime release v6.0.0 + +* Tue Feb 02 2021 Luke Hinds 5.8.1-1 +- Updating for Keylime release v5.8.1 + +* Tue Jan 26 2021 Fedora Release Engineering - 5.8.0-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild + +* Sat Jan 23 2021 Luke Hinds 5.8.0-1 +- Updating for Keylime release v5.8.0 + +* Fri Jul 17 2020 Luke Hinds 5.7.2-1 +- Updating for Keylime release v5.7.2 + +* Tue May 26 2020 Miro Hrončok - 5.6.2-2 +- Rebuilt for Python 3.9 + +* Fri May 01 2020 Luke Hinds 5.6.2-1 +- Updating for Keylime release v5.6.2 + +* Thu Feb 06 2020 Luke Hinds 5.5.0-1 +- Updating for Keylime release v5.5.0 + +* Wed Jan 29 2020 Fedora Release Engineering - 5.4.1-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild + +* Thu Dec 12 2019 Luke Hinds 5.4.1-1 +– Initial Packaging + +## END: Generated by rpmautospec diff --git a/SOURCES/keylime.sysusers b/keylime.sysusers similarity index 100% rename from SOURCES/keylime.sysusers rename to keylime.sysusers diff --git a/SOURCES/keylime.tmpfiles b/keylime.tmpfiles similarity index 100% rename from SOURCES/keylime.tmpfiles rename to keylime.tmpfiles diff --git a/sources b/sources new file mode 100644 index 0000000..be30057 --- /dev/null +++ b/sources @@ -0,0 +1,2 @@ +SHA512 (keylime-selinux-42.1.2.tar.gz) = cb7b7b10d1d81af628a7ffdadc1be5af6d75851a44f58cff04edc575cbba1613447e56bfa1fb86660ec7c15e5fcf16ba51f2984094550ba3e08f8095b800b741 +SHA512 (v7.12.1.tar.gz) = c1297ebfc659102d73283255cfda4a977dfbff9bdd3748e05de405dadb70f752ad39aa5848edda9143d8ec620d07c21f1551fa4a914c99397620ab1682e58458