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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **A refusal is now evidence.** `enforce_peer_call` previously raised on an over-scoped call and emitted nothing, so a denial left no artifact and an auditor saw a gap where a hop should be. It now builds a linked denial record (`provenance.denial_record_for`) carrying the requested capability, the effective scope it fell outside, and the reason, and attaches it to `ScopeNotPermitted.record`. The call still fails closed; the record is evidence of the refusal, not a way to continue. `verify_dag` treats a denial as terminal and rejects any chain that continues past a refused hop, `cross_check_chain` matches only hop records positionally against the chain while still requiring a denial to name a credential that is in it, and `ca2a verify-dag` reports `outcome: denied` with the requested capability and effective scope. Denial fields are omitted from an allow record's hashed body, so existing allow-record hashes are unchanged.
- **`examples/rejection-with-proof/`**: the front-door demo. An agent asks for authority nobody delegated to it, is refused, and the refusal verifies offline through the shipped CLI against the committed chain and DAG. The callee's local policy deliberately permits the capability, so the refusal turns on delegation rather than on the callee not supporting it. The example runs in CI (`tests/unit/test_example_rejection_with_proof.py`) so the README cannot promise something the code stopped doing. It makes no attestation claim and says so.

### 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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,38 @@ pip install --pre ca2a-runtime
ca2a verify-chain --chain ./examples/minimal/chain.json
```

### See it refuse a call, and prove why

An agent asks for authority nobody delegated to it, is refused, and hands over a
record a third party checks offline without trusting the operator that produced it:

```bash
python examples/rejection-with-proof/demo.py
```

```
ALLOW tool:search effective scope ['tool:search']
DENY tool:purchase capability 'tool:purchase' is not in the effective scope
requested tool:purchase
effective ['tool:search']
```

```bash
ca2a verify-dag --dag examples/rejection-with-proof/dag.json \
--chain examples/rejection-with-proof/chain.json
```

```json
{"verified": true, "records": 4, "outcome": "denied",
"requested_capability": "tool:purchase", "effective_scope": ["tool:search"],
"cross_checked": true}
```

The callee's own policy permits `tool:purchase`. It is refused anyway, because
the delegated scope does not carry it. See
[examples/rejection-with-proof/](examples/rejection-with-proof/) for what this
does and does not claim; it makes no attestation claim.

See [docs/quickstart.md](docs/quickstart.md) for the full walkthrough.

---
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.
89 changes: 89 additions & 0 deletions examples/rejection-with-proof/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Rejection with proof

An agent asks for authority nobody delegated to it. The call is refused, and the
refusal is handed over as a linked provenance record that a third party verifies
offline, from the committed files, without trusting the operator that produced
them.

```
python examples/rejection-with-proof/demo.py
```

```
Delegation chain, narrowing at every hop
depth 0 orchestrator scope=['task:read', 'task:write', 'tool:purchase', 'tool:search']
depth 1 researcher scope=['task:read', 'tool:purchase', 'tool:search']
depth 2 retriever scope=['tool:search']

ALLOW tool:search effective scope ['tool:search']
DENY tool:purchase capability 'tool:purchase' is not in the effective scope
requested tool:purchase
effective ['tool:search']
why the leaf grant carries ['tool:search']; the callee's own
policy would have permitted it, but nobody delegated it
```

Then check it the way an auditor would, with no access to the runtime that
produced it:

```
ca2a verify-chain --chain examples/rejection-with-proof/chain.json
ca2a verify-dag --dag examples/rejection-with-proof/dag.json \
--chain examples/rejection-with-proof/chain.json
```

```json
{"verified": true, "records": 4, "leaf_scope": ["tool:search"],
"outcome": "denied", "requested_capability": "tool:purchase",
"effective_scope": ["tool:search"],
"denial_reason": "capability 'tool:purchase' is not in the effective scope",
"cross_checked": true}
```

## Why the callee's policy is permissive here

The callee's local policy deliberately **allows** `tool:purchase`. The call is
still refused, because the retriever's delegated scope does not carry it. That is
the distinction cA2A exists to enforce: authorization is the intersection of what
the callee permits and what was actually delegated, and the second half is what
transport-layer auth cannot express. If the demo used a callee that simply did
not support purchasing, the refusal would prove nothing.

## What makes the refusal checkable

1. **The chain is signed and attenuating.** Each hop's scope is a provable
subset of its parent's, so `sorted(leaf.scope)` is not the callee's word for
it: it is derivable from signatures the callee did not produce.
2. **The denial record states the gap.** It carries the capability requested and
the effective scope it fell outside, so a verifier reproduces the decision
rather than believing the outcome.
3. **The denial links into the same DAG.** It is hash-chained to the preceding
hop, so it cannot be silently dropped from an audit trail. Reparenting it
breaks the link, which the demo shows at the end.
4. **A denial is terminal.** `verify_dag` rejects any chain that continues past a
refused hop, so a trail cannot claim work proceeded after the denial.

## What this example does not claim

Everything here is real and hardware-independent: chain verification, scope
attenuation, the scope-intersect-policy decision, and DAG verification need no
TEE. This example makes **no attestation claim**. It does not seal a payload,
does not attest a peer, and proves nothing about where the code ran. It proves
what was authorized and what was refused.

For the attestation side, see
[`../cross-operator-delegation/`](../cross-operator-delegation/), which is
explicit that its attestation step uses synthetic vectors, and
[`../../docs/hardware-validation.md`](../../docs/hardware-validation.md) for what
has been verified against real silicon.

## Files

| File | What it is |
|---|---|
| `demo.py` | The runnable story. Regenerates the two JSON files. |
| `chain.json` | The signed delegation chain, root to leaf. |
| `dag.json` | The provenance DAG: three hop records plus the terminal denial. |

Keys are generated per run, so re-running the demo rewrites both files with new
signatures. The committed copies are a sample, not a fixture.
42 changes: 42 additions & 0 deletions examples/rejection-with-proof/chain.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"chain": [
{
"credential_id": "cred-0-orchestrator",
"issuer": "667e9aa02b1e19e4ef9a24627e10b97e099532dff71a1065a343a6037a8e3c3a",
"subject": "2108c06f9eb264876fc88c0c79c0986177a87222a776ce1b0de7c24483084ea3",
"scope": [
"task:read",
"task:write",
"tool:purchase",
"tool:search"
],
"depth": 0,
"parent_id": null,
"signature": "bf10c51cda37517c28f20f66f9f216eade1fd49c766003f2343a7bf5a8b0d423b704f936dd55851a3debb2afb982dd8ba685dd18c171c3bb6a025442063cb20f"
},
{
"credential_id": "cred-1-researcher",
"issuer": "2108c06f9eb264876fc88c0c79c0986177a87222a776ce1b0de7c24483084ea3",
"subject": "de5c05ea1d7946c8f2c6ffde727cc6ed0da7b906ff2e1219f5916a3cc372f46b",
"scope": [
"task:read",
"tool:purchase",
"tool:search"
],
"depth": 1,
"parent_id": "cred-0-orchestrator",
"signature": "1702778d81160bc46f1044b1b628e28b010d9d45c0767c1d1ce50b9f4ebfb3fe954455805ed5e9fdf7d72cc8ec18bb83cd23966ba88d95de21fa94b8b4026f06"
},
{
"credential_id": "cred-2-retriever",
"issuer": "de5c05ea1d7946c8f2c6ffde727cc6ed0da7b906ff2e1219f5916a3cc372f46b",
"subject": "6eb31885c7ac3d745186ed36a2e77b754325a1ceba6f81b76994a36866d68f52",
"scope": [
"tool:search"
],
"depth": 2,
"parent_id": "cred-1-researcher",
"signature": "08999679fff8d3d0940fb94c085ed549511aa026bf9b702da91e8244387db3d86c50004e547950009cf5ce7bf7625bf67893583011097622bed1782de844cf0d"
}
]
}
Loading