diff --git a/0020-idevid-hash-before-certify.patch b/0020-idevid-hash-before-certify.patch new file mode 100644 index 0000000..6117741 --- /dev/null +++ b/0020-idevid-hash-before-certify.patch @@ -0,0 +1,586 @@ +From 35a7699d08688c4111d01de18709820a6da6247e Mon Sep 17 00:00:00 2001 +From: rpm-build +Date: Wed, 27 May 2026 02:00:00 +0200 +Subject: [PATCH] Add support for hashed qualifying data for IAK certification + +Add support for both hashed or raw UUID used as input for IAK-based AK +certification. + +This allows long UUIDs to be used on systems with SHA-256-only TPMs. + +Backported from: https://github.com/keylime/keylime/pull/1911 + +Signed-off-by: Anderson Toshiyuki Sasaki +--- + docs/user_guide/idevid_and_iak.rst | 14 +- + keylime/api_version.py | 6 +- + keylime/models/registrar/registrar_agent.py | 1 + + keylime/tpm/tpm2_objects.py | 1 + + keylime/tpm/tpm_main.py | 51 +++++- + test/test_api_version.py | 6 +- + test/test_tpm_verify.py | 108 ++++++----- + test/test_tpm_verify_aik_with_iak.py | 190 ++++++++++++++++++++ + 8 files changed, 317 insertions(+), 60 deletions(-) + create mode 100644 test/test_tpm_verify_aik_with_iak.py + +diff --git a/docs/user_guide/idevid_and_iak.rst b/docs/user_guide/idevid_and_iak.rst +index 019d60f..3ee68b3 100644 +--- a/docs/user_guide/idevid_and_iak.rst ++++ b/docs/user_guide/idevid_and_iak.rst +@@ -60,6 +60,18 @@ H-5 ECC SM2 P256 SM3_256 + ========== =============== ========== + + +-.. [#] IEEE Standard for Local and Metropolitan Area Networks - Secure Device Identity, https://standards.ieee.org/standard/802_1AR-2018.html ++Qualifying Data in AK Certification (API v2.6+) ++^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ++ ++Starting with API version 2.6, the agent hashes the agent ID using SHA-256 ++before passing it as ``qualifyingData`` to ``TPM2_Certify`` when certifying ++the AK with the IAK. This ensures the qualifying data fits within the ++``TPM2B_DATA`` size limit on TPMs that only support SHA-256 (where the limit ++is 34 bytes), which is smaller than a typical UUID string (36 bytes). ++ ++The registrar accepts both the hashed format (from agents using API v2.6+) and ++the raw agent ID format (from older agents) to maintain backward compatibility. ++ ++.. [#] IEEE Standard for Local and Metropolitan Area Networks - Secure Device Identity, https://standards.ieee.org/standard/802_1AR-2018.html + .. [#tcg] TPM 2.0 Keys for Device Identity and Attestation, https://trustedcomputinggroup.org/wp-content/uploads/TPM-2p0-Keys-for-Device-Identity-and-Attestation_v1_r12_pub10082021.pdf + +diff --git a/keylime/api_version.py b/keylime/api_version.py +index 6a59d85..df65736 100644 +--- a/keylime/api_version.py ++++ b/keylime/api_version.py +@@ -6,9 +6,9 @@ from packaging import version + + VersionType = Union[int, float, str] + +-CURRENT_VERSION: str = "2.5" +-VERSIONS: List[str] = ["1.0", "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "3.0"] +-LATEST_VERSIONS: Dict[str, str] = {"1": "1.0", "2": "2.5", "3": "3.0"} ++CURRENT_VERSION: str = "2.6" ++VERSIONS: List[str] = ["1.0", "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "3.0"] ++LATEST_VERSIONS: Dict[str, str] = {"1": "1.0", "2": "2.6", "3": "3.0"} + DEPRECATED_VERSIONS: List[str] = ["1.0"] + + +diff --git a/keylime/models/registrar/registrar_agent.py b/keylime/models/registrar/registrar_agent.py +index 5319b89..9894f45 100644 +--- a/keylime/models/registrar/registrar_agent.py ++++ b/keylime/models/registrar/registrar_agent.py +@@ -290,6 +290,7 @@ class RegistrarAgent(PersistableModel): + # If the iak_attest and iak_sign values are missing, treat this as an error + if not iak_attest or not iak_sign: + self._add_error("aik_tpm", "cannot be bound to the IAK because of a missing 'iak_attest' or 'iak_sign'") ++ return + + # Decode Base64 values to binary TPM structures + iak_attest = base64.b64decode(iak_attest) +diff --git a/keylime/tpm/tpm2_objects.py b/keylime/tpm/tpm2_objects.py +index d33ebaa..2bebd12 100644 +--- a/keylime/tpm/tpm2_objects.py ++++ b/keylime/tpm/tpm2_objects.py +@@ -59,6 +59,7 @@ TPM_ALG_ECDSA = 0x0018 + + TPM_GENERATED_VALUE = 0xFF544347 + ++TPM_ST_ATTEST_CERTIFY = 0x8017 + TPM_ST_ATTEST_QUOTE = 0x8018 + + # These are the object attribute values important for EK certs +diff --git a/keylime/tpm/tpm_main.py b/keylime/tpm/tpm_main.py +index 0f68c26..e9f3803 100644 +--- a/keylime/tpm/tpm_main.py ++++ b/keylime/tpm/tpm_main.py +@@ -1,4 +1,6 @@ + import base64 ++import hashlib ++import hmac + import struct + import zlib + from typing import Any, Dict, List, Optional, Set, Tuple, Union +@@ -99,6 +101,14 @@ class Tpm: + if not isinstance(pub, (RSAPublicKey, EllipticCurvePublicKey)): + raise ValueError(f"Unsupported key type {type(pub).__name__}") + ++ # Validate magic number and structure type before parsing ++ try: ++ magic, attest_type = struct.unpack_from(">IH", attest) ++ except struct.error as exc: ++ raise ObjectNameMismatch("malformed TPM2B_ATTEST structure") from exc ++ if magic != tpm2_objects.TPM_GENERATED_VALUE or attest_type != tpm2_objects.TPM_ST_ATTEST_CERTIFY: ++ raise ObjectNameMismatch("invalid magic or structure type in TPM2B_ATTEST") ++ + # Skip over qualifiedSigner field, so that we can locate the qualifying data which comes after + _key_name, rest = Tpm._unpackv(attest, 6) + # Extract buffer from extraData +@@ -109,7 +119,13 @@ class Tpm: + raise QualifyingDataMismatch("qualifying data does not match TPM2B_ATTEST structure") + + # Check object is present in attest structure +- if tpm2_objects.get_tpm2b_public_name(tpm_object) != rest[27:61].hex(): ++ # Skip clockInfo(17) + firmwareVersion(8) to reach TPMS_CERTIFY_INFO, ++ # then extract the TPM2B_NAME (variable-length, depends on name algorithm) ++ try: ++ certify_name, _ = Tpm._unpackv(rest, 25) ++ except struct.error as exc: ++ raise ObjectNameMismatch("malformed TPMS_CERTIFY_INFO in TPM2B_ATTEST structure") from exc ++ if tpm2_objects.get_tpm2b_public_name(tpm_object) != certify_name.hex(): + raise ObjectNameMismatch("name of TPM object not found in TPM2B_ATTEST structure") + + # Calculate digest from attest info +@@ -162,17 +178,38 @@ class Tpm: + + @staticmethod + def verify_aik_with_iak(uuid: str, aik_tpm: bytes, iak_tpm: bytes, iak_attest: bytes, iak_sign: bytes) -> bool: +- attest_body = iak_attest.split(b"\x00$")[1] ++ try: ++ magic, attest_type = struct.unpack_from(">IH", iak_attest) ++ if magic != tpm2_objects.TPM_GENERATED_VALUE or attest_type != tpm2_objects.TPM_ST_ATTEST_CERTIFY: ++ logger.warning("Agent %s AIK verification failed, invalid magic or structure is not CERTIFY", uuid) ++ return False ++ _qualified_signer, rest = Tpm._unpackv(iak_attest, 6) ++ extra_data, rest = Tpm._unpackv(rest) ++ except struct.error: ++ logger.warning("Agent %s AIK verification failed, malformed IAK attestation structure", uuid) ++ return False + iak_pub = tpm2_objects.pubkey_from_tpm2b_public(iak_tpm) + +- # check UUID in certify matches UUID registering +- if attest_body[: len(uuid)] != bytes(uuid, "utf-8"): +- logger.warning("Agent %s AIK verification failed, uuid does not match attest info", uuid) ++ # check qualifying data: accept SHA-256 hash of UUID (new agents) or raw UUID (old agents) ++ expected_hashed = hashlib.sha256(uuid.encode("utf-8")).digest() ++ if hmac.compare_digest(extra_data, expected_hashed): ++ pass ++ elif hmac.compare_digest(extra_data, uuid.encode("utf-8")): ++ logger.info("Agent %s uses raw UUID as qualifying data (pre-2.6 format)", uuid) ++ else: ++ logger.warning("Agent %s AIK verification failed, qualifying data does not match agent ID", uuid) + return False + + # check aik in certify matches aik being registered +- if tpm2_objects.get_tpm2b_public_name(aik_tpm) != attest_body[len(uuid) + 27 : len(uuid) + 61].hex(): +- logger.warning(" Agent %s AIK verification failed, name of aik does not match attest info", uuid) ++ # Skip clockInfo(17) + firmwareVersion(8) to reach TPMS_CERTIFY_INFO, ++ # then extract the TPM2B_NAME (variable-length, depends on name algorithm) ++ try: ++ certify_name, _ = Tpm._unpackv(rest, 25) ++ except struct.error: ++ logger.warning("Agent %s AIK verification failed, malformed TPMS_CERTIFY_INFO", uuid) ++ return False ++ if tpm2_objects.get_tpm2b_public_name(aik_tpm) != certify_name.hex(): ++ logger.warning("Agent %s AIK verification failed, name of aik does not match attest info", uuid) + return False + + # generate digest of attest info +diff --git a/test/test_api_version.py b/test/test_api_version.py +index f87dcf4..75ba669 100644 +--- a/test/test_api_version.py ++++ b/test/test_api_version.py +@@ -8,7 +8,7 @@ class APIVersion_Test(unittest.TestCase): + + def test_current_version(self): + """Test current_version.""" +- self.assertEqual(api_version.current_version(), "2.5", "Current version is 2.5") ++ self.assertEqual(api_version.current_version(), "2.6", "Current version is 2.6") + + def test_latest_minor_version(self): + """Test laster_minor_version.""" +@@ -83,8 +83,8 @@ class APIVersion_Test(unittest.TestCase): + def test_negotiate_version_returns_highest(self): + """Test that negotiate_version returns the highest common version.""" + # All versions supported by both +- result = api_version.negotiate_version(["1.0", "2.0", "2.1", "2.2", "2.3", "2.4", "2.5"]) +- self.assertEqual(result, "2.5") ++ result = api_version.negotiate_version(["1.0", "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6"]) ++ self.assertEqual(result, "2.6") + + # Only lower versions in common + result = api_version.negotiate_version(["1.0", "2.0"]) +diff --git a/test/test_tpm_verify.py b/test/test_tpm_verify.py +index bf0cbd0..c2f249a 100644 +--- a/test/test_tpm_verify.py ++++ b/test/test_tpm_verify.py +@@ -1,5 +1,6 @@ + """Unit tests for Tpm.verify_tpm_object() function.""" + ++import struct + import unittest + from unittest.mock import patch + +@@ -10,31 +11,34 @@ from cryptography.hazmat.primitives.asymmetric import rsa + from keylime.tpm.errors import IncorrectSignature, ObjectNameMismatch, QualifyingDataMismatch + from keylime.tpm.tpm_main import Tpm + ++# 34-byte AK name: nameAlg(2) + SHA-256 digest(32) ++_AK_NAME = b"\x00\x0b" + b"\xaa" * 32 ++ ++ ++def _build_certify_attest(extra_data: bytes, certify_name: bytes) -> bytes: ++ """Build a minimal TPMS_ATTEST structure for TPM_ST_ATTEST_CERTIFY.""" ++ header = b"\xff\x54\x43\x47" + b"\x80\x17" ++ qualified_signer = struct.pack(">H", 4) + b"\x00" * 4 ++ extra_data_field = struct.pack(">H", len(extra_data)) + extra_data ++ clock_info = b"\x00" * 17 ++ firmware_version = b"\x00" * 8 ++ name_field = struct.pack(">H", len(certify_name)) + certify_name ++ qualified_name = struct.pack(">H", 2) + b"\x00\x00" ++ return header + qualified_signer + extra_data_field + clock_info + firmware_version + name_field + qualified_name ++ + + class TestTpmVerifyObject(unittest.TestCase): + """Test cases for Tpm.verify_tpm_object() error handling.""" + + def test_qualifying_data_mismatch_exception(self): + """Test that QualifyingDataMismatch is raised when qualifying data doesn't match.""" +- # Generate a real RSA key for testing + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + public_key = private_key.public_key() + +- # Create minimal test data + tpm_object = b"\x00\x01" + b"\x00" * 100 + key = b"\x00\x01" + b"\x00" * 100 +- # Create a minimal attest structure with mismatched qualifying data +- # Structure: magic(4) + type(2) + qualifiedSigner_size(2) + qualifiedSigner + extraData_size(2) + extraData + ... +- attest = ( +- b"\xff\x54\x43\x47" # TPM_GENERATED magic +- + b"\x00\x17" # TPM_ST_ATTEST_CERTIFY +- + b"\x00\x04" # qualifiedSigner size = 4 +- + b"\x00\x00\x00\x00" # qualifiedSigner data +- + b"\x00\x04" # extraData size = 4 +- + b"\x11\x22\x33\x44" # extraData (qualifying data in attest) +- + b"\x00" * 100 # rest of structure +- ) +- sig = b"\x00\x14" + b"\x00\x0b" + b"\x00\x20" + b"\x00" * 100 # Minimal signature structure ++ attest = _build_certify_attest(b"\x11\x22\x33\x44", _AK_NAME) ++ sig = b"\x00\x14" + b"\x00\x0b" + b"\x00\x20" + b"\x00" * 100 + qual = b"\x99\x88\x77\x66" # Different from what's in attest + + with patch("keylime.tpm.tpm2_objects.pubkey_from_tpm2b_public") as mock_pubkey: +@@ -47,25 +51,14 @@ class TestTpmVerifyObject(unittest.TestCase): + + def test_object_name_mismatch_exception(self): + """Test that ObjectNameMismatch is raised when object name doesn't match.""" +- # Generate a real RSA key for testing + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + public_key = private_key.public_key() + + tpm_object = b"\x00\x01" + b"\x00" * 100 + key = b"\x00\x01" + b"\x00" * 100 +- +- # Create attest structure where object name won't match +- attest = ( +- b"\xff\x54\x43\x47" # TPM_GENERATED magic +- + b"\x00\x17" # TPM_ST_ATTEST_CERTIFY +- + b"\x00\x04" # qualifiedSigner size +- + b"\x00\x00\x00\x00" # qualifiedSigner +- + b"\x00\x04" # extraData size +- + b"\x11\x22\x33\x44" # extraData (matching qual) +- + b"\x00" * 100 # rest including object name field +- ) ++ qual = b"\x11\x22\x33\x44" ++ attest = _build_certify_attest(qual, _AK_NAME) + sig = b"\x00\x14" + b"\x00\x0b" + b"\x00\x20" + b"\x00" * 100 +- qual = b"\x11\x22\x33\x44" # Matching qualifying data + + with patch("keylime.tpm.tpm2_objects.pubkey_from_tpm2b_public") as mock_pubkey: + with patch("keylime.tpm.tpm2_objects.get_tpm2b_public_name") as mock_name: +@@ -79,41 +72,27 @@ class TestTpmVerifyObject(unittest.TestCase): + + def test_incorrect_signature_exception(self): + """Test that IncorrectSignature is raised when signature verification fails.""" +- # Generate a real RSA key for testing + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + public_key = private_key.public_key() + + tpm_object = b"\x00\x01" + b"\x00" * 100 + key = b"\x00\x01" + b"\x00" * 100 +- +- # Create a valid-looking attest structure +- attest = ( +- b"\xff\x54\x43\x47" +- + b"\x00\x17" +- + b"\x00\x04" +- + b"\x00\x00\x00\x00" +- + b"\x00\x04" +- + b"\x11\x22\x33\x44" +- + b"\x00" * 100 +- ) +- # Create signature structure with wrong signature data ++ qual = b"\x11\x22\x33\x44" ++ attest = _build_certify_attest(qual, _AK_NAME) + sig = ( + b"\x00\x14" # TPM_ALG_RSASSA + + b"\x00\x0b" # TPM_ALG_SHA256 + + b"\x01\x00" # signature size = 256 +- + b"\x00" * 256 # Invalid signature bytes ++ + b"\x00" * 256 + ) +- qual = b"\x11\x22\x33\x44" + + with patch("keylime.tpm.tpm2_objects.pubkey_from_tpm2b_public") as mock_pubkey: + with patch("keylime.tpm.tpm2_objects.get_tpm2b_public_name") as mock_name: + with patch("keylime.tpm.tpm_util.crypt_hash") as mock_hash: + mock_pubkey.return_value = public_key +- # Make name check pass by returning matching hash +- mock_name.return_value = "00" * 34 # Will match attest[offset:offset+34] ++ mock_name.return_value = _AK_NAME.hex() + mock_hash.return_value = (b"digest_data", hashes.SHA256()) + +- # Mock verify to raise InvalidSignature + with patch("keylime.tpm.tpm_util.verify") as mock_verify: + mock_verify.side_effect = InvalidSignature("Signature verification failed") + +@@ -130,7 +109,6 @@ class TestTpmVerifyObject(unittest.TestCase): + sig = b"\x00" * 100 + + with patch("keylime.tpm.tpm2_objects.pubkey_from_tpm2b_public") as mock_pubkey: +- # Return an unsupported key type + mock_pubkey.return_value = "not_a_supported_key_type" + + with self.assertRaises(ValueError) as context: +@@ -138,6 +116,44 @@ class TestTpmVerifyObject(unittest.TestCase): + + self.assertIn("Unsupported key type", str(context.exception)) + ++ def test_invalid_magic_raises(self): ++ """Test that ObjectNameMismatch is raised when the magic number is wrong.""" ++ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) ++ public_key = private_key.public_key() ++ ++ key = b"\x00\x01" + b"\x00" * 100 ++ tpm_object = b"\x00\x01" + b"\x00" * 100 ++ # Corrupt the magic number (first 4 bytes) ++ bad_attest = b"\xde\xad\xbe\xef" + b"\x80\x17" + b"\x00" * 50 ++ sig = b"\x00\x14" + b"\x00\x0b" + b"\x00\x20" + b"\x00" * 100 ++ ++ with patch("keylime.tpm.tpm2_objects.pubkey_from_tpm2b_public") as mock_pubkey: ++ mock_pubkey.return_value = public_key ++ ++ with self.assertRaises(ObjectNameMismatch) as context: ++ Tpm.verify_tpm_object(tpm_object, key, bad_attest, sig) ++ ++ self.assertIn("invalid magic", str(context.exception)) ++ ++ def test_wrong_structure_type_raises(self): ++ """Test that ObjectNameMismatch is raised when the structure type is not CERTIFY.""" ++ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) ++ public_key = private_key.public_key() ++ ++ key = b"\x00\x01" + b"\x00" * 100 ++ tpm_object = b"\x00\x01" + b"\x00" * 100 ++ # Valid magic but wrong type: 0x8018 = TPM_ST_ATTEST_QUOTE ++ bad_attest = b"\xff\x54\x43\x47" + b"\x80\x18" + b"\x00" * 50 ++ sig = b"\x00\x14" + b"\x00\x0b" + b"\x00\x20" + b"\x00" * 100 ++ ++ with patch("keylime.tpm.tpm2_objects.pubkey_from_tpm2b_public") as mock_pubkey: ++ mock_pubkey.return_value = public_key ++ ++ with self.assertRaises(ObjectNameMismatch) as context: ++ Tpm.verify_tpm_object(tpm_object, key, bad_attest, sig) ++ ++ self.assertIn("invalid magic", str(context.exception)) ++ + + if __name__ == "__main__": + unittest.main() +diff --git a/test/test_tpm_verify_aik_with_iak.py b/test/test_tpm_verify_aik_with_iak.py +new file mode 100644 +index 0000000..53b7b8c +--- /dev/null ++++ b/test/test_tpm_verify_aik_with_iak.py +@@ -0,0 +1,190 @@ ++"""Unit tests for Tpm.verify_aik_with_iak() function.""" ++ ++import hashlib ++import struct ++import unittest ++from unittest.mock import patch ++ ++from cryptography.hazmat.primitives import hashes ++from cryptography.hazmat.primitives.asymmetric import rsa ++ ++from keylime.tpm import tpm2_objects ++from keylime.tpm.tpm_main import Tpm ++ ++ ++def _build_attest(extra_data: bytes, ak_name: bytes) -> bytes: ++ """Build a minimal TPMS_ATTEST structure for TPM2_Certify. ++ ++ Layout: ++ magic(4) + type(2) = 6-byte header ++ qualifiedSigner: TPM2B_NAME (2-byte size + data) ++ extraData: TPM2B_DATA (2-byte size + data) ++ clockInfo: TPMS_CLOCK_INFO (17 bytes: clock(8) + resetCount(4) + restartCount(4) + safe(1)) ++ firmwareVersion(8) ++ certifyInfo: TPMS_CERTIFY_INFO = TPM2B_NAME(qualifiedName) + TPM2B_NAME(name) ++ """ ++ header = b"\xff\x54\x43\x47" + b"\x80\x17" # magic + TPM_ST_ATTEST_CERTIFY ++ qualified_signer = struct.pack(">H", 4) + b"\x00" * 4 ++ extra_data_field = struct.pack(">H", len(extra_data)) + extra_data ++ clock_info = b"\x00" * 17 ++ firmware_version = b"\x00" * 8 ++ # TPMS_CERTIFY_INFO: name (TPM2B_NAME) then qualifiedName (TPM2B_NAME) ++ name_field = struct.pack(">H", len(ak_name)) + ak_name ++ qualified_name = struct.pack(">H", 2) + b"\x00\x00" ++ certify_info = name_field + qualified_name ++ ++ return header + qualified_signer + extra_data_field + clock_info + firmware_version + certify_info ++ ++ ++def _build_sig(sig_alg: int, hash_alg: int, signature: bytes) -> bytes: ++ """Build a minimal TPMT_SIGNATURE structure for RSASSA.""" ++ return struct.pack(">HHH", sig_alg, hash_alg, len(signature)) + signature ++ ++ ++# Fixed 34-byte AK name (nameAlg(2) + SHA-256(32)) ++AK_NAME = b"\x00\x0b" + b"\xaa" * 32 ++AK_NAME_HEX = AK_NAME.hex() ++ ++ ++class TestVerifyAikWithIak(unittest.TestCase): ++ """Test cases for Tpm.verify_aik_with_iak() qualifying data handling.""" ++ ++ def setUp(self): ++ self.uuid = "d432fbb3-d2f1-4a97-9ef7-75bd81c00000" ++ self.private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) ++ self.public_key = self.private_key.public_key() ++ self.aik_tpm = b"\x00\x01" + b"\x00" * 100 ++ self.iak_tpm = b"\x00\x01" + b"\x00" * 100 ++ self.fake_sig = b"\x00" * 256 ++ ++ def _run_verify(self, extra_data: bytes) -> bool: ++ """Run verify_aik_with_iak with the given extra_data, mocking internals.""" ++ attest = _build_attest(extra_data, AK_NAME) ++ sig = _build_sig(tpm2_objects.TPM_ALG_RSASSA, tpm2_objects.TPM_ALG_SHA256, self.fake_sig) ++ ++ with ( ++ patch("keylime.tpm.tpm2_objects.pubkey_from_tpm2b_public") as mock_pubkey, ++ patch("keylime.tpm.tpm2_objects.get_tpm2b_public_name") as mock_name, ++ patch("keylime.tpm.tpm_util.crypt_hash") as mock_hash, ++ patch("keylime.tpm.tpm_util.verify") as mock_verify, ++ ): ++ mock_pubkey.return_value = self.public_key ++ mock_name.return_value = AK_NAME_HEX ++ mock_hash.return_value = (b"\x00" * 32, hashes.SHA256()) ++ mock_verify.return_value = None ++ ++ return Tpm.verify_aik_with_iak(self.uuid, self.aik_tpm, self.iak_tpm, attest, sig) ++ ++ def test_hashed_qualifying_data(self): ++ """New agents (>= 2.6) send SHA-256(uuid) as qualifying data.""" ++ extra_data = hashlib.sha256(self.uuid.encode("utf-8")).digest() ++ self.assertTrue(self._run_verify(extra_data)) ++ ++ def test_raw_qualifying_data(self): ++ """Old agents (< 2.6) send raw uuid bytes as qualifying data.""" ++ extra_data = self.uuid.encode("utf-8") ++ self.assertTrue(self._run_verify(extra_data)) ++ ++ def test_mismatched_qualifying_data(self): ++ """Qualifying data that matches neither hash nor raw should fail.""" ++ extra_data = b"wrong-qualifying-data" ++ self.assertFalse(self._run_verify(extra_data)) ++ ++ def test_wrong_ak_name(self): ++ """Verification should fail when AK name doesn't match.""" ++ extra_data = hashlib.sha256(self.uuid.encode("utf-8")).digest() ++ wrong_ak_name = b"\x00\x0b" + b"\xbb" * 32 ++ attest = _build_attest(extra_data, wrong_ak_name) ++ sig = _build_sig(tpm2_objects.TPM_ALG_RSASSA, tpm2_objects.TPM_ALG_SHA256, self.fake_sig) ++ ++ with ( ++ patch("keylime.tpm.tpm2_objects.pubkey_from_tpm2b_public") as mock_pubkey, ++ patch("keylime.tpm.tpm2_objects.get_tpm2b_public_name") as mock_name, ++ ): ++ mock_pubkey.return_value = self.public_key ++ mock_name.return_value = AK_NAME_HEX # expects the standard AK_NAME ++ ++ result = Tpm.verify_aik_with_iak(self.uuid, self.aik_tpm, self.iak_tpm, attest, sig) ++ ++ self.assertFalse(result) ++ ++ def test_long_agent_id_hashed(self): ++ """Agent IDs longer than 34 bytes work when hashed.""" ++ self.uuid = "a" * 100 ++ extra_data = hashlib.sha256(self.uuid.encode("utf-8")).digest() ++ self.assertTrue(self._run_verify(extra_data)) ++ ++ def test_long_raw_qualifying_data_accepted(self): ++ """Raw qualifying data longer than 34 bytes is accepted if it matches the agent ID. ++ ++ In practice, a pre-2.6 agent with an ID longer than 34 bytes would fail ++ at the TPM level (TPM_RC_SIZE) and never reach the registrar. But the ++ registrar correctly accepts the exact match via the old-format fallback. ++ """ ++ self.uuid = "d432fbb3-d2f1-4a97-9ef7-75bd81c00000-extra-suffix" ++ extra_data = self.uuid.encode("utf-8") ++ self.assertTrue(self._run_verify(extra_data)) ++ ++ def test_raw_qualifying_data_prefix_match_rejected(self): ++ """Raw qualifying data whose prefix matches the UUID must not pass. ++ ++ This verifies that the comparison is exact: if the extra_data in ++ the attest structure starts with the UUID bytes but has additional ++ trailing data, it must be rejected. ++ """ ++ extra_data = self.uuid.encode("utf-8") + b"\x00extra-garbage" ++ self.assertFalse(self._run_verify(extra_data)) ++ ++ def test_sha384_ak_name(self): ++ """AK name using SHA-384 (50 bytes) should be parsed correctly.""" ++ ak_name_384 = b"\x00\x0c" + b"\xcc" * 48 ++ ak_name_384_hex = ak_name_384.hex() ++ extra_data = hashlib.sha256(self.uuid.encode("utf-8")).digest() ++ attest = _build_attest(extra_data, ak_name_384) ++ sig = _build_sig(tpm2_objects.TPM_ALG_RSASSA, tpm2_objects.TPM_ALG_SHA256, self.fake_sig) ++ ++ with ( ++ patch("keylime.tpm.tpm2_objects.pubkey_from_tpm2b_public") as mock_pubkey, ++ patch("keylime.tpm.tpm2_objects.get_tpm2b_public_name") as mock_name, ++ patch("keylime.tpm.tpm_util.crypt_hash") as mock_hash, ++ patch("keylime.tpm.tpm_util.verify") as mock_verify, ++ ): ++ mock_pubkey.return_value = self.public_key ++ mock_name.return_value = ak_name_384_hex ++ mock_hash.return_value = (b"\x00" * 32, hashes.SHA256()) ++ mock_verify.return_value = None ++ ++ result = Tpm.verify_aik_with_iak(self.uuid, self.aik_tpm, self.iak_tpm, attest, sig) ++ ++ self.assertTrue(result) ++ ++ def test_malformed_attest_rejected(self): ++ """Malformed iak_attest that causes struct.error should return False.""" ++ malformed_attest = b"\xff\x54\x43\x47\x80\x17\xff\xff" ++ sig = _build_sig(tpm2_objects.TPM_ALG_RSASSA, tpm2_objects.TPM_ALG_SHA256, self.fake_sig) ++ ++ result = Tpm.verify_aik_with_iak(self.uuid, self.aik_tpm, self.iak_tpm, malformed_attest, sig) ++ ++ self.assertFalse(result) ++ ++ def test_invalid_magic_rejected(self): ++ """iak_attest with a bad magic number should return False.""" ++ bad_attest = b"\xde\xad\xbe\xef" + b"\x80\x17" + b"\x00" * 50 ++ sig = _build_sig(tpm2_objects.TPM_ALG_RSASSA, tpm2_objects.TPM_ALG_SHA256, self.fake_sig) ++ ++ result = Tpm.verify_aik_with_iak(self.uuid, self.aik_tpm, self.iak_tpm, bad_attest, sig) ++ ++ self.assertFalse(result) ++ ++ def test_wrong_structure_type_rejected(self): ++ """iak_attest with a non-CERTIFY structure type (e.g. QUOTE) should return False.""" ++ bad_attest = b"\xff\x54\x43\x47" + b"\x80\x18" + b"\x00" * 50 ++ sig = _build_sig(tpm2_objects.TPM_ALG_RSASSA, tpm2_objects.TPM_ALG_SHA256, self.fake_sig) ++ ++ result = Tpm.verify_aik_with_iak(self.uuid, self.aik_tpm, self.iak_tpm, bad_attest, sig) ++ ++ self.assertFalse(result) ++ ++ ++if __name__ == "__main__": ++ unittest.main() +-- +2.54.0 + diff --git a/keylime.spec b/keylime.spec index bbdd48d..91e1f9b 100644 --- a/keylime.spec +++ b/keylime.spec @@ -9,7 +9,7 @@ Name: keylime Version: 7.14.1 -Release: 6%{?dist} +Release: 7%{?dist} Summary: Open source TPM software for Bootstrapping and Maintaining Trust URL: https://github.com/keylime/keylime @@ -54,6 +54,11 @@ Patch: 0017-verifier-graceful-shutdown.patch Patch: 0018-ignore-sigterm-sigint-manager-parent-processes.patch Patch: 0019-move-socket-var-run.patch +# RHEL-169745 - Add support for hashed qualifying data for IAK-based AK +# certification +# Backport: https://github.com/keylime/keylime/pull/1911 +Patch: 0020-idevid-hash-before-certify.patch + # Main program: Apache-2.0 # Icons: MIT License: Apache-2.0 AND MIT