diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce1e27..16a16f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Peer-call enforcement decision core (Tier 2): `ca2a_runtime.policy.LocalPolicy` and `ca2a_runtime.peer` (`effective_scope`, `enforce_peer_call`). Effective permission is the delegated leaf scope intersected with the callee's local policy; a granted call emits a linked provenance record. New error `SCOPE_NOT_PERMITTED`. Claim C3 (scope-policy intersection) is now a validated experiment. Cedar-engine binding of the local policy and live A2A transport wiring remain open. - Sealed peer channel (Tier 2): `ca2a_runtime.channel` (`SealedChannel`, `generate_channel_keypair`, `open_sealed`). HPKE-style X25519 -> HKDF-SHA256 -> ChaCha20-Poly1305 sealing a payload to the peer's attested key; only the peer's private key opens it, and a wrong key or tampered ciphertext fails closed. Claim C4 (sealed-payload confidentiality) is now a validated experiment at the cryptographic layer. The enclave-binding of the private key (a hardware property) and live-path wiring remain open. - Cross-operator attestation (Claim C6) validated in software: a two-operator harness composing the SEV-SNP verifier, measurement pinning, and the sealed channel demonstrates independent keys, mutual attestation, confidential cross-operator delegation, and binary-swap detection. Synthetic report vectors (a genuine report needs SEV-SNP hardware); real hardware end to end remains open. **All six claims (C1-C6) are now validated experiments.** +- Intel TDX attestation backend: `ca2a_runtime.tee.tdx` (DCAP Quote v4 parsing, `TdxProvider`) and `ca2a_verify.tdx.verify_tdx_quote` (PCK chain to a trusted Intel root, QE report signature, attestation-key binding, quote signature, and MRTD/report-data binding), all fail-closed. Chain path validated against the genuine Intel SGX Root CA; multi-level signature path validated with a synthetic self-consistent quote. Quote generation requires a real TDX guest. - Transport-agnostic inbound peer request handler: `ca2a_runtime.peer.handle_peer_request` with `PeerRequest` / `PeerResult`. Composes the full pipeline (verify chain, intersect scope and enforce, open a sealed payload with the enclave key, emit a linked provenance record) fail-closed. A transport parses its wire format into a `PeerRequest`; cA2A does not define the transport (profile, not protocol). - RFC 8785 (JSON Canonicalization Scheme) canonicalization: `ca2a_runtime.canonical.canonicalize`. Credential and provenance bodies are now signed over the JCS encoding (UTF-16 key ordering, JCS string escaping, literal non-ASCII, shortest-decimal integers), so cA2A signatures are cross-verifiable with agent-manifest. ASCII credentials are byte-identical to the previous encoding, so existing signatures still verify. - Repository scaffold: governance, CI/CD, docs framework, and packaging at parity with the agentrust-io house standard @@ -26,6 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Live A2A transport wiring for the peer-enforcement decision core, including binding the seal to a verified attestation report (Tier 2) - Cedar policy engine binding for the local policy (Tier 2) -- Intel TDX and TPM attestation backends (Tier 3); end-to-end SEV-SNP validation against real hardware vectors +- TPM attestation backend (Tier 3); end-to-end SEV-SNP and TDX validation against real hardware quotes [Unreleased]: https://github.com/agentrust-io/ca2a/commits/main diff --git a/LIMITATIONS.md b/LIMITATIONS.md index be98b82..e8613e2 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -11,7 +11,7 @@ cA2A is a pre-release profile in active design. This document states plainly wha - **Runtime peer-delegation enforcement.** The runtime does not yet accept a delegation credential on a live inbound peer call, verify it in the request path, and intersect the delegated scope with a local Cedar policy. This is Tier 2 on the roadmap. - **Sealed peer channel.** The channel is implemented: a payload is sealed to the peer's attested X25519 key (X25519 ECDH, HKDF-SHA256, ChaCha20-Poly1305), and only the holder of the peer's private key can open it. The remaining gap is the hardware property that the private key never leaves the peer's enclave (established by attestation), and wiring the seal to a verified report on a live inbound call. Until that end-to-end binding lands on hardware, do not assume a payload is confined to a specific attested measurement. -- **Real hardware attestation.** The **SEV-SNP verifier is implemented**: report parsing, VCEK certificate chain verification, ECDSA-P384 report-signature verification, and measurement/report-data binding, all fail-closed. The chain path is validated against the genuine AMD Milan root chain; the report-signature path is validated with synthetic vectors, since a real report plus VCEK pair needs SEV-SNP hardware. Report generation (`SevSnpProvider.attest`) still requires a real SEV-SNP guest. **Intel TDX and TPM backends are not yet implemented (Tier 3).** Until a backend verifies a real quote end to end against a golden measurement on hardware, cA2A must not be described as fully attested across trust domains. +- **Real hardware attestation.** The **SEV-SNP verifier is implemented**: report parsing, VCEK certificate chain verification, ECDSA-P384 report-signature verification, and measurement/report-data binding, all fail-closed. The chain path is validated against the genuine AMD Milan root chain; the report-signature path is validated with synthetic vectors, since a real report plus VCEK pair needs SEV-SNP hardware. Report generation (`SevSnpProvider.attest`) still requires a real SEV-SNP guest. The **Intel TDX verifier is also implemented** (DCAP Quote v4: PCK chain to the genuine Intel SGX Root CA, QE report, attestation-key binding, quote signature, MRTD binding; synthetic-quote validated, quote generation needs a TDX guest). **The TPM backend is not yet implemented (Tier 3).** Until a backend verifies a real quote end to end against a golden measurement on hardware, cA2A must not be described as fully attested across trust domains. ## Out of scope diff --git a/ROADMAP.md b/ROADMAP.md index 151c6d2..0a6a136 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -31,8 +31,9 @@ Already implemented and tested elsewhere; cA2A depends on it rather than reimple Real hardware attestation verification (SEV-SNP VCEK chain, Intel TDX quote via QVL/PCS, TPM AK cert + checkquote). This is a dependency for any cross-operator trust claim, single-agent or multi-agent, and is shared with cmcp. At least one real hardware backend must land before cA2A is marketed as attested across trust domains, so the demo matches the claim. - **SEV-SNP verifier: landed.** Report parsing, VCEK chain verification (validated against the real AMD Milan root), ECDSA-P384 report-signature verification, and measurement/report-data binding, all fail-closed. Report generation still requires a real SEV-SNP guest. See `ca2a_verify.sev_snp` and [docs/spec/attestation.md](docs/spec/attestation.md). +- **TDX verifier: landed.** DCAP Quote v4 parsing, PCK chain to the genuine Intel SGX Root CA, QE report signature, attestation-key binding, quote signature, and MRTD binding, all fail-closed. Quote generation requires a real TDX guest. See `ca2a_verify.tdx`. - **Cross-operator attestation (C6): validated in software.** A two-operator harness (SEV-SNP verifier + measurement pinning + sealed channel) shows independent keys, mutual attestation, confidential cross-operator delegation, and binary-swap detection. All six claims (C1-C6) are now validated experiments. -- **Pending:** Intel TDX and TPM backends; end-to-end validation of the report-signature path against real hardware vectors on a confidential VM; and the live A2A transport binding that drives the whole pipeline off a real inbound call. +- **Pending:** the TPM backend; end-to-end validation of the SEV-SNP and TDX signature paths against real hardware quotes on a confidential VM; and a transport that parses real A2A wire messages into a `PeerRequest`. ## v1.0: Stable profile diff --git a/docs/spec/attestation.md b/docs/spec/attestation.md index 0901d45..ac709e5 100644 --- a/docs/spec/attestation.md +++ b/docs/spec/attestation.md @@ -17,8 +17,8 @@ An `AttestationReport` carries `platform`, `measurement`, the bound `public_key` |---|---|---| | `software-only` | none | Available; for development and CI. Reports `platform: software-only`, never a hardware platform string. | | `sev-snp` | AMD SEV-SNP | Verifier implemented (see below). Report generation requires a real SEV-SNP guest. | +| `tdx` | Intel TDX | Verifier implemented (see below). Quote generation requires a real TDX guest. | | `tpm` | TPM 2.0 / vTPM | Tier 3, not yet implemented | -| `tdx` | Intel TDX | Tier 3, not yet implemented | | `opaque` | OPAQUE Confidential Runtime | Tier 3, explicit opt-in, not auto-selected | ## SEV-SNP verification @@ -33,9 +33,15 @@ An `AttestationReport` carries `platform`, `measurement`, the bound `public_key` **Cross-operator use.** Two operators in separate trust domains each bind their sealed-channel public key into a report and verify the counterparty's report against a pinned golden measurement. This composes into mutual attestation, confidential cross-operator delegation (seal to the attested key), and binary-swap detection (a changed measurement is rejected), validated in software as claim C6. See the [call graph](call-graph.md) and the `claim6-cross-operator-attestation` experiment. +## TDX verification + +`ca2a_verify.tdx.verify_tdx_quote` appraises an Intel TDX quote (DCAP, ECDSA-256) offline in four fail-closed steps: the PCK certificate chain is verified up to a trusted Intel root; the Quoting Enclave report is verified against the PCK; the attestation key is confirmed to be the one the QE report data commits to (SHA-256 of the key and the QE auth data); and the attestation key's signature over the quote body is verified, along with the launch measurement (MRTD) and report data. + +**What is validated.** The chain-verification path accepts the genuine self-signed Intel SGX Root CA fetched from Intel (`tests/fixtures/tdx/`) and rejects an untrusted root. The multi-level signature path (PCK to QE report to attestation key to quote) is exercised end to end with a synthetic self-consistent quote, because a genuine quote requires a TDX guest. Byte offsets follow the Intel DCAP Quote v4 layout; end-to-end validation against a real hardware quote requires a TDX guest and remains open. + ## Fail closed -Providers without a backend `detect()` to False, so they are never selected automatically, and verification fails closed when evidence is absent or invalid. This is deliberate: cA2A must not be described as attested across trust domains until a real hardware backend verifies a quote against a golden measurement. TDX and TPM backends remain Tier 3. See [LIMITATIONS.md](../../LIMITATIONS.md). +Providers without a backend `detect()` to False, so they are never selected automatically, and verification fails closed when evidence is absent or invalid. This is deliberate: cA2A must not be described as attested across trust domains until a real hardware backend verifies a quote against a golden measurement on hardware. The TPM backend remains Tier 3. See [LIMITATIONS.md](../../LIMITATIONS.md). ## Why this is the critical path diff --git a/pyproject.toml b/pyproject.toml index 498a0fb..2d231be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,5 +91,5 @@ warn_return_any = false files = ["src/ca2a_runtime", "src/ca2a_verify"] [[tool.mypy.overrides]] -module = ["ca2a_runtime.tee.sev_snp", "ca2a_runtime.tee.tdx", "ca2a_runtime.channel.sealed"] +module = ["ca2a_runtime.tee.sev_snp", "ca2a_runtime.tee.tdx", "ca2a_verify.tdx", "ca2a_runtime.channel.sealed"] warn_unused_ignores = false diff --git a/src/ca2a_runtime/tee/__init__.py b/src/ca2a_runtime/tee/__init__.py index 788da18..5b7ddcf 100644 --- a/src/ca2a_runtime/tee/__init__.py +++ b/src/ca2a_runtime/tee/__init__.py @@ -1,11 +1,19 @@ -"""TEE provider abstraction for peer attestation. - -cA2A reuses the pluggable provider model from cmcp: a provider produces an -attestation report that binds a public key to a hardware measurement. Real -hardware backends are Tier 3 (see ROADMAP.md) and fail closed until implemented. -""" - -from ca2a_runtime.tee.base import AttestationReport, BaseProvider -from ca2a_runtime.tee.sev_snp import SevSnpProvider, SevSnpReport - -__all__ = ["AttestationReport", "BaseProvider", "SevSnpProvider", "SevSnpReport"] +"""TEE provider abstraction for peer attestation. + +cA2A reuses the pluggable provider model from cmcp: a provider produces an +attestation report that binds a public key to a hardware measurement. Real +hardware backends are Tier 3 (see ROADMAP.md) and fail closed until implemented. +""" + +from ca2a_runtime.tee.base import AttestationReport, BaseProvider +from ca2a_runtime.tee.sev_snp import SevSnpProvider, SevSnpReport +from ca2a_runtime.tee.tdx import TdxProvider, TdxQuote + +__all__ = [ + "AttestationReport", + "BaseProvider", + "SevSnpProvider", + "SevSnpReport", + "TdxProvider", + "TdxQuote", +] diff --git a/src/ca2a_runtime/tee/tdx.py b/src/ca2a_runtime/tee/tdx.py new file mode 100644 index 0000000..e90a839 --- /dev/null +++ b/src/ca2a_runtime/tee/tdx.py @@ -0,0 +1,133 @@ +"""Intel TDX quote (DCAP, ECDSA-256) parsing and the TDX provider. + +Parses a TDX v4 quote: the header, the TD report body (from which the launch +measurement MRTD and the report data are read), and the ECDSA signature section +(the quote signature, the attestation key, the Quoting Enclave report and its +PCK signature, and the PCK certificate chain). Verification lives in +:mod:`ca2a_verify.tdx`. + +Producing a quote requires a real TDX guest, so :meth:`TdxProvider.attest` fails +closed off hardware. Byte offsets follow the Intel DCAP Quote v4 layout; the +verifier is exercised against synthetic self-consistent vectors plus the real +Intel SGX Root CA in the test suite. End-to-end validation against a real +hardware quote requires a TDX guest and remains open. +""" + +from __future__ import annotations + +import struct +from dataclasses import dataclass + +from cryptography import x509 + +from ca2a_runtime.errors import AttestationFailed, AttestationUnsupported +from ca2a_runtime.tee.base import AttestationReport, BaseProvider + +HEADER_LEN = 48 +TD_REPORT_LEN = 584 +SIGNED_LEN = HEADER_LEN + TD_REPORT_LEN # quote signature covers header + TD report +MRTD_OFFSET = HEADER_LEN + 136 +MRTD_LEN = 48 +REPORT_DATA_OFFSET = HEADER_LEN + 520 +REPORT_DATA_LEN = 64 + +# Signature section (relative to SIGNED_LEN + 4-byte sig_data_len). +QUOTE_SIG_LEN = 64 +ATT_KEY_LEN = 64 +QE_REPORT_LEN = 384 +QE_REPORT_DATA_OFFSET = 320 # within the QE SGX report + +TEE_TYPE_TDX = 0x81 +CERT_TYPE_PCK_CHAIN = 5 +TDX_GUEST_DEVICE = "/dev/tdx_guest" + + +@dataclass(frozen=True) +class TdxQuote: + """The parsed subset of a TDX quote cA2A appraises.""" + + version: int + tee_type: int + measurement: bytes # MRTD + report_data: bytes + signed_body: bytes # header + TD report body + quote_signature: bytes # 64 bytes, r||s big-endian + attestation_key: bytes # 64 bytes, raw P-256 x||y + qe_report: bytes # 384-byte SGX report + qe_report_signature: bytes # 64 bytes, r||s big-endian + qe_auth_data: bytes + pck_chain: list[x509.Certificate] # leaf (PCK) first, root last + + @classmethod + def parse(cls, blob: bytes) -> TdxQuote: + if len(blob) < SIGNED_LEN + 4: + raise AttestationFailed( + "TDX quote too short", + detail=f"got {len(blob)} bytes, need at least {SIGNED_LEN + 4}", + ) + version, _att_key_type, tee_type = struct.unpack_from(" len(blob): + raise AttestationFailed("TDX quote signature section is truncated") + + quote_sig = blob[pos : pos + QUOTE_SIG_LEN] + pos += QUOTE_SIG_LEN + att_key = blob[pos : pos + ATT_KEY_LEN] + pos += ATT_KEY_LEN + qe_report = blob[pos : pos + QE_REPORT_LEN] + pos += QE_REPORT_LEN + qe_report_sig = blob[pos : pos + QUOTE_SIG_LEN] + pos += QUOTE_SIG_LEN + (qe_auth_len,) = struct.unpack_from(" bool: + import os + + return os.path.exists(TDX_GUEST_DEVICE) + + def attest(self, public_key: str, nonce: str) -> AttestationReport: + raise AttestationUnsupported( + "TDX quote generation requires a real TDX guest", + detail=f"{TDX_GUEST_DEVICE} not present; run on an Intel TDX confidential VM", + ) diff --git a/src/ca2a_verify/tdx.py b/src/ca2a_verify/tdx.py new file mode 100644 index 0000000..96478ce --- /dev/null +++ b/src/ca2a_verify/tdx.py @@ -0,0 +1,96 @@ +"""Offline appraisal of an Intel TDX quote (DCAP, ECDSA-256). + +Appraisal chains four fail-closed checks: + +1. Certificate chain: the PCK is verified up to a trusted Intel root. +2. Quoting Enclave report: the QE report is signed by the PCK. +3. Attestation key binding: the QE report data commits to the attestation key + (SHA-256 of the attestation key and the QE auth data), so the key that signs + the quote is the one the QE vouched for. +4. Quote signature: the attestation key signs the quote body, and the launch + measurement (MRTD) and report data match expected values. + +Any missing or mismatched step raises AttestationFailed. The verifier needs no +hardware. Byte offsets follow the Intel DCAP Quote v4 layout; end-to-end +validation against a real hardware quote requires a TDX guest and remains open. +""" + +from __future__ import annotations + +import hashlib + +from cryptography import x509 +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature +from cryptography.hazmat.primitives.hashes import SHA256 + +from ca2a_runtime.errors import AttestationFailed +from ca2a_runtime.tee.tdx import ( + QE_REPORT_DATA_OFFSET, + TEE_TYPE_TDX, + TdxQuote, +) +from ca2a_verify.sev_snp import verify_cert_chain + +__all__ = ["verify_tdx_quote"] + + +def _p256_key(raw_xy: bytes) -> ec.EllipticCurvePublicKey: + return ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), b"\x04" + raw_xy) + + +def _verify_ecdsa(public_key: ec.EllipticCurvePublicKey, sig_rs: bytes, message: bytes) -> None: + r = int.from_bytes(sig_rs[:32], "big") + s = int.from_bytes(sig_rs[32:64], "big") + public_key.verify(encode_dss_signature(r, s), message, ec.ECDSA(SHA256())) + + +def verify_tdx_quote( + quote_bytes: bytes, + *, + trusted_roots: list[x509.Certificate], + expected_mrtd: bytes | None = None, + expected_report_data: bytes | None = None, +) -> TdxQuote: + """Appraise a TDX quote offline. Raises AttestationFailed on any failure.""" + quote = TdxQuote.parse(quote_bytes) + + if quote.tee_type != TEE_TYPE_TDX: + raise AttestationFailed( + "quote is not a TDX quote", detail=f"tee_type={quote.tee_type:#x}" + ) + + # 1. PCK chain to a trusted Intel root. + verify_cert_chain(quote.pck_chain, trusted_roots) + + pck_key = quote.pck_chain[0].public_key() + if not isinstance(pck_key, ec.EllipticCurvePublicKey): + raise AttestationFailed("PCK does not carry an elliptic-curve public key") + + # 2. QE report signed by the PCK. + try: + _verify_ecdsa(pck_key, quote.qe_report_signature, quote.qe_report) + except (InvalidSignature, ValueError) as exc: + raise AttestationFailed("QE report signature failed to verify") from exc + + # 3. Attestation key binding: the QE report data commits to the attestation key. + expected_binding = hashlib.sha256(quote.attestation_key + quote.qe_auth_data).digest() + qe_report_data = quote.qe_report[QE_REPORT_DATA_OFFSET : QE_REPORT_DATA_OFFSET + 64] + if qe_report_data[:32] != expected_binding: + raise AttestationFailed("attestation key is not the one the QE report vouched for") + + # 4. Quote signature by the attestation key, over the quote body. + try: + _verify_ecdsa(_p256_key(quote.attestation_key), quote.quote_signature, quote.signed_body) + except (InvalidSignature, ValueError) as exc: + raise AttestationFailed("TDX quote signature failed to verify") from exc + + if expected_mrtd is not None and quote.measurement != expected_mrtd: + raise AttestationFailed( + "MRTD does not match the expected value", detail=f"got {quote.measurement.hex()}" + ) + if expected_report_data is not None and quote.report_data != expected_report_data: + raise AttestationFailed("report data does not match the expected binding") + + return quote diff --git a/tests/fixtures/tdx/intel_sgx_root_ca.pem b/tests/fixtures/tdx/intel_sgx_root_ca.pem new file mode 100644 index 0000000..7dee743 --- /dev/null +++ b/tests/fixtures/tdx/intel_sgx_root_ca.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw +aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv +cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ +BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG +A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0 +aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT +AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7 +1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB +uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ +MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50 +ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV +Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI +KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg +AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI= +-----END CERTIFICATE----- diff --git a/tests/unit/test_tdx.py b/tests/unit/test_tdx.py new file mode 100644 index 0000000..9a8841d --- /dev/null +++ b/tests/unit/test_tdx.py @@ -0,0 +1,149 @@ +"""Tests for Intel TDX quote parsing and offline appraisal. + +The certificate-chain verification is exercised against the real Intel SGX Root +CA (tests/fixtures/tdx/intel_sgx_root_ca.pem). The multi-level signature path +(PCK -> QE report -> attestation key -> quote) is exercised end to end with a +synthetic self-consistent quote, because a genuine quote requires a TDX guest. +""" + +from __future__ import annotations + +import hashlib +import struct +from pathlib import Path + +import pytest +from cryptography import x509 +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature +from cryptography.hazmat.primitives.hashes import SHA256 +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +from ca2a_runtime.errors import AttestationFailed, AttestationUnsupported +from ca2a_runtime.tee.tdx import ( + MRTD_OFFSET, + QE_REPORT_DATA_OFFSET, + SIGNED_LEN, + TdxProvider, + TdxQuote, +) +from ca2a_verify.sev_snp import verify_cert_chain +from ca2a_verify.tdx import verify_tdx_quote +from tests.unit.conftest import make_ec_cert + +FIXTURE = Path(__file__).parent.parent / "fixtures" / "tdx" / "intel_sgx_root_ca.pem" + + +def _raw_p256(key: ec.EllipticCurvePrivateKey) -> bytes: + return key.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)[1:] + + +def _sig_rs(key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes: + r, s = decode_dss_signature(key.sign(message, ec.ECDSA(SHA256()))) + return r.to_bytes(32, "big") + s.to_bytes(32, "big") + + +def build_quote(mrtd: bytes, report_data: bytes, *, root_key, root_name="test-intel-root"): + """Build a synthetic, self-consistent TDX v4 quote and its trusted root.""" + root = make_ec_cert(root_name, root_name, root_key, root_key) + pck_key = ec.generate_private_key(ec.SECP256R1()) + pck = make_ec_cert("PCK", root_name, pck_key, root_key) + att_key = ec.generate_private_key(ec.SECP256R1()) + att_raw = _raw_p256(att_key) + + header = struct.pack(" None: + q = verify_tdx_quote( + quote_and_root["quote"], trusted_roots=[quote_and_root["root"]], + expected_mrtd=quote_and_root["mrtd"], expected_report_data=quote_and_root["rd"], + ) + assert q.measurement == quote_and_root["mrtd"] + assert q.tee_type == 0x81 + + +def test_tampered_mrtd_fails(quote_and_root) -> None: + q = bytearray(quote_and_root["quote"]) + q[MRTD_OFFSET] ^= 0xFF # flip an MRTD byte after signing -> quote sig breaks + with pytest.raises(AttestationFailed): + verify_tdx_quote(bytes(q), trusted_roots=[quote_and_root["root"]]) + + +def test_wrong_expected_mrtd_fails(quote_and_root) -> None: + with pytest.raises(AttestationFailed): + verify_tdx_quote(quote_and_root["quote"], trusted_roots=[quote_and_root["root"]], + expected_mrtd=b"\x99" * 48) + + +def test_untrusted_root_fails(quote_and_root) -> None: + stranger = make_ec_cert("x", "x", ec.generate_private_key(ec.SECP256R1()), + ec.generate_private_key(ec.SECP256R1())) + with pytest.raises(AttestationFailed): + verify_tdx_quote(quote_and_root["quote"], trusted_roots=[stranger]) + + +def test_qe_report_tampered_fails(quote_and_root) -> None: + # Corrupt the attestation-key binding in the QE report data. + q = bytearray(quote_and_root["quote"]) + # Locate the QE report data: signed body + 4 + quote_sig(64) + att_key(64) + qe_report. + base = SIGNED_LEN + 4 + 64 + 64 + q[base + QE_REPORT_DATA_OFFSET] ^= 0xFF + with pytest.raises(AttestationFailed): + verify_tdx_quote(bytes(q), trusted_roots=[quote_and_root["root"]]) + + +def test_non_tdx_tee_type_rejected(quote_and_root) -> None: + q = bytearray(quote_and_root["quote"]) + struct.pack_into(" None: + with pytest.raises(AttestationFailed): + TdxQuote.parse(b"\x00" * 100) + + +def test_real_intel_root_accepted_and_stranger_rejected() -> None: + intel_root = x509.load_pem_x509_certificates(FIXTURE.read_bytes())[0] + # The genuine, self-signed Intel SGX Root CA is accepted as its own trust anchor. + verify_cert_chain([intel_root], trusted_roots=[intel_root]) + stranger = make_ec_cert("x", "x", ec.generate_private_key(ec.SECP256R1()), + ec.generate_private_key(ec.SECP256R1())) + with pytest.raises(AttestationFailed): + verify_cert_chain([intel_root], trusted_roots=[stranger]) + + +def test_provider_detect_and_attest() -> None: + assert TdxProvider.detect() is False + with pytest.raises(AttestationUnsupported): + TdxProvider().attest("deadbeef", "nonce")