From e17d489fa0ac41233126d4c20c3e10f792e47a81 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Sun, 26 Jul 2026 22:04:32 -0700 Subject: [PATCH] feat(tee): share the validated DCAP v4 signature-section parse Expose parse_tdx_quote_signature() and TdxQuoteSignature so cmcp and ca2a can delegate the TDX quote layout instead of carrying their own copy of the offsets. Real DCAP v4 quotes nest the Quoting Enclave material: the bytes after the attestation key are a type-6 QE_REPORT_CERTIFICATION_DATA header, and the QE report, its PCK signature, the auth data and the type-5 PCK chain live inside it. A flat parse reads the QE report six bytes early and rejects every genuine quote. Both sibling repos have that bug today, found by running their verifiers against the real GCP C3 quote this parser was written against. verify_tdx_quote() now calls the shared parse, so the layout has one copy, and two regression tests pin the nested structure and the type check. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 4 + python/src/agent_manifest/__init__.py | 7 +- python/src/agent_manifest/_tdx_verify.py | 124 +++++++++++++++-------- python/tests/test_tdx_verify.py | 30 ++++++ 4 files changed, 121 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5206eff..cd59fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Agent Manifest are documented here. Format follows [Keep ## [Unreleased] +### Added + +**[SDK]** `parse_tdx_quote_signature()` and `TdxQuoteSignature` expose the validated DCAP v4 signature-section parse (de-nested QE report, QE signature, auth data, PCK chain PEM) so sibling repos can delegate to it instead of reimplementing the offsets. Real quotes nest the QE material under a type-6 `QE_REPORT_CERTIFICATION_DATA` header; a flat parse reads the QE report six bytes early and rejects every genuine quote, which is what happened in cmcp and ca2a. `verify_tdx_quote()` now calls the shared parse, so there is one copy of the layout, and two regression tests pin the nested structure. + ## [0.6.1] — 2026-07-27 Closes a crash in `verify_manifest()` reachable from untrusted input on a default install, and settles the signature-envelope question for v0.2. No change to how manifests are signed or to any existing verification result. diff --git a/python/src/agent_manifest/__init__.py b/python/src/agent_manifest/__init__.py index 0ebf951..4b0bfdc 100644 --- a/python/src/agent_manifest/__init__.py +++ b/python/src/agent_manifest/__init__.py @@ -42,8 +42,8 @@ ) from ._cert_chain import verify_cert_chain, CertChainError from ._tdx_verify import ( - TdxQuote, TdxVerificationError, - parse_tdx_quote, verify_tdx_quote, + TdxQuote, TdxQuoteSignature, TdxVerificationError, + parse_tdx_quote, parse_tdx_quote_signature, verify_tdx_quote, ) from ._tpm_verify import ( TpmQuote, TpmVerificationError, @@ -94,7 +94,8 @@ "verify_snp_signature", "verify_vcek_chain", "verify_runtime_data_binding", "fetch_vcek", "verify_cert_chain", "CertChainError", - "TdxQuote", "TdxVerificationError", "parse_tdx_quote", "verify_tdx_quote", + "TdxQuote", "TdxQuoteSignature", "TdxVerificationError", + "parse_tdx_quote", "parse_tdx_quote_signature", "verify_tdx_quote", "TpmQuote", "TpmVerificationError", "parse_tpm_quote", "verify_tpm_quote", "verify_manifest", "verify_runtime_report", "VerificationContext", "VerificationResult", diff --git a/python/src/agent_manifest/_tdx_verify.py b/python/src/agent_manifest/_tdx_verify.py index e3bcb4c..477a153 100644 --- a/python/src/agent_manifest/_tdx_verify.py +++ b/python/src/agent_manifest/_tdx_verify.py @@ -86,6 +86,83 @@ class TdxQuote: raw: bytes # the full quote +@dataclass +class TdxQuoteSignature: + """The signature section of a TDX v4 DCAP quote, already de-nested. + + Real DCAP v4 quotes nest the Quoting Enclave material: the bytes after the + attestation key are a certification-data header of type 6 + (``QE_REPORT_CERTIFICATION_DATA``), and the QE report, its PCK signature, + the QE auth data and the type-5 PCK certificate chain live *inside* that. + A flat parse that reads the QE report directly after the attestation key is + off by the 6-byte header and rejects every genuine quote, which is why this + parse is shared rather than reimplemented per repo. + """ + + signed_body: bytes # quote header + TD report body; what the att key signs + quote_signature: bytes # 64 bytes, raw r||s + attestation_key: bytes # 64 bytes, raw P-256 x||y + qe_report: bytes # 384-byte SGX report + qe_report_signature: bytes # 64 bytes, raw r||s, by the PCK leaf + qe_auth_data: bytes + pck_chain_pem: bytes # PEM bundle, leaf (PCK) first, Intel root last + + +def parse_tdx_quote_signature(quote: bytes) -> TdxQuoteSignature: + """Parse the signature section of a TDX v4 DCAP quote, fail-closed. + + Handles the nested type-6 QE-report certification data described on + :class:`TdxQuoteSignature`. Raises :class:`TdxVerificationError` on a + malformed or truncated quote. Performs no cryptographic verification; + :func:`verify_tdx_quote` does that. + """ + if len(quote) < _QUOTE_HEADER_LEN + _TD_REPORT_LEN + 4: + raise TdxVerificationError("quote too short to contain a signature") + + signed = quote[:_QUOTE_HEADER_LEN + _TD_REPORT_LEN] + off = _QUOTE_HEADER_LEN + _TD_REPORT_LEN + (auth_size,) = struct.unpack_from(" TdxQuote: """Parse an Intel TDX v4 DCAP quote's header + TD report body. @@ -157,58 +234,23 @@ def verify_tdx_quote(quote: bytes, *, trusted_root_pem: Optional[bytes] = None) "TDX quote verification requires the 'cryptography' package" ) from e - if len(quote) < _QUOTE_HEADER_LEN + _TD_REPORT_LEN + 4: - raise TdxVerificationError("quote too short to contain a signature") - - signed = quote[:_QUOTE_HEADER_LEN + _TD_REPORT_LEN] - off = _QUOTE_HEADER_LEN + _TD_REPORT_LEN - (auth_size,) = struct.unpack_from("