diff --git a/.github/workflows/limitations-parity.yml b/.github/workflows/limitations-parity.yml new file mode 100644 index 0000000..87636e0 --- /dev/null +++ b/.github/workflows/limitations-parity.yml @@ -0,0 +1,75 @@ +name: Limitations parity + +# The SEV-SNP platform-state caveat in LIMITATIONS.md is shared verbatim with +# agentrust-io/trace-spec and agentrust-io/cmcp, because the gap it describes is a +# property of the shared TEE provider layer rather than of any one project. Three +# hand-maintained copies of a security caveat drift, and the copy that goes stale +# is the one that quietly overstates what the project checks. +# +# The full text stays in this file on purpose: LIMITATIONS.md exists to state the +# bounds where a reader will actually meet them, not to send them somewhere else. +# This job keeps the copies honest instead of thinning them out. +# +# Same shape as the schema-parity check in agentrust-io/agentrust-io.github.io. +# Only the text between the markers is compared. The project-specific paragraph +# after the end marker is this repository's own. + +on: + push: + branches: [main] + paths: ['LIMITATIONS.md', '.github/workflows/limitations-parity.yml'] + pull_request: + paths: ['LIMITATIONS.md', '.github/workflows/limitations-parity.yml'] + schedule: + # trace-spec can change without anything happening here, so also check daily. + - cron: '41 6 * * *' + workflow_dispatch: + +permissions: + contents: read + +jobs: + parity: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Compare the shared block with trace-spec + run: | + set -euo pipefail + src=https://raw.githubusercontent.com/agentrust-io/trace-spec/main/LIMITATIONS.md + begin='' + end='' + + extract() { + awk -v b="$begin" -v e="$end" ' + index($0, b) { grab = 1; next } + index($0, e) { grab = 0 } + grab { print } + ' "$1" + } + + if ! curl -fsSL "$src" -o /tmp/upstream.md; then + echo "::error::could not fetch $src" + exit 1 + fi + + extract LIMITATIONS.md > /tmp/local.txt + extract /tmp/upstream.md > /tmp/remote.txt + + if [ ! -s /tmp/local.txt ]; then + echo "::error::LIMITATIONS.md has no shared:platform-state-appraisal block" + exit 1 + fi + if [ ! -s /tmp/remote.txt ]; then + echo "::error::trace-spec/LIMITATIONS.md has no shared:platform-state-appraisal block" + exit 1 + fi + + if diff -u /tmp/remote.txt /tmp/local.txt; then + echo "ok shared block matches trace-spec" + else + echo "::error::the shared block has drifted from trace-spec/LIMITATIONS.md." + echo "::error::Edit it in trace-spec first, then copy the block here." + exit 1 + fi diff --git a/LIMITATIONS.md b/LIMITATIONS.md index 4eadc6c..d6f906e 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -1,53 +1,65 @@ -# Limitations - -cA2A 0.2 is a Developer Preview with a runnable, tested profile and runtime. This document states plainly what is built, what remains before 1.0, and what is out of scope, so no claim in the documentation runs ahead of the code. This is a deliberate discipline: proof, not promises. - -## What is built - -- The delegation credential model and the offline chain verifier skeleton: signature checks, scope attenuation (a child grant must be a provable subset of its parent), depth limits, and cross-chain replay rejection. The hardest of these semantics is reused from [agent-manifest](https://github.com/agentrust-io/agent-manifest), where it is implemented and tested. -- Configuration, error registry, and the CLI surface, including `ca2a start`, which builds a `PeerNode` from a config file and serves it over the reference transport. -- A reference HTTP transport and the attestation handshake, in software mode. `ca2a_runtime.transport.server` and `ca2a_runtime.transport.client` (standard library only) run a live inbound A2A-profile call end to end: the caller fetches the callee's attested channel key, seals a payload to it, and sends a delegated task; the callee parses the A2A metadata with the adapter, runs verify + policy + enforce + open-sealed + provenance, and replies. `ca2a_runtime.attestation` gates the seal on a verified channel key. This is a **reference** transport, not part of the profile: the profile mandates no wire protocol (see Out of scope), and in software mode the peer key is accepted at `assurance="none"`. - -## What remains before 1.0 - -- **Hardware-attested live binding.** The `verifier` seam in `ca2a_runtime.attestation` has now been driven off a real SEV-SNP quote on an Azure confidential VM: `verify_offer` returned `assurance="hardware"`, a payload was sealed to a channel key a hardware-verified measurement vouches for, and both a measurement mismatch and a stale nonce were rejected. See [docs/hardware-validation.md](docs/hardware-validation.md). Two gaps remain. First, the reference server/client still run in **software mode** by default (`assurance="none"`); the hardware path is a validated capability, not the default configuration. `ca2a start` inherits that: it refuses to start under `provider: auto` when no confidential-computing platform is detected, so a software-mode listener is always a config that names `software-only`, never a downgrade. Second, attestation on that run was one-directional: a follow-on cross-operator run (an Azure SEV-SNP peer calling a GCP Intel TDX peer, recorded in the same document) had the caller appraise the callee's real TDX quote before sealing, but the callee did not appraise the caller in return. The *protocol* is no longer one-directional -- a callee now issues a challenge and appraises the caller's offer before opening the sealed payload ([docs/spec/mutual-attestation.md](docs/spec/mutual-attestation.md)) -- but that is implemented and tested in **software mode only**, it is off by default, and making the protocol mutual does not make the recorded hardware run mutual. Mutual attestation on real silicon in both directions is still outstanding, it is still not *simultaneous* (the caller commits a sealed payload before the callee has appraised it), and both peers were driven by one operator's harness. -- **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 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, and the key-and-nonce binding) is implemented, and its cryptography is delegated to `agent_manifest.verify_tpm_quote` rather than being a third copy. Quote *generation* for SEV-SNP and TDX is now implemented, through the kernel configfs-TSM interface, but has **not been run on real silicon**: the collectors are exercised against a simulated configfs tree and synthetic reports only, so they are code that should work rather than a validated capability. Azure confidential VMs are explicitly out of scope for the SEV-SNP collector, since a paravisor-mediated guest cannot set `REPORT_DATA` and roots its channel key through the vTPM instead. This validates those verifiers, 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. -- **TPM collection works on hardware; chained verification does not, on every host.** Measured on a real Azure Trusted Launch vTPM (`Standard_D2s_v7`, eastus2, 2026-08-01). What passed: `TpmProvider.attest` produced a genuine quote signed by the platform attestation key (RSASSA/SHA-256), the shipped certificate certifies the quoting key, `parse_tpmt_signature` unwrapped the real `TPMT_SIGNATURE` and the bare signature verified against the shipped key, a tampered attest blob was rejected, and the quote's `extraData` equalled the derived key-and-nonce binding. Collector and verifier also ran **in one process**, which the previous caveat here said was untested: building `tpm2-pytss` from source inside a venv resolves the conflict with `agent-manifest`'s `cryptography`, so that tooling limitation is retired. - - What **failed**, and it is the important half: `verify_tpm_report` could not chain to a pinned root, reporting "AK chain root is not among the supplied trusted TPM roots". On that host the AK certificate at NV `0x01C101D0` is 994 bytes, is issued by `CN=Global Virtual TPM CA - 03`, and carries **no AIA extension at all**, so there are no intermediates to fetch and none stored elsewhere in NV. A different Azure host (`Standard_D2s_v5`, eastus, 2026-07-31) presented a 1596-byte certificate under `Azure Cloud Virtual TPM CA - 11` with a walkable AIA chain reaching the root pinned in `ca2a_verify/tpm_roots.py`. Both observations are real: Azure runs more than one vTPM CA generation, so **the shipped Azure root is not sufficient fleet-wide** and a deployment must pin the hierarchy its own hosts actually present. Until then, treat the TPM tier as: evidence is genuine and its signature and binding are verifiable, but key provenance is host-dependent. -- **TPM evidence proves key provenance only where a chain reaches a pinnable root.** A quote signed by the transient fallback key is a verifiable signature but carries no certificate chain, so it proves nothing about *where* the key lives, and `verify_tpm_report` rejects it. As above, a platform-provisioned certified attestation key is necessary but not sufficient: the chain must also be assemblable, which fails when the AK certificate carries no AIA. A GCP Shielded VM is weaker still (probed 2026-07-31: no EK certificate, no persistent handles, and `get-shielded-identity` returns a bare `ekPub` with no certificate). Client firmware TPM vendor roots are not yet published in a pinnable form. Separately, the TCG event log is 0 bytes on both Azure and GCP, so PCR values cannot be attributed to specific boot events on either cloud. - -## Platform state is not appraised - -The SEV-SNP path here establishes that a report is authentic and which workload it -describes: report signature, the VCEK to ASK to ARK chain with the ARK pinned by the -operator, and measurement binding. Those are the right four checks and they are not -in dispute. - -**What none of them ask is what kind of machine the report came from.** A SEV-SNP -report carries that separately in `PLATFORM_INFO` at offset 0x40: whether SMT is on, -whether ECC is enabled, whether ciphertext hiding is enforced, and whether the -firmware completed its boot-time DRAM alias check, which is AMD's mitigation for -BadRAM (security bulletin SB-3015). `agent-manifest` parses and can appraise these fields as of 2026-08-20; cA2A does not yet call that appraisal, on either the attested-peer or the sealed-channel path. - -The practical consequence: a report from a machine with SMT enabled and the alias -check never completed verifies exactly as cleanly as one from a machine with neither -condition. If that distinction matters to your deployment, it has to be asserted -explicitly, and today cA2A does not assert it for you. - -Related: [google/go-sev-guest#195](https://github.com/google/go-sev-guest/issues/195), -where the reference verifier's own platform-info policy field is documented as a -ceiling while four of its seven fields are enforced as minimums. Worth reading before -writing any policy over these bits. - -## Out of scope - -- A normative or production A2A transport. cA2A is a profile on A2A, not a replacement for it, and mandates no wire protocol. A reference HTTP transport ships (`ca2a_runtime.transport.server`/`client`) so the peer path is runnable on ordinary compute, but it is a convenience, not part of the profile: any A2A server can drive the adapter and a `PeerNode` instead, and cA2A makes no claim about the reference transport's production hardening. -- Agent identity issuance beyond delegation. -- AI model governance beyond delegation and provenance. -- Hardware TEE platform SDKs and firmware. - -## Dependencies on sibling projects - -cA2A composes primitives from [agent-manifest](https://github.com/agentrust-io/agent-manifest), [cmcp](https://github.com/agentrust-io/cmcp), and [trace-spec](https://github.com/agentrust-io/trace-spec). Version skew across those repos can change cA2A behavior; pin compatible versions before relying on cross-repo guarantees. +# Limitations + +cA2A 0.2 is a Developer Preview with a runnable, tested profile and runtime. This document states plainly what is built, what remains before 1.0, and what is out of scope, so no claim in the documentation runs ahead of the code. This is a deliberate discipline: proof, not promises. + +## What is built + +- The delegation credential model and the offline chain verifier skeleton: signature checks, scope attenuation (a child grant must be a provable subset of its parent), depth limits, and cross-chain replay rejection. The hardest of these semantics is reused from [agent-manifest](https://github.com/agentrust-io/agent-manifest), where it is implemented and tested. +- Configuration, error registry, and the CLI surface, including `ca2a start`, which builds a `PeerNode` from a config file and serves it over the reference transport. +- A reference HTTP transport and the attestation handshake, in software mode. `ca2a_runtime.transport.server` and `ca2a_runtime.transport.client` (standard library only) run a live inbound A2A-profile call end to end: the caller fetches the callee's attested channel key, seals a payload to it, and sends a delegated task; the callee parses the A2A metadata with the adapter, runs verify + policy + enforce + open-sealed + provenance, and replies. `ca2a_runtime.attestation` gates the seal on a verified channel key. This is a **reference** transport, not part of the profile: the profile mandates no wire protocol (see Out of scope), and in software mode the peer key is accepted at `assurance="none"`. + +## What remains before 1.0 + +- **Hardware-attested live binding.** The `verifier` seam in `ca2a_runtime.attestation` has now been driven off a real SEV-SNP quote on an Azure confidential VM: `verify_offer` returned `assurance="hardware"`, a payload was sealed to a channel key a hardware-verified measurement vouches for, and both a measurement mismatch and a stale nonce were rejected. See [docs/hardware-validation.md](docs/hardware-validation.md). Two gaps remain. First, the reference server/client still run in **software mode** by default (`assurance="none"`); the hardware path is a validated capability, not the default configuration. `ca2a start` inherits that: it refuses to start under `provider: auto` when no confidential-computing platform is detected, so a software-mode listener is always a config that names `software-only`, never a downgrade. Second, attestation on that run was one-directional: a follow-on cross-operator run (an Azure SEV-SNP peer calling a GCP Intel TDX peer, recorded in the same document) had the caller appraise the callee's real TDX quote before sealing, but the callee did not appraise the caller in return. The *protocol* is no longer one-directional -- a callee now issues a challenge and appraises the caller's offer before opening the sealed payload ([docs/spec/mutual-attestation.md](docs/spec/mutual-attestation.md)) -- but that is implemented and tested in **software mode only**, it is off by default, and making the protocol mutual does not make the recorded hardware run mutual. Mutual attestation on real silicon in both directions is still outstanding, it is still not *simultaneous* (the caller commits a sealed payload before the callee has appraised it), and both peers were driven by one operator's harness. +- **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 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, and the key-and-nonce binding) is implemented, and its cryptography is delegated to `agent_manifest.verify_tpm_quote` rather than being a third copy. Quote *generation* for SEV-SNP and TDX is now implemented, through the kernel configfs-TSM interface, but has **not been run on real silicon**: the collectors are exercised against a simulated configfs tree and synthetic reports only, so they are code that should work rather than a validated capability. Azure confidential VMs are explicitly out of scope for the SEV-SNP collector, since a paravisor-mediated guest cannot set `REPORT_DATA` and roots its channel key through the vTPM instead. This validates those verifiers, 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. +- **TPM collection works on hardware; chained verification does not, on every host.** Measured on a real Azure Trusted Launch vTPM (`Standard_D2s_v7`, eastus2, 2026-08-01). What passed: `TpmProvider.attest` produced a genuine quote signed by the platform attestation key (RSASSA/SHA-256), the shipped certificate certifies the quoting key, `parse_tpmt_signature` unwrapped the real `TPMT_SIGNATURE` and the bare signature verified against the shipped key, a tampered attest blob was rejected, and the quote's `extraData` equalled the derived key-and-nonce binding. Collector and verifier also ran **in one process**, which the previous caveat here said was untested: building `tpm2-pytss` from source inside a venv resolves the conflict with `agent-manifest`'s `cryptography`, so that tooling limitation is retired. + + What **failed**, and it is the important half: `verify_tpm_report` could not chain to a pinned root, reporting "AK chain root is not among the supplied trusted TPM roots". On that host the AK certificate at NV `0x01C101D0` is 994 bytes, is issued by `CN=Global Virtual TPM CA - 03`, and carries **no AIA extension at all**, so there are no intermediates to fetch and none stored elsewhere in NV. A different Azure host (`Standard_D2s_v5`, eastus, 2026-07-31) presented a 1596-byte certificate under `Azure Cloud Virtual TPM CA - 11` with a walkable AIA chain reaching the root pinned in `ca2a_verify/tpm_roots.py`. Both observations are real: Azure runs more than one vTPM CA generation, so **the shipped Azure root is not sufficient fleet-wide** and a deployment must pin the hierarchy its own hosts actually present. Until then, treat the TPM tier as: evidence is genuine and its signature and binding are verifiable, but key provenance is host-dependent. +- **TPM evidence proves key provenance only where a chain reaches a pinnable root.** A quote signed by the transient fallback key is a verifiable signature but carries no certificate chain, so it proves nothing about *where* the key lives, and `verify_tpm_report` rejects it. As above, a platform-provisioned certified attestation key is necessary but not sufficient: the chain must also be assemblable, which fails when the AK certificate carries no AIA. A GCP Shielded VM is weaker still (probed 2026-07-31: no EK certificate, no persistent handles, and `get-shielded-identity` returns a bare `ekPub` with no certificate). Client firmware TPM vendor roots are not yet published in a pinnable form. Separately, the TCG event log is 0 bytes on both Azure and GCP, so PCR values cannot be attributed to specific boot events on either cloud. + +## Platform state is not appraised + + + + +The SEV-SNP path here establishes that a report is authentic and which workload it +describes: report signature, the VCEK to ASK to ARK chain with the ARK pinned by the +operator, and measurement binding. Those are the right four checks and they are not +in dispute. + +**What none of them ask is what kind of machine the report came from.** A SEV-SNP +report carries that separately in `PLATFORM_INFO` at offset 0x40: whether SMT is on, +whether ECC is enabled, whether ciphertext hiding is enforced, and whether the +firmware completed its boot-time DRAM alias check, which is AMD's mitigation for +BadRAM (security bulletin SB-3015). + +The practical consequence: a report from a machine with SMT enabled and the alias +check never completed verifies exactly as cleanly as one from a machine with neither +condition. If that distinction matters to your deployment, it has to be asserted +explicitly. + +Related: [google/go-sev-guest#195](https://github.com/google/go-sev-guest/issues/195), +where the reference verifier's own platform-info policy field is documented as a +ceiling while four of its seven fields are enforced as minimums. Worth reading before +writing any policy over these bits. + + +**In cA2A.** [`agent-manifest`](https://manifest.agentrust-io.com/limitations/) parses these fields and can +enforce a policy over them as of 2026-08-20. cA2A does not yet call that appraisal, on +either the attested-peer or the sealed-channel path, so cA2A does not assert it for +you. + +## Out of scope + +- A normative or production A2A transport. cA2A is a profile on A2A, not a replacement for it, and mandates no wire protocol. A reference HTTP transport ships (`ca2a_runtime.transport.server`/`client`) so the peer path is runnable on ordinary compute, but it is a convenience, not part of the profile: any A2A server can drive the adapter and a `PeerNode` instead, and cA2A makes no claim about the reference transport's production hardening. +- Agent identity issuance beyond delegation. +- AI model governance beyond delegation and provenance. +- Hardware TEE platform SDKs and firmware. + +## Dependencies on sibling projects + +cA2A composes primitives from [agent-manifest](https://github.com/agentrust-io/agent-manifest), [cmcp](https://github.com/agentrust-io/cmcp), and [trace-spec](https://github.com/agentrust-io/trace-spec). Version skew across those repos can change cA2A behavior; pin compatible versions before relying on cross-repo guarantees.