diff --git a/examples/rejection-with-proof/chain.json b/examples/rejection-with-proof/chain.json index 6ee96aa..20a64dd 100644 --- a/examples/rejection-with-proof/chain.json +++ b/examples/rejection-with-proof/chain.json @@ -2,8 +2,8 @@ "chain": [ { "credential_id": "cred-0-orchestrator", - "issuer": "0aca4cf1251cb3648cd1f8605481d8dece5cd3a106a362a11b86f43d270da087", - "subject": "41eebfb4e9ac7f0944041b6c8fad485159f3d0391721dcec1bbe478986c6879f", + "issuer": "4b6b80f73e41b5401523db167775027ee5d41a1d83954557c905b00962cd5c8e", + "subject": "f5d7ff727931d0acb4393c9012d9a4fe9c3a0837a1c191a31e5ad1c35fa2dd07", "scope": [ "task:read", "task:write", @@ -12,12 +12,12 @@ ], "depth": 0, "parent_id": null, - "signature": "2380d30571a6493f6236c3aa9d381fd28a9cd3fcd4fa0fd5316166867575b206d8eb882c39fc7362c89cd407eef77848c228410a7816a10b563888eb99789a08" + "signature": "749c4a53497b1bb6ca17f4aafe6bad30674484baa4f8f3083581cf9bbbb950d5cb9e3a2ab0453b7cb9c24b339496f4eaa6a6d989960d356633aeb53c58251007" }, { "credential_id": "cred-1-researcher", - "issuer": "41eebfb4e9ac7f0944041b6c8fad485159f3d0391721dcec1bbe478986c6879f", - "subject": "cb42a0b6b9ec7839846a0ddbcb67d851d9b1013fc04066687181938d9ea7dce8", + "issuer": "f5d7ff727931d0acb4393c9012d9a4fe9c3a0837a1c191a31e5ad1c35fa2dd07", + "subject": "747e897486d4d32abd45d3a9e04e5357f6a88a1bad8d0ce9dc6084cd10c7b121", "scope": [ "task:read", "tool:purchase", @@ -25,18 +25,18 @@ ], "depth": 1, "parent_id": "cred-0-orchestrator", - "signature": "ad8d7b0aa00bd04e45bd9dca84f92bd7f23b4f4dde9e6eeff441ccf7c5222c5a0ccb45b5ba1b8399b6625cd3b4cfd97bc953b16bba01034a05ae01d55ba50503" + "signature": "0a5d95fded81c149bc652bfa5903c7a02e3caa9e0f5a41761ffef19d2dad9fd3e4ac5e1f2ef921bc92a6ed146aad0b3765709491447e7f87d9331df502e3af02" }, { "credential_id": "cred-2-retriever", - "issuer": "cb42a0b6b9ec7839846a0ddbcb67d851d9b1013fc04066687181938d9ea7dce8", - "subject": "2014e45331bb1717e9983dbba3fd4b6adc81ee48257afe5c0e027f12f61e8d7d", + "issuer": "747e897486d4d32abd45d3a9e04e5357f6a88a1bad8d0ce9dc6084cd10c7b121", + "subject": "b05e7f85927cc4a5886ec097da0200c4a02d0d944dd2d73c9a10f5662041074c", "scope": [ "tool:search" ], "depth": 2, "parent_id": "cred-1-researcher", - "signature": "0a18f20de83626e8f32960f3a3a543fed07b4c9f1ccce1eec4e3a6b7798311b68d08052ec408a675cd153fbfb9594d369c6333b672d2c1ba16854c517ebebf05" + "signature": "a6ec5c6c8add5923b9bf3f9fc9cb8cd0c10cf6faf53a3e04571a418f199d8191bbf9ee4f0b7ddd06f75eaeaa4e25e0a761e62ca59b09d31d6544b4bc90ce1e0d" } ] } diff --git a/examples/rejection-with-proof/dag.json b/examples/rejection-with-proof/dag.json index 76de619..5af7884 100644 --- a/examples/rejection-with-proof/dag.json +++ b/examples/rejection-with-proof/dag.json @@ -3,7 +3,7 @@ { "record_id": "rec-0-orchestrator", "credential_id": "cred-0-orchestrator", - "subject": "41eebfb4e9ac7f0944041b6c8fad485159f3d0391721dcec1bbe478986c6879f", + "subject": "f5d7ff727931d0acb4393c9012d9a4fe9c3a0837a1c191a31e5ad1c35fa2dd07", "scope": [ "task:read", "task:write", @@ -15,31 +15,31 @@ { "record_id": "rec-1-researcher", "credential_id": "cred-1-researcher", - "subject": "cb42a0b6b9ec7839846a0ddbcb67d851d9b1013fc04066687181938d9ea7dce8", + "subject": "747e897486d4d32abd45d3a9e04e5357f6a88a1bad8d0ce9dc6084cd10c7b121", "scope": [ "task:read", "tool:purchase", "tool:search" ], - "parent_record_hash": "143db9ccd89d3cf4c50426e5e8dedc32e7feac0bbc2735f63b605bba45eef613" + "parent_record_hash": "f8fd5e5073d51406bdc9fe7e79b90cd3c70afbee1ac47d5763996d4af20af5a7" }, { "record_id": "rec-2-retriever", "credential_id": "cred-2-retriever", - "subject": "2014e45331bb1717e9983dbba3fd4b6adc81ee48257afe5c0e027f12f61e8d7d", + "subject": "b05e7f85927cc4a5886ec097da0200c4a02d0d944dd2d73c9a10f5662041074c", "scope": [ "tool:search" ], - "parent_record_hash": "2e0ac019c2a7949a0cb5c8eab207ddef3351405db20dd4360144b6c4e605c6a0" + "parent_record_hash": "f940a2e460960035bbe39f93b7c76ac965d914857e10bce66bffce871fbc102e" }, { "record_id": "rec-denied-purchase", "credential_id": "cred-2-retriever", - "subject": "2014e45331bb1717e9983dbba3fd4b6adc81ee48257afe5c0e027f12f61e8d7d", + "subject": "b05e7f85927cc4a5886ec097da0200c4a02d0d944dd2d73c9a10f5662041074c", "scope": [ "tool:search" ], - "parent_record_hash": "c8ce42ebbbe084bbe88571a12576d8a4c1d5d3753d071e35dd19838274605f09", + "parent_record_hash": "3867abdd0574dce6cd20cae0b1e4a22afa5ab0a22778cda449c82c12dca4f127", "decision": "deny", "requested_capability": "tool:purchase", "effective_scope": [ diff --git a/pyproject.toml b/pyproject.toml index 47b0609..dc7927a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "agentrust-trace>=0.5", # Shared hardware-attestation verification (generic cert-chain verifier, # SNP/TDX/TPM primitives) consumed via PyPI instead of duplicated per repo. - "agent-manifest>=0.5", + "agent-manifest>=0.8", "rfc8785>=0.1", ] diff --git a/src/ca2a_verify/tpm.py b/src/ca2a_verify/tpm.py index e37a64c..1b15506 100644 --- a/src/ca2a_verify/tpm.py +++ b/src/ca2a_verify/tpm.py @@ -13,9 +13,11 @@ hardware-validated. Three divergent copies of one TPM verifier is the problem being retired; see cmcp#447. -What does live here is the piece agent-manifest does not model: ``TPMT_SIGNATURE``, -the wire format ``tpm2_quote -s`` and tpm2-pytss ``signature.marshal()`` actually -emit. agent-manifest takes a bare signature, so the envelope is unwrapped here. +``TPMT_SIGNATURE`` used to be unwrapped here too, because agent-manifest did not +model it. It does as of 0.8, so :func:`parse_tpmt_signature` is now a thin +delegation that keeps cA2A's error contract (:class:`AttestationFailed`) while the +wire format itself lives in one place. cmcp carried a byte-identical copy of the +same parse; both are retired. There is no single published TPM root, so the caller supplies the vendor roots it trusts. :mod:`ca2a_verify.tpm_roots` carries the one root validated on hardware as @@ -24,9 +26,8 @@ from __future__ import annotations -import struct -from dataclasses import dataclass - +from agent_manifest import ParsedSignature, TpmVerificationError +from agent_manifest import parse_tpmt_signature as _am_parse_tpmt_signature from cryptography import x509 from cryptography.hazmat.primitives.serialization import Encoding @@ -43,70 +44,26 @@ "verify_tpm_report", ] -# TPM2_ALG_ID values for the signing schemes a quote can use. -_ALG_RSASSA = 0x0014 -_ALG_RSAPSS = 0x0016 -_ALG_ECDSA = 0x0018 - - -@dataclass(frozen=True) -class ParsedSignature: - """A parsed ``TPMT_SIGNATURE``: the algorithm ids and the bare signature.""" - - sig_alg: int - hash_alg: int - signature: bytes +# ParsedSignature is re-exported from agent-manifest, which owns the layout. Its +# fields (sig_alg, hash_alg, signature) are unchanged from cA2A's former copy. def parse_tpmt_signature(blob: bytes) -> ParsedSignature: """Unwrap a ``TPMT_SIGNATURE`` into a bare signature. - Layout: ``sigAlg`` (2), ``hashAlg`` (2), then the algorithm-specific body. For - RSA that is a size-prefixed ``TPM2B_PUBLIC_KEY_RSA``. For ECDSA it is two - size-prefixed integers, R then S, which are re-encoded as a DER sequence - because that is what ``cryptography`` verifies against. + Delegates the layout to ``agent_manifest.parse_tpmt_signature`` and translates + its error into cA2A's, so callers keep catching :class:`AttestationFailed`. + The parse handles RSASSA/RSAPSS (a size-prefixed ``TPM2B_PUBLIC_KEY_RSA``) and + ECDSA (R and S as size-prefixed integers, re-encoded as a DER sequence). - Raises :class:`AttestationFailed` on anything malformed. + The upstream reason is passed through as the message rather than collapsed + into a generic one: "unsupported algorithm" and "truncated" are different + faults and a caller that cannot tell them apart cannot report usefully. """ - if len(blob) < 6: - raise AttestationFailed("TPMT_SIGNATURE too short") try: - sig_alg, hash_alg = struct.unpack_from(">HH", blob, 0) - offset = 4 - - if sig_alg in (_ALG_RSASSA, _ALG_RSAPSS): - (size,) = struct.unpack_from(">H", blob, offset) - offset += 2 - if len(blob) < offset + size: - raise AttestationFailed("TPMT_SIGNATURE truncated inside the RSA signature") - return ParsedSignature(sig_alg, hash_alg, blob[offset : offset + size]) - - if sig_alg == _ALG_ECDSA: - from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature - - parts: list[bytes] = [] - for _ in range(2): - (size,) = struct.unpack_from(">H", blob, offset) - offset += 2 - if len(blob) < offset + size: - raise AttestationFailed( - "TPMT_SIGNATURE truncated inside the ECDSA signature" - ) - parts.append(blob[offset : offset + size]) - offset += size - return ParsedSignature( - sig_alg, - hash_alg, - encode_dss_signature( - int.from_bytes(parts[0], "big"), int.from_bytes(parts[1], "big") - ), - ) - except struct.error as exc: - raise AttestationFailed("TPMT_SIGNATURE is malformed", detail=str(exc)) from exc - - raise AttestationFailed( - "unsupported TPM signature algorithm", detail=f"sigAlg={sig_alg:#06x}" - ) + return _am_parse_tpmt_signature(blob) + except TpmVerificationError as exc: + raise AttestationFailed(str(exc)) from exc def _delegate(