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("