Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Certificate-chain verification now delegates to agent-manifest's shared generic verifier (`agent-manifest>=0.5`) instead of ca2a's own copy. `ca2a_verify.verify_cert_chain` (used by the SEV-SNP VCEK, TDX PCK, and TPM AK chains alike) is now a thin wrapper over `agent_manifest.verify_cert_chain`, re-raising `CertChainError` as `AttestationFailed` to preserve ca2a's contract. This removes the org's last duplicated cert-chain verifier; ca2a keeps its own report/quote parsers, per-format signature checks, appraisal shapes, trust-root pinning, and `verify_offer` semantics. Behavior unchanged (all 201 tests pass unchanged).
- Bumped `agentrust-trace` to `>=0.4`. The previous `<0.4` cap worked around the
published 0.3.0 lacking the TRACE A2A `delegation` block that cA2A records
carry; 0.4.0 ships that block, so the cap is removed. cA2A still owns the
Expand Down
2 changes: 1 addition & 1 deletion LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ cA2A is a pre-release profile in active design. This document states plainly wha

- **Hardware-attested live binding.** The live transport and handshake run today in **software mode only** (the reference server/client above, `assurance="none"`). The remaining gap is hardware: driving the `verifier` seam in `ca2a_runtime.attestation` off a real SEV-SNP, TDX, or TPM quote (wrapping `ca2a_verify`) so the peer's channel key is bound to a hardware-verified enclave measurement, and relying on the enclave to hold the channel private key. There is also no CLI listener (`ca2a start`); serving is via `ca2a_runtime.transport.server.serve`. A software-mode live call is Tier 2 progress, not a claim that cA2A is attested across trust domains.
- **Sealed peer channel (hardware property).** 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. On a live call the handshake now gates the seal on a channel key the caller has appraised, but in software mode that appraisal is `assurance="none"`. Until the seal is bound to a hardware-verified measurement (above), do not assume a payload is confined to a specific attested measurement. Adapter-decoded `sealed_payload` bytes are opaque ciphertext only.
- **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** (DCAP Quote v4: PCK chain to the genuine Intel SGX Root CA, QE report, attestation-key binding, quote signature, MRTD binding) and the **TPM 2.0 verifier** (AK chain to a caller-supplied vendor root, AK signature, magic/type, qualifying-data and PCR-digest binding) are also implemented and synthetic-vector validated; quote generation needs the respective hardware. 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 and Intel TDX verifiers now appraise genuine hardware evidence end to end**: a real Azure CVM SEV-SNP report (VCEK chain to the AMD ARK-Milan root, ECDSA-P384 report signature, measurement binding) and a real GCP C3 DCAP v4 TDX quote (PCK chain to the Intel SGX Root CA, QE binding, quote signature, MRTD binding), both fail-closed and both rejecting a tampered copy. Runs are recorded in [docs/hardware-validation.md](docs/hardware-validation.md). The **TPM 2.0 verifier** (AK chain to a caller-supplied vendor root, AK signature, magic/type, qualifying-data and PCR-digest binding) is implemented but synthetic-vector validated only. Quote *generation* still requires the respective hardware for all three. This validates the verifier, not a running attested peer: until the `verifier` seam in `ca2a_runtime.attestation` is driven off a live quote on a confidential VM, cA2A must not be described as attested across trust domains.

## Out of scope

Expand Down
6 changes: 3 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ 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`.
- **SEV-SNP verifier: landed and validated on real evidence.** Report parsing, VCEK chain verification, ECDSA-P384 report-signature verification, and measurement/report-data binding, all fail-closed, run against a genuine Azure CVM report (see [docs/hardware-validation.md](docs/hardware-validation.md)). 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 and validated on real evidence.** DCAP Quote v4 parsing (including the nested type-6 QE certification data), PCK chain to the genuine Intel SGX Root CA, QE report signature, attestation-key binding, quote signature, and MRTD binding, all fail-closed, run against a genuine GCP C3 quote. Quote generation requires a real TDX guest. See `ca2a_verify.tdx`.
- **TPM 2.0 verifier: landed.** TPMS_ATTEST parsing, AK chain to a caller-supplied vendor root, AK signature (ECDSA or RSA), magic/type checks, and qualifying-data/PCR-digest binding, all fail-closed. Quote generation requires a real TPM. See `ca2a_verify.tpm`.
- **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:** end-to-end validation of the SEV-SNP, TDX, and TPM signature paths against real hardware quotes on a confidential VM. The transport that parses A2A messages into a `PeerRequest` has **landed** (`ca2a_runtime.transport.a2a`), running in software mode; the hardware seam is the `verifier` callable in `ca2a_runtime.attestation`.
- **Pending:** the TPM signature path against a real quote, and a live attested peer (the `verifier` seam driven off a hardware quote on a running confidential VM). SEV-SNP and TDX appraisal of real evidence is done. The transport that parses A2A messages into a `PeerRequest` has **landed** (`ca2a_runtime.transport.a2a`), running in software mode; the hardware seam is the `verifier` callable in `ca2a_runtime.attestation`.

## v1.0: Stable profile

Expand Down
82 changes: 82 additions & 0 deletions docs/hardware-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Hardware validation

What `ca2a_verify` has been run against real confidential-computing hardware,
what it has not, and how to reproduce each run. [LIMITATIONS.md](../LIMITATIONS.md)
and [ROADMAP.md](../ROADMAP.md) link here rather than restating it.

The rule: no document describes cA2A as attested for a platform until a genuine
quote from that platform has been verified end to end by the committed verifier,
and the run is recorded below.

## Current state

| Platform | Parsing | Certificate chain | Report signature | Verified against real hardware evidence |
|---|---|---|---|---|
| AMD SEV-SNP (Azure CVM, vTPM-rooted) | Yes | Yes, to the real AMD ARK-Milan root | Yes | **Yes**, 2026-07-27, capture of 2026-07-20 |
| Intel TDX (GCP C3, non-paravisor) | Yes | Yes, to the real Intel SGX Root CA | Yes | **Yes**, 2026-07-27, capture of 2026-07-21 |
| TPM 2.0 | Yes | Yes, to a caller-supplied vendor root | Yes | No. Synthetic vectors only |

## What these runs do and do not establish

They establish that the appraisal path in `ca2a_verify` accepts genuine evidence
from that silicon and fails closed on a tampered copy. They do **not** establish
that cA2A runs inside a TEE: quote *generation* still requires the hardware, and
the `verifier` seam in `ca2a_runtime.attestation` is not yet driven off a live
quote on a running peer. A live cA2A call is still software mode
(`assurance="none"`).

So the claim that is now true is "the cA2A verifier appraises real SEV-SNP and
TDX evidence." The claim that is still not true is "cA2A is attested across trust
domains." The second needs a peer whose channel key is bound to a hardware
measurement on a live confidential VM, which stays on the critical path in
[ROADMAP.md](../ROADMAP.md).

Verification is also bounded to a remote or rogue-admin adversary, not to one
with physical access: [TEE.fail](https://tee.fail) extracts attestation keys from
fully-patched SEV-SNP and TDX with a sub-$1000 DDR5 interposer.

## SEV-SNP, Azure confidential VM

Evidence: an SNP report and its VCEK plus the AMD ASK/ARK chain, captured from an
Azure DCasv5 CVM (family 0x19 / model 0x01, Milan) via the vTPM NV index
`0x01400001`. Azure SEV-SNP is paravisor-mediated, so `REPORT_DATA` binds the
vTPM attestation key rather than a cA2A-supplied value.

```
CA2A_SNP_FIXTURE_DIR=<capture dir> pytest tests/unit/test_sev_snp.py
```

The directory holds `snp_report.bin`, `vcek.der` and `cert_chain.pem`. Not
committed: the report's 64-byte `CHIP_ID` is a per-CPU hardware identifier, and
zeroing it invalidates the signature, so a redacted vector cannot exercise the
signature path.

## Intel TDX, GCP C3 confidential VM

Evidence: a DCAP v4 ECDSA quote from a GCP C3 CVM (non-paravisor TDX, kernel
6.17, configfs-TSM `tdx_guest` provider).

```
CA2A_TDX_QUOTE=<path to tdx_quote.bin> pytest tests/unit/test_tdx.py
```

Not committed: the PCK certificate identifies the CPU.

This run found the parser defect fixed alongside this page. Genuine DCAP v4
quotes nest the Quoting Enclave material under a type-6
`QE_REPORT_CERTIFICATION_DATA` header; `TdxQuote.parse` read the QE report six
bytes early and threw on every real quote. The synthetic fixture emitted the same
flat layout, so the tests validated the defect. Failure was closed, so this was a
false negative rather than an unsound accept, but the TDX path had never worked
against real evidence. Synthetic self-consistency is not validation.

## Not yet validated

- **TPM 2.0**: implemented and synthetic-vector validated; needs a real AK-signed
quote plus a vendor AK certificate chain.
- **Live attested peer binding**: the handshake gating the sealed channel on a
verified channel key runs in software mode. Driving it off a real quote on a
confidential VM is the remaining hardware property, and the precondition for
describing cA2A as attested across trust domains.
- **Cross-operator run on real hardware**: the two-operator harness
(`examples/cross-operator-delegation`) is validated in software.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ dependencies = [
# (see ROADMAP Tier 0). Pin a compatible minor; cross-repo skew is a known
# risk documented in LIMITATIONS.md.
"agentrust-trace>=0.4",
# Shared hardware-attestation verification (generic cert-chain verifier,
# SNP/TDX/TPM primitives) consumed via PyPI instead of duplicated per repo.
"agent-manifest>=0.5",
"rfc8785>=0.1",
]

Expand Down
51 changes: 36 additions & 15 deletions src/ca2a_runtime/tee/tdx.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
: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.
closed off hardware. Byte offsets follow the Intel DCAP Quote v4 layout, including
the nested type-6 QE certification data that wraps the QE report and the PCK
chain. The verifier is exercised against synthetic self-consistent vectors plus
the real Intel SGX Root CA in the test suite, and against a genuine GCP C3 quote
when ``CA2A_TDX_QUOTE`` points at one; see ``docs/hardware-validation.md``.
"""

from __future__ import annotations
Expand Down Expand Up @@ -38,7 +39,10 @@
QE_REPORT_DATA_OFFSET = 320 # within the QE SGX report

TEE_TYPE_TDX = 0x81
# Intel DCAP certification-data types, each preceded by a uint16 type + uint32 size.
CERT_TYPE_PCK_CHAIN = 5
CERT_TYPE_QE_REPORT = 6
CERT_DATA_HEADER_LEN = 6
TDX_GUEST_DEVICE = "/dev/tdx_guest"


Expand Down Expand Up @@ -79,17 +83,34 @@ def parse(cls, blob: bytes) -> TdxQuote:
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("<H", blob, pos)
pos += 2
qe_auth = blob[pos : pos + qe_auth_len]
pos += qe_auth_len
cert_type, cert_len = struct.unpack_from("<HI", blob, pos)
pos += 6
cert_bytes = blob[pos : pos + cert_len]

# The QE material is nested, not flat: what follows the attestation key is a
# certification-data header of type 6 wrapping the QE report, its PCK
# signature, the auth data and the type-5 PCK chain. Reading the QE report
# here directly lands six bytes early and rejects every genuine quote.
outer_type, outer_len = struct.unpack_from("<HI", blob, pos)
pos += CERT_DATA_HEADER_LEN
if outer_type != CERT_TYPE_QE_REPORT:
raise AttestationFailed(
"unsupported certification data type",
detail=f"type={outer_type}, expected {CERT_TYPE_QE_REPORT} (QE report)",
)
cert_data = blob[pos : pos + outer_len]
if len(cert_data) < QE_REPORT_LEN + QUOTE_SIG_LEN + 2 + CERT_DATA_HEADER_LEN:
raise AttestationFailed("QE certification data is truncated")

inner = 0
qe_report = cert_data[inner : inner + QE_REPORT_LEN]
inner += QE_REPORT_LEN
qe_report_sig = cert_data[inner : inner + QUOTE_SIG_LEN]
inner += QUOTE_SIG_LEN
(qe_auth_len,) = struct.unpack_from("<H", cert_data, inner)
inner += 2
qe_auth = cert_data[inner : inner + qe_auth_len]
inner += qe_auth_len
cert_type, cert_len = struct.unpack_from("<HI", cert_data, inner)
inner += CERT_DATA_HEADER_LEN
cert_bytes = cert_data[inner : inner + cert_len]
if cert_type != CERT_TYPE_PCK_CHAIN:
raise AttestationFailed(
"unsupported QE certification data type",
Expand Down
38 changes: 15 additions & 23 deletions src/ca2a_verify/sev_snp.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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, SHA384
from cryptography.hazmat.primitives.hashes import SHA384

from ca2a_runtime.errors import AttestationFailed
from ca2a_runtime.tee.sev_snp import SEV_GUEST_DEVICE, SIG_ALGO_ECDSA_P384_SHA384, SevSnpReport
Expand All @@ -33,30 +33,22 @@ def verify_cert_chain(
) -> None:
"""Verify a leaf-to-root certificate chain against a set of trusted roots.

``chain`` is ordered leaf first (VCEK), root last (ARK). Each certificate
must be directly issued by the next, and the final certificate must match a
trusted root by fingerprint. Raises AttestationFailed on any failure.
``chain`` is ordered leaf first (VCEK), root last (ARK). Delegates to
agent-manifest's shared, algorithm-agnostic cert-chain verifier (one
implementation across the org, consumed via PyPI) and re-raises its failure
as AttestationFailed to preserve ca2a's error contract. Used for the SEV-SNP
VCEK chain, the TDX PCK chain, and the TPM AK chain alike. Raises
AttestationFailed on any failure.
"""
if not chain:
raise AttestationFailed("empty certificate chain")

for i in range(len(chain) - 1):
child, issuer = chain[i], chain[i + 1]
try:
child.verify_directly_issued_by(issuer)
except (ValueError, TypeError, InvalidSignature) as exc:
raise AttestationFailed(
f"certificate at position {i} is not validly issued by the next",
detail=str(exc),
) from exc

root = chain[-1]
trusted = {c.fingerprint(SHA256()) for c in trusted_roots}
if root.fingerprint(SHA256()) not in trusted:
from agent_manifest import CertChainError
from agent_manifest import verify_cert_chain as _shared_verify_cert_chain

try:
_shared_verify_cert_chain(chain, trusted_roots)
except CertChainError as exc:
raise AttestationFailed(
"chain root is not a trusted AMD root",
detail=root.subject.rfc4514_string(),
)
"certificate chain verification failed", detail=str(exc)
) from exc


def verify_sev_snp_report(
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/test_sev_snp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from __future__ import annotations

import os
import struct
from datetime import UTC, datetime, timedelta
from pathlib import Path
Expand Down Expand Up @@ -157,3 +158,32 @@ def test_provider_detect_and_attest() -> None:
assert SevSnpProvider.detect() is False # no /dev/sev-guest in this environment
with pytest.raises(AttestationUnsupported):
SevSnpProvider().attest("deadbeef", "nonce")


@pytest.mark.skipif(
not os.environ.get("CA2A_SNP_FIXTURE_DIR"),
reason="set CA2A_SNP_FIXTURE_DIR to a dir with snp_report.bin + vcek.der + "
"cert_chain.pem (a real SEV-SNP capture) to run the hardware test",
)
def test_real_snp_report_verifies_to_the_amd_root() -> None:
"""Full-chain appraisal of a genuine SEV-SNP report.

The capture is not committed: the report's 64-byte CHIP_ID is a per-CPU
hardware identifier. See docs/hardware-validation.md.
"""
d = Path(os.environ["CA2A_SNP_FIXTURE_DIR"])
report = (d / "snp_report.bin").read_bytes()
vcek = x509.load_der_x509_certificate((d / "vcek.der").read_bytes())
rest = x509.load_pem_x509_certificates((d / "cert_chain.pem").read_bytes())
ark = rest[-1]
assert ark.subject == ark.issuer, "last cert in the chain must be the self-signed ARK"

parsed = verify_sev_snp_report(report, [vcek, *rest], trusted_roots=[ark])
assert len(parsed.measurement) == 48
assert parsed.measurement != bytes(48)

# and it fails closed on a flipped bit in the signed body
tampered = bytearray(report)
tampered[0x50] ^= 0x01
with pytest.raises(AttestationFailed):
verify_sev_snp_report(bytes(tampered), [vcek, *rest], trusted_roots=[ark])
Loading