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:
parent
ce7d5c46a7
commit
851a04ac9c
586
0020-idevid-hash-before-certify.patch
Normal file
586
0020-idevid-hash-before-certify.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user