From 78d0d31f7bf4cb2dc3278d556085b1fe215bf959 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Sat, 1 Aug 2026 21:37:13 -0700 Subject: [PATCH] refactor(tee): consume the shared SNP report layout from agent-manifest 0.10 Phase B2 of the TEE consolidation, matching cmcp#466. The SEV-SNP ABI table and parse now come from agent-manifest, so cA2A stops defining its own. Between them the org carried four mirrors of one ABI table, two of them inside cmcp alone. This removes the last one. The offsets cA2A re-exports (REPORT_SIZE, SIG_OFFSET, REPORT_DATA_OFFSET, MEASUREMENT_OFFSET) are now derived from the shared table rather than declared locally. SevSnpReport stays as cA2A's public surface and keeps its error contract and the signature_rs helper; only the byte reading moves. Callers still catch AttestationFailed, and SnpVerificationError is translated at the boundary, the same idiom used for parse_tpmt_signature and verify_tpm_quote. Verified against the published wheel rather than an editable install: 251 passed, 3 skipped with agent-manifest 0.10.0 resolved from PyPI. Co-Authored-By: Claude Opus 5 (1M context) --- examples/rejection-with-proof/chain.json | 18 +++---- examples/rejection-with-proof/dag.json | 14 ++--- pyproject.toml | 2 +- src/ca2a_runtime/tee/sev_snp.py | 65 +++++++++++++----------- 4 files changed, 52 insertions(+), 47 deletions(-) diff --git a/examples/rejection-with-proof/chain.json b/examples/rejection-with-proof/chain.json index 20a64dd..4993281 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": "4b6b80f73e41b5401523db167775027ee5d41a1d83954557c905b00962cd5c8e", - "subject": "f5d7ff727931d0acb4393c9012d9a4fe9c3a0837a1c191a31e5ad1c35fa2dd07", + "issuer": "5d950cd9fea67de730ac35d540c48d62a22dada5618db0c8474b5eae5562a7cf", + "subject": "2f35acc0747fb45e739288d02bbd18e54e5283f770e30391b1c272fc1285e3bf", "scope": [ "task:read", "task:write", @@ -12,12 +12,12 @@ ], "depth": 0, "parent_id": null, - "signature": "749c4a53497b1bb6ca17f4aafe6bad30674484baa4f8f3083581cf9bbbb950d5cb9e3a2ab0453b7cb9c24b339496f4eaa6a6d989960d356633aeb53c58251007" + "signature": "21056267f494e8ac773bf9aa76b23815baded9b17605c265296f719241050a76b991cbd0162385359302607f19483d81cacc971f8d3cdbf1bc3d8c4cca778d0e" }, { "credential_id": "cred-1-researcher", - "issuer": "f5d7ff727931d0acb4393c9012d9a4fe9c3a0837a1c191a31e5ad1c35fa2dd07", - "subject": "747e897486d4d32abd45d3a9e04e5357f6a88a1bad8d0ce9dc6084cd10c7b121", + "issuer": "2f35acc0747fb45e739288d02bbd18e54e5283f770e30391b1c272fc1285e3bf", + "subject": "b12a13df30c7123fe6ab8e03551d7c7465cb1acfa04701e0efb05eeba9a2b7d6", "scope": [ "task:read", "tool:purchase", @@ -25,18 +25,18 @@ ], "depth": 1, "parent_id": "cred-0-orchestrator", - "signature": "0a5d95fded81c149bc652bfa5903c7a02e3caa9e0f5a41761ffef19d2dad9fd3e4ac5e1f2ef921bc92a6ed146aad0b3765709491447e7f87d9331df502e3af02" + "signature": "308b8c1cab0b49bb1993f185b67515adf832bf50a8cc9ddbf7e9e4d785cac9f3ab20a73be8cef332e28b409fc67a904eef4955ac0b1fcded39734303f0e0bf01" }, { "credential_id": "cred-2-retriever", - "issuer": "747e897486d4d32abd45d3a9e04e5357f6a88a1bad8d0ce9dc6084cd10c7b121", - "subject": "b05e7f85927cc4a5886ec097da0200c4a02d0d944dd2d73c9a10f5662041074c", + "issuer": "b12a13df30c7123fe6ab8e03551d7c7465cb1acfa04701e0efb05eeba9a2b7d6", + "subject": "2da412a4c5cd6d9f59cf6bf84fc8d78cb9ba3352c515f85031592cd6a59fb9a2", "scope": [ "tool:search" ], "depth": 2, "parent_id": "cred-1-researcher", - "signature": "a6ec5c6c8add5923b9bf3f9fc9cb8cd0c10cf6faf53a3e04571a418f199d8191bbf9ee4f0b7ddd06f75eaeaa4e25e0a761e62ca59b09d31d6544b4bc90ce1e0d" + "signature": "6307eb2fb085b2dcca5d5cbdc21a6762c18019db5e8c29714bc7e99837d696d7f18ddbbe60ebfb3c6815f4a44e61020f71a4b30da5d387a3b9afc22ef1b49204" } ] } diff --git a/examples/rejection-with-proof/dag.json b/examples/rejection-with-proof/dag.json index 5af7884..594c7b8 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": "f5d7ff727931d0acb4393c9012d9a4fe9c3a0837a1c191a31e5ad1c35fa2dd07", + "subject": "2f35acc0747fb45e739288d02bbd18e54e5283f770e30391b1c272fc1285e3bf", "scope": [ "task:read", "task:write", @@ -15,31 +15,31 @@ { "record_id": "rec-1-researcher", "credential_id": "cred-1-researcher", - "subject": "747e897486d4d32abd45d3a9e04e5357f6a88a1bad8d0ce9dc6084cd10c7b121", + "subject": "b12a13df30c7123fe6ab8e03551d7c7465cb1acfa04701e0efb05eeba9a2b7d6", "scope": [ "task:read", "tool:purchase", "tool:search" ], - "parent_record_hash": "f8fd5e5073d51406bdc9fe7e79b90cd3c70afbee1ac47d5763996d4af20af5a7" + "parent_record_hash": "3655a4752c7d0841cf4cc2adb80523ea988469a548d4b05636707132ab25c1b1" }, { "record_id": "rec-2-retriever", "credential_id": "cred-2-retriever", - "subject": "b05e7f85927cc4a5886ec097da0200c4a02d0d944dd2d73c9a10f5662041074c", + "subject": "2da412a4c5cd6d9f59cf6bf84fc8d78cb9ba3352c515f85031592cd6a59fb9a2", "scope": [ "tool:search" ], - "parent_record_hash": "f940a2e460960035bbe39f93b7c76ac965d914857e10bce66bffce871fbc102e" + "parent_record_hash": "1b5cc71b77a37400961d71ccc75c4292f5ed122bd77ef854137dda059540faf9" }, { "record_id": "rec-denied-purchase", "credential_id": "cred-2-retriever", - "subject": "b05e7f85927cc4a5886ec097da0200c4a02d0d944dd2d73c9a10f5662041074c", + "subject": "2da412a4c5cd6d9f59cf6bf84fc8d78cb9ba3352c515f85031592cd6a59fb9a2", "scope": [ "tool:search" ], - "parent_record_hash": "3867abdd0574dce6cd20cae0b1e4a22afa5ab0a22778cda449c82c12dca4f127", + "parent_record_hash": "169191a74d471318788dab7619b3914121763baa8a34b2f4465b7e16ba10398e", "decision": "deny", "requested_capability": "tool:purchase", "effective_scope": [ diff --git a/pyproject.toml b/pyproject.toml index dc7927a..a6f9c0d 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.8", + "agent-manifest>=0.10", "rfc8785>=0.1", ] diff --git a/src/ca2a_runtime/tee/sev_snp.py b/src/ca2a_runtime/tee/sev_snp.py index 6d96696..4cd0049 100644 --- a/src/ca2a_runtime/tee/sev_snp.py +++ b/src/ca2a_runtime/tee/sev_snp.py @@ -1,10 +1,15 @@ """AMD SEV-SNP attestation report parsing and the SEV-SNP provider. -This module parses the SEV-SNP ``ATTESTATION_REPORT`` structure (1184 bytes, -AMD SEV-SNP ABI) and exposes the fields the cA2A verifier appraises: the launch -measurement, the report data (which binds the runtime key and nonce), and the -ECDSA-P384 signature over the report body. The verification and certificate -chain appraisal live in :mod:`ca2a_verify.sev_snp`. +This module exposes the SEV-SNP ``ATTESTATION_REPORT`` fields the cA2A verifier +appraises: the launch measurement, the report data (which binds the runtime key +and nonce), and the ECDSA-P384 signature over the report body. The verification +and certificate chain appraisal live in :mod:`ca2a_verify.sev_snp`. + +The layout itself is **not** defined here. Offsets and parsing come from +``agent_manifest`` (>=0.10), which cmcp also consumes; between them the org +carried four mirrors of one ABI table, two inside cmcp alone. :class:`SevSnpReport` +stays as cA2A's surface, keeping its error contract and the ``signature_rs`` +helper, but the bytes are read by the shared parser. Producing a report requires a real SEV-SNP guest (``/dev/sev-guest``), so :meth:`SevSnpProvider.attest` fails closed off hardware. The verifier does not @@ -14,18 +19,20 @@ from __future__ import annotations -import struct from dataclasses import dataclass +from agent_manifest import SNP_OFFSETS, SNP_REPORT_LEN, SnpVerificationError, parse_snp_report + from ca2a_runtime.errors import AttestationFailed, AttestationUnsupported from ca2a_runtime.tee.base import AttestationReport, BaseProvider -# Layout of the SEV-SNP ATTESTATION_REPORT (offsets in bytes). -REPORT_SIZE = 0x4A0 # 1184 -SIG_OFFSET = 0x2A0 # signature covers report[:SIG_OFFSET] -REPORT_DATA_OFFSET = 0x50 +# Layout of the SEV-SNP ATTESTATION_REPORT, re-exported from agent-manifest's +# shared ABI table so cA2A and cmcp cannot disagree about where a field sits. +REPORT_SIZE = SNP_REPORT_LEN +SIG_OFFSET = SNP_OFFSETS["signature"] # signature covers report[:SIG_OFFSET] +REPORT_DATA_OFFSET = SNP_OFFSETS["report_data"] REPORT_DATA_LEN = 64 -MEASUREMENT_OFFSET = 0x90 +MEASUREMENT_OFFSET = SNP_OFFSETS["measurement"] MEASUREMENT_LEN = 48 # ECDSA-P384 signature: r then s, each in a 72-byte little-endian field. SIG_COMPONENT_LEN = 72 @@ -64,26 +71,24 @@ def signature_rs(self) -> tuple[int, int]: @classmethod def parse(cls, blob: bytes) -> SevSnpReport: - """Parse a raw report, raising AttestationFailed on any malformed input.""" - if len(blob) < REPORT_SIZE: - raise AttestationFailed( - "SEV-SNP report too short", - detail=f"got {len(blob)} bytes, need at least {REPORT_SIZE}", - ) - version, guest_svn, policy = struct.unpack_from("