Add support for hashed qualifying data in IAK certification

Support both hashed and raw UUID used as qualifying data for IAK-based
AK certification.

Resolves: RHEL-169745

Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
This commit is contained in:
Anderson Toshiyuki Sasaki 2026-05-27 21:08:58 +02:00
parent ce7d5c46a7
commit 851a04ac9c
No known key found for this signature in database
2 changed files with 592 additions and 1 deletions

View File

@ -0,0 +1,586 @@
From 35a7699d08688c4111d01de18709820a6da6247e Mon Sep 17 00:00:00 2001
From: rpm-build <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 <ansasaki@redhat.com>
---
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

View File

@ -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