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

### Added

- **Delegation revocation.** Until now a delegated grant could not be withdrawn
inside its validity window (`docs/spec/threat-model.md`). The issuer of a
credential, or any issuer above it in the chain, can now sign a
`RevocationStatement` naming the credential by the SHA-256 of its canonical
body. `verify_chain`, `verify_delegation_chain`, `verify_chain_file`,
`handle_peer_request` and `ca2a verify-chain` / `verify-dag` take an optional
`RevocationSnapshot` and refuse a chain containing a revoked hop with
`CREDENTIAL_REVOKED`, which also refuses every grant beneath it. A delegate
cannot revoke upward, a statement from an unrelated key has no effect, nothing
can un-revoke, and a snapshot with a forged or unsigned statement is refused as
a whole with `INVALID_REVOCATION`. Without a snapshot, verification stays
offline and unchanged, and now reports revocation as `not_checked`
(`verify_chain` returns a `RevocationStatus`; `ChainResult`, `PeerResult` and
the CLI output carry it). An optional `max_revocation_staleness` fails closed
with `REVOCATION_STATUS_UNKNOWN` when the snapshot is missing or too old.
`PeerNode` takes a `revocation_source` callable consulted on every call.
Distributing revocation data remains the deployment's job.

- Add an explicit hardware floor for outbound peer appraisal, a pinned SNP
verifier with platform/DEBUG/VMPL/guest-SVN checks, and a two-host acceptance
harness. A same-operator hardware diagnostic completed both directions;
Expand Down
1 change: 1 addition & 0 deletions LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ cA2A 0.2 is a Developer Preview with a runnable, tested profile and runtime. Thi
## What is built

- The delegation credential model and offline chain verifier: trusted-root checks, signatures, scope attenuation, depth and validity bounds, duplicate credential IDs within a chain, and cross-chain splice rejection. These checks do not maintain a global history of used credentials.
- Revocation of a delegation before its validity window closes. The issuer of a credential, or an issuer above it in the chain, signs a revocation statement naming the credential by digest; a verifier given a snapshot of such statements refuses a chain containing a revoked hop (`CREDENTIAL_REVOKED`). A delegate cannot revoke its delegator, statements cannot be un-revoked, and a snapshot carrying a forged or unsigned statement is refused as a whole. **What it does not do:** cA2A does not publish or fetch revocation data, so a verifier knows only what its supplied snapshot held at its `as_of` time, and a supplier can withhold a statement. A verifier with no snapshot still verifies offline and cannot learn of a revocation; the result says revocation was `not_checked`. `max_revocation_staleness` lets a verifier fail closed without a recent snapshot, but it is off by default, and `ca2a start` does not load a snapshot.
- 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"`.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ ca2a verify-dag --dag examples/rejection-with-proof/dag.json \
```json
{"verified": true, "records": 4, "outcome": "denied",
"requested_capability": "tool:purchase", "effective_scope": ["tool:search"],
"cross_checked": true}
"cross_checked": true, "revocation": "not_checked"}
```

The callee's own policy permits `tool:purchase`. It is refused anyway, because
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ ca2a verify-chain --chain demo-chain.json --trusted-root-issuer "$(cat trusted-r
Expected exit code: `0`.

```json
{"verified": true, "hops": 2, "leaf_scope": ["cap:read"]}
{"verified": true, "hops": 2, "leaf_scope": ["cap:read"], "revocation": "not_checked"}
```

In production, the relying party obtains trusted roots through its own approval process. Copying the issuer from an arbitrary incoming chain into the trust list would let that chain choose its own authority.
Expand Down
84 changes: 84 additions & 0 deletions docs/spec/delegation-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ without invalidating the signature.
| No `credential_id` repeats | `CREDENTIAL_REPLAY` |
| Each hop's validity window, when present, contains the evaluation time | `CREDENTIAL_NOT_YET_VALID` / `CREDENTIAL_EXPIRED` |
| The root issuer is pinned by the callee for runtime authorization | `UNTRUSTED_DELEGATION_ROOT` |
| No hop is revoked by its issuer or by an issuer above it (checked only when a revocation snapshot is supplied) | `CREDENTIAL_REVOKED` |
| A snapshot no older than `max_revocation_staleness` was supplied (only when that bound is set) | `REVOCATION_STATUS_UNKNOWN` |

Signature validity establishes who issued a chain; it does not establish that
the issuer is trusted. A live callee therefore supplies its local
Expand Down Expand Up @@ -83,6 +85,88 @@ Windows are not required to nest across hops. A chain is usable only at times
inside every hop's window, so the effective window is already the intersection
of the hops'; requiring structural nesting would add no authority bound.

<a id="ca2a-delegation-revocation"></a>

## Revocation

A validity window bounds how long a grant lasts. Revocation lets a party with
authority over a grant withdraw it before `not_after`. See
`ca2a_runtime.delegation.revocation`.

### Revocation statement

A `RevocationStatement` is a signed body plus a detached Ed25519 signature,
canonicalized with the same RFC 8785 helper as credentials:

| Field | Type | Meaning |
|---|---|---|
| `type` | string | Always `ca2a.delegation-revocation.v1`. Signed, so a revocation signature cannot be taken for a signature over any other object the same key signs |
| `revoked_digest` | string | `sha256:` followed by the lowercase hex SHA-256 of the revoked credential's canonical body (the bytes its issuer signed) |
| `revoker` | hex | Ed25519 public key of the party withdrawing the grant |
| `issued_at` | int | Unix epoch seconds, as claimed by the revoker |
| `signature` | hex | Ed25519 over the canonical body, by `revoker` |

The wire object is strict in the same way a credential is: unknown or missing
fields are rejected, so there is no field that could express an un-revoke.

### Who may revoke

A statement is effective against hop `i` of a chain only when `revoker` is the
issuer of hop `i` or of an earlier hop. The delegator can withdraw what it
granted, and any issuer above it can withdraw a grant made below it. The subject
of hop `i` is not in that set, so a delegate cannot revoke the grant it received
or anything above it. A validly signed statement from any other key has no effect
on the chain. Authority is judged against the presented chain at verification
time, after its structure has verified, because a statement names a credential
rather than a chain.

Revoking hop `i` refuses every chain that contains it, which is every chain
through which a grant beneath it could be exercised. The error names the first
revoked hop.

Revocation is monotonic. A hop is revoked when any effective statement for it is
present, so no statement, earlier or later, can reverse it.

### Snapshots and what verification reports

`verify_chain` takes an optional `RevocationSnapshot`: a set of statements plus
`as_of`, the time its supplier last brought it up to date. Every statement's
signature is checked when the snapshot is built, and a snapshot containing an
unsigned or forged statement is refused as a whole with `INVALID_REVOCATION`
rather than having the bad entry dropped, since dropping it would let tampering
with the feed act as an un-revocation. `as_of` is asserted by the supplier and is
not signed by any revoker.

`verify_chain` returns a `RevocationStatus`. With no snapshot, verification is
exactly as before, still offline under P-4, and the status has `checked=False`
(`not_checked`): the chain may have been revoked and the verifier would not know.
With a snapshot and no revoked hop, the status has `checked=True` and carries the
snapshot's `as_of` (`not_revoked`). `ChainResult`, `PeerResult` and the
`ca2a verify-chain` output carry the same status, and the CLI prints it on every
successful verification.

`max_revocation_staleness` (seconds, default unset) makes revocation checking
mandatory. With it set, a missing snapshot, or one whose `as_of` is more than
that many seconds before the evaluation time, raises
`REVOCATION_STATUS_UNKNOWN`. It is unset by default so that offline verification
with no revocation data keeps working. A stale snapshot can still prove a hop
revoked, since revocation is monotonic, so a revoked hop is reported as
`CREDENTIAL_REVOKED` before staleness is judged.

When `at_time` is supplied, only statements with `issued_at` at or before it
count, so an audit of a past decision is not rewritten by a revocation issued
afterwards. Without `at_time` (a live decision), every statement the verifier
holds applies, whatever the revoker's clock said.

### Out of scope

cA2A defines no protocol for publishing or fetching revocation statements.
Getting current snapshots to verifiers is the deployment's job. A verifier with
no snapshot cannot learn of a revocation; it is told that it did not check. A
snapshot supplier can withhold statements. `PeerNode` accepts a
`revocation_source` callable that it consults on every call, but the
config-driven `ca2a start` does not load one.

<a id="ca2a-capability-attenuation"></a>

## Attenuation is the whole point
Expand Down
7 changes: 5 additions & 2 deletions docs/spec/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ An error also carries a human-readable message and an optional `detail`. The mes
| `CredentialReplay` | `CREDENTIAL_REPLAY` | 409 | A `credential_id` appears more than once in a single chain. Raised by `verify_chain`. |
| `CredentialNotYetValid` | `CREDENTIAL_NOT_YET_VALID` | 403 | A hop's `not_before` bound is after the evaluation time. The chain is well formed and validly signed, but the grant is not yet in force. Raised by `verify_chain`. |
| `CredentialExpired` | `CREDENTIAL_EXPIRED` | 403 | A hop's `not_after` bound is before the evaluation time. Raised by `verify_chain`. |
| `CredentialRevoked` | `CREDENTIAL_REVOKED` | 403 | A hop's credential was revoked by its issuer or by an issuer above it in the chain, according to the revocation snapshot the verifier supplied. Every chain containing that hop is refused. Raised by `verify_chain` only when a snapshot is supplied. See [delegation chain](delegation-chain.md#ca2a-delegation-revocation). |
| `RevocationStatusUnknown` | `REVOCATION_STATUS_UNKNOWN` | 503 | The verifier set `max_revocation_staleness` and has no revocation snapshot, or its snapshot's `as_of` is older than that bound at the evaluation time. Nothing is wrong with the chain; the verifier cannot currently establish that it has not been revoked, and fails closed. Raised by `verify_chain`. |
| `InvalidRevocation` | `INVALID_REVOCATION` | 400 | A revocation statement or snapshot is malformed, unsigned, or carries a signature that does not verify, or a snapshot file is missing or not valid JSON. A snapshot with any such statement is refused as a whole. Raised by `RevocationStatement`, `RevocationSnapshot` and `load_revocation_snapshot`. |
| `HolderProofInvalid` | `HOLDER_PROOF_INVALID` | 401 | The presenter of a delegation chain did not prove it controls the leaf `subject`: no proof was presented, the proof was malformed, it answered a challenge this callee did not issue or which has expired, or its signature did not verify over the exact request being made. 401 rather than 403 because the chain may well carry the authority requested while the caller has not shown it is the party that authority was delegated to. Distinct from `ATTESTATION_FAILED`, which is about what the caller is *running*: a caller can appraise perfectly and still fail this. Raised by `verify_holder_proof`, `handle_peer_request`, and the A2A adapter on a malformed proof. See [profile](profile.md) P-4a. |
| `AttestationUnsupported` | `ATTESTATION_UNSUPPORTED` | 500 | An attestation provider was requested that the host cannot supply. Raised by any provider's `attest` when the host lacks what its collector needs, and by `OpaqueProvider`, which has no collector. The `detail` names the missing piece. See [Peer Attestation](attestation.md). |
| `AttestationFailed` | `ATTESTATION_FAILED` | 412 | Attestation evidence was present but did not verify. Raised by the SEV-SNP verifier on a malformed report, an untrusted or broken certificate chain, a bad report signature, or a measurement / report-data mismatch. See [Peer Attestation](attestation.md). |
Expand All @@ -29,7 +32,7 @@ An error also carries a human-readable message and an optional `detail`. The mes

## Which errors are live today

`ConfigError`, `InvalidCredential`, `ScopeEscalation`, `BrokenDelegationLink`, `DelegationDepthExceeded`, `CredentialReplay`, `CredentialNotYetValid`, `CredentialExpired`, , `ProvenanceLinkBroken`, and `TraceDigestUnsupported` are raised by shipping code paths: attenuated delegation, offline chain verification, and the provenance DAG. `ScopeNotPermitted` is raised by the peer-call enforcement decision core (`enforce_peer_call`), and `SealedChannelError` by the sealed channel (`SealedChannel.seal`, `open_sealed`), both of which are implemented. `TransportError` is raised by the A2A metadata adapter when cA2A keys are present but cannot be parsed into a `PeerRequest`.
`ConfigError`, `InvalidCredential`, `ScopeEscalation`, `BrokenDelegationLink`, `DelegationDepthExceeded`, `CredentialReplay`, `CredentialNotYetValid`, `CredentialExpired`, `CredentialRevoked`, `RevocationStatusUnknown`, `InvalidRevocation`, `ProvenanceLinkBroken`, and `TraceDigestUnsupported` are raised by shipping code paths: attenuated delegation, offline chain verification, and the provenance DAG. `ScopeNotPermitted` is raised by the peer-call enforcement decision core (`enforce_peer_call`), and `SealedChannelError` by the sealed channel (`SealedChannel.seal`, `open_sealed`), both of which are implemented. `TransportError` is raised by the A2A metadata adapter when cA2A keys are present but cannot be parsed into a `PeerRequest`.

`AttestationFailed` is raised by the SEV-SNP verifier (chain, report signature, and measurement binding), and by a collector whose hardware returned evidence that does not commit the key and nonce it asked for. `AttestationUnsupported` is raised where a host cannot collect at all: no TPM or tpm2-pytss for `tpm`, no configfs-TSM or guest device for `sev-snp` and `tdx`, and on Azure confidential VMs, where SEV-SNP runs behind a paravisor that owns `REPORT_DATA`. See [Peer Attestation](attestation.md) and [ROADMAP.md](../../ROADMAP.md).

Expand Down Expand Up @@ -57,7 +60,7 @@ Verification fails closed. `verify_chain`, `verify_dag`, and `cross_check_chain`

## See also

- [Delegation Chain](delegation-chain.md) for the checks behind `ScopeEscalation`, `BrokenDelegationLink`, `DelegationDepthExceeded`, `CredentialReplay`, `CredentialNotYetValid`, and `CredentialExpired`.
- [Delegation Chain](delegation-chain.md) for the checks behind `ScopeEscalation`, `BrokenDelegationLink`, `DelegationDepthExceeded`, `CredentialReplay`, `CredentialNotYetValid`, `CredentialExpired`, `CredentialRevoked`, `RevocationStatusUnknown`, and `InvalidRevocation`.
- [Provenance DAG](provenance-dag.md) for the checks behind `ProvenanceLinkBroken`.
- [Verification Library](verification-library.md) for `verify_chain`, `verify_chain_file`, `verify_dag`, and `cross_check_chain`.
- [Failure Modes](failure-modes.md) for how these errors map to observable runtime behavior.
2 changes: 2 additions & 0 deletions docs/spec/profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ A peer that does not implement this profile MUST ignore the cA2A extension field

A callee MUST verify the presented delegation chain before acting: every credential's signature, the continuity of each parent link, and the attenuation rule that a child's scope is a subset of its parent's. A callee MUST reject a chain whose depth exceeds its configured maximum, and MUST reject a credential replayed from a different chain. Verification MUST be possible offline, without contacting the issuer.

> **Revocation is an optional input, not a network dependency.** A verifier can also be given a snapshot of signed revocation statements and will then refuse a chain containing a hop revoked by its issuer or an issuer above it. Consulting a snapshot contacts nobody, so this does not conflict with offline verification. Without one, the reference verifier reports revocation as `not_checked` rather than implying the chain is unrevoked. See [delegation chain](delegation-chain.md#ca2a-delegation-revocation).

### P-4a Holder binding

A callee MUST NOT act on a delegation chain until the presenter has proved it controls the private key of the leaf credential's `subject`. The callee MUST issue the challenge the proof answers, and MUST reject a proof that does not commit to the callee's own identity, that challenge, the leaf `credential_id` and `subject`, the requested capability, the `record_id`, the `parent_record_hash`, the sealed payload if one is present, and the caller's own offered channel key if one is present. A chain presented without such a proof MUST be refused with `HOLDER_PROOF_INVALID`.
Expand Down
Loading
Loading