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>
587 lines
27 KiB
Diff
587 lines
27 KiB
Diff
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
|
|
|