You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(delegation): add credential validity windows (not_before/not_after)
Optional not_before / not_after fields (Unix epoch seconds, inclusive) on
DelegationCredential, enforced per hop by verify_chain at a caller-supplied
at_time defaulting to the current time, and threaded through
ca2a_verify.verify_delegation_chain, verify_chain_file, and the verify-chain /
verify-dag CLI as --at-time.
An absent bound is omitted from the signed body rather than encoded as null,
so every previously signed credential keeps its exact signed bytes; a present
bound is signed, so it cannot be stripped without failing verification. The
a2a-sdk bridge restores the bounds' integer-ness across the protobuf Struct
round trip exactly as it already did for depth.
New error codes CREDENTIAL_EXPIRED and CREDENTIAL_NOT_YET_VALID. Conformance
DELEG-007..009 and ACTION-012/013 cover the expired and not-yet-valid cases
from the #36 residual checklist.
Refs #36
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Murtaza Munaim <manizzle.msf@gmail.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -70,6 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
70
70
71
71
### Added
72
72
73
+
-**Delegation credentials can now carry a validity window (#36).** Optional `not_before` / `not_after` fields (Unix epoch seconds, inclusive at both ends) on `DelegationCredential`, enforced per hop by `verify_chain` at a caller-supplied `at_time` defaulting to the current time, and threaded through `ca2a_verify.verify_delegation_chain`, `verify_chain_file`, and `ca2a verify-chain` / `verify-dag` as `--at-time`. An absent bound is omitted from the signed body rather than encoded as null, so every previously signed credential keeps its exact signed bytes; a bound that is present is signed, so it cannot be stripped without failing verification. The a2a-sdk bridge restores the bounds' integer-ness across the protobuf `Struct` round trip exactly as it already did for `depth`. New error codes `CREDENTIAL_EXPIRED` and `CREDENTIAL_NOT_YET_VALID`; conformance `DELEG-007`–`DELEG-009` and `ACTION-012`/`ACTION-013` cover the expired and not-yet-valid cases from the #36 action-evidence checklist.
74
+
73
75
-**A bridge to the official `a2a-sdk`, so cA2A reaches the SDK that A2A agents actually run (#91).** cA2A describes itself as a profile on A2A and, until now, integrated with no A2A implementation: `transport.a2a_adapter` parsed A2A-shaped dicts and `transport.server` was a bespoke standard-library HTTP server. Both are honest about being a *reference*, but the practical effect was that a team already running the official SDK could only adopt the profile by replacing their transport with ours, which nobody does to try an alpha. A2A reached v1.0 in April 2026 under the Linux Foundation with SDKs in six languages, and is wired into Google ADK, Azure AI Foundry, Amazon Bedrock AgentCore and Copilot Studio; the profile reached none of it.
74
76
75
77
`ca2a_runtime.transport.a2a_sdk` is deliberately thin. The SDK carries A2A `metadata` as a `google.protobuf.Struct`, so converting that to a plain mapping hands the existing adapter exactly what it already parses: one parser, one set of tests, and the profile stays transport-agnostic. Optional extra (`pip install 'ca2a[a2a-sdk]'`); the base install still depends on no A2A implementation.
Copy file name to clipboardExpand all lines: docs/spec/delegation-chain.md
+27Lines changed: 27 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,6 +15,8 @@ A `DelegationCredential` has the following signed body plus a detached signature
15
15
|`depth`| int | 0 at the root, +1 per hop |
16
16
|`parent_id`| string or null |`credential_id` of the parent hop; null at the root |
17
17
|`signature`| hex | Ed25519 over the canonical body, by the issuer |
18
+
|`not_before`| int, optional | Unix epoch seconds; the credential is not valid before this time (inclusive) |
19
+
|`not_after`| int, optional | Unix epoch seconds; the credential is not valid after this time (inclusive) |
18
20
19
21
## Canonicalization
20
22
@@ -26,6 +28,12 @@ be a non-negative JSON integer (not a boolean or float), and `scope` must be a
26
28
non-empty array of unique non-empty strings. This ensures the object accepted by
27
29
one implementation is the same signed object another implementation sees.
28
30
31
+
An absent validity bound is omitted from the body, not encoded as null: emitting
32
+
nulls would change the canonical bytes of every credential signed before the
33
+
fields existed. A bound that is present is part of the signed body (and must be
34
+
a non-negative JSON integer, never null), so it cannot be stripped or altered
35
+
without invalidating the signature.
36
+
29
37
## Verification invariants
30
38
31
39
`verify_chain` raises the specific error for the first invariant that fails:
@@ -39,6 +47,7 @@ one implementation is the same signed object another implementation sees.
39
47
| Each hop's depth is previous + 1, and at most `max_depth`|`BROKEN_DELEGATION_LINK` / `DELEGATION_DEPTH_EXCEEDED`|
40
48
| Each hop's scope is a subset of its parent's scope |`SCOPE_ESCALATION`|
41
49
| No `credential_id` repeats |`CREDENTIAL_REPLAY`|
50
+
| Each hop's validity window, when present, contains the evaluation time |`CREDENTIAL_NOT_YET_VALID` / `CREDENTIAL_EXPIRED`|
42
51
| The root issuer is pinned by the callee for runtime authorization |`UNTRUSTED_DELEGATION_ROOT`|
43
52
44
53
Signature validity establishes who issued a chain; it does not establish that
@@ -48,6 +57,24 @@ root is absent. Offline tooling may omit that set when it only needs to check a
48
57
chain's internal structure, but structural verification alone does not authorize
49
58
work.
50
59
60
+
## Validity window
61
+
62
+
`not_before` / `not_after` bound when a credential may be used, as Unix epoch
63
+
seconds, inclusive at both ends. Either bound may appear alone; an absent bound
64
+
means unbounded on that side, which is exactly what every credential issued
65
+
before these fields existed already says.
66
+
67
+
`verify_chain` checks every hop's window against a single evaluation time:
68
+
`at_time` when the caller supplies one, the current time otherwise. Live
69
+
authorization always evaluates now. Offline audit of recorded evidence should
70
+
pass the time the action was decided (`ca2a verify-chain --at-time`), because a
71
+
window that has lapsed by audit time says nothing about validity at decision
72
+
time.
73
+
74
+
Windows are not required to nest across hops. A chain is usable only at times
75
+
inside every hop's window, so the effective window is already the intersection
76
+
of the hops'; requiring structural nesting would add no authority bound.
77
+
51
78
## Attenuation is the whole point
52
79
53
80
Attenuation, the guarantee that a child grant cannot exceed its parent, is the confused-deputy defense. Without it, B could accept a narrow task from A and then act with authority A never granted. The subset check on `scope` at every hop is what forecloses that.
Copy file name to clipboardExpand all lines: docs/spec/error-codes.md
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,6 +16,8 @@ An error also carries a human-readable message and an optional `detail`. The mes
16
16
|`BrokenDelegationLink`|`BROKEN_DELEGATION_LINK`| 409 | A hop does not chain to its stated parent, or continuity is broken: empty chain, a root credential that names a parent or has nonzero depth, a hop whose parent link or subject does not match the previous hop, or a hop depth that is not previous + 1. |
17
17
|`DelegationDepthExceeded`|`DELEGATION_DEPTH_EXCEEDED`| 403 | A chain is longer than the configured `max_delegation_depth`. Raised by `verify_chain`. |
18
18
|`CredentialReplay`|`CREDENTIAL_REPLAY`| 409 | A `credential_id` appears more than once in a single chain. Raised by `verify_chain`. |
19
+
|`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`. |
20
+
|`CredentialExpired`|`CREDENTIAL_EXPIRED`| 403 | A hop's `not_after` bound is before the evaluation time. Raised by `verify_chain`. |
19
21
|`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. |
20
22
|`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). |
21
23
|`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). |
@@ -26,7 +28,7 @@ An error also carries a human-readable message and an optional `detail`. The mes
26
28
27
29
## Which errors are live today
28
30
29
-
`ConfigError`, `InvalidCredential`, `ScopeEscalation`, `BrokenDelegationLink`, `DelegationDepthExceeded`, `CredentialReplay`, and `ProvenanceLinkBroken` 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`.
31
+
`ConfigError`, `InvalidCredential`, `ScopeEscalation`, `BrokenDelegationLink`, `DelegationDepthExceeded`, `CredentialReplay`, `CredentialNotYetValid`, `CredentialExpired`, and `ProvenanceLinkBroken` 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`.
30
32
31
33
`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).
-[Delegation Chain](delegation-chain.md) for the checks behind `ScopeEscalation`, `BrokenDelegationLink`, `DelegationDepthExceeded`, and `CredentialReplay`.
57
+
-[Delegation Chain](delegation-chain.md) for the checks behind `ScopeEscalation`, `BrokenDelegationLink`, `DelegationDepthExceeded`, `CredentialReplay`, `CredentialNotYetValid`, and `CredentialExpired`.
56
58
-[Provenance DAG](provenance-dag.md) for the checks behind `ProvenanceLinkBroken`.
57
59
-[Verification Library](verification-library.md) for `verify_chain`, `verify_chain_file`, `verify_dag`, and `cross_check_chain`.
58
60
-[Failure Modes](failure-modes.md) for how these errors map to observable runtime behavior.
-`verify_delegation_chain(chain, max_depth=8)` verifies a list of `DelegationCredential` and returns a `ChainResult` summary, or raises a `CA2AError` subtype.
15
-
-`verify_chain_file(path, max_depth=8)` loads a chain from JSON (a bare list, or `{"chain": [...]}`) and verifies it.
14
+
-`verify_delegation_chain(chain, max_depth=8, at_time=None)` verifies a list of `DelegationCredential` and returns a `ChainResult` summary, or raises a `CA2AError` subtype.
15
+
-`verify_chain_file(path, max_depth=8, at_time=None)` loads a chain from JSON (a bare list, or `{"chain": [...]}`) and verifies it.
16
+
17
+
`at_time` is the Unix time validity windows are evaluated at; `None` means the current time. An auditor replaying recorded evidence passes the time the action was decided, not its own. See [delegation chain](delegation-chain.md).
0 commit comments