Skip to content

Latest commit

 

History

History
84 lines (64 loc) · 5.03 KB

File metadata and controls

84 lines (64 loc) · 5.03 KB

Delegation Chain

A delegation chain is a root-to-leaf list of signed credentials. It is the primitive that lets Agent A hand a bounded slice of its authority to B, and B a still-smaller slice to C, with each grant provably within the one above it.

Credential

A DelegationCredential has the following signed body plus a detached signature:

Field Type Meaning
credential_id string Unique id of this hop
issuer hex Ed25519 public key of the delegator
subject hex Ed25519 public key of the delegate
scope set of strings Capabilities granted at this hop
depth int 0 at the root, +1 per hop
parent_id string or null credential_id of the parent hop; null at the root
signature hex Ed25519 over the canonical body, by the issuer
not_before int, optional Unix epoch seconds; the credential is not valid before this time (inclusive)
not_after int, optional Unix epoch seconds; the credential is not valid after this time (inclusive)

Canonicalization

The signed bytes are the RFC 8785 (JSON Canonicalization Scheme) encoding of the body: keys sorted by UTF-16 code units, JCS minimal string escaping, non-ASCII emitted literally as UTF-8, integers in shortest decimal form, scope as a sorted array. This is the byte string signed and verified. Using JCS makes cA2A signatures cross-verifiable with agent-manifest and any other conforming implementation. See ca2a_runtime.canonical.

The wire object is strict: fields are not coerced, unknown fields are rejected, keys and signatures must use their exact lowercase hex encodings, depth must be a non-negative JSON integer (not a boolean or float), and scope must be a non-empty array of unique non-empty strings. This ensures the object accepted by one implementation is the same signed object another implementation sees.

An absent validity bound is omitted from the body, not encoded as null: emitting nulls would change the canonical bytes of every credential signed before the fields existed. A bound that is present is part of the signed body (and must be a non-negative JSON integer, never null), so it cannot be stripped or altered without invalidating the signature.

Verification invariants

verify_chain raises the specific error for the first invariant that fails:

Invariant Error on violation
Every hop's signature verifies against its issuer INVALID_CREDENTIAL
Root has no parent and depth 0 BROKEN_DELEGATION_LINK
Each hop's parent_id equals the previous credential_id BROKEN_DELEGATION_LINK
Each hop's issuer equals the previous hop's subject BROKEN_DELEGATION_LINK
Each hop's depth is previous + 1, and at most max_depth BROKEN_DELEGATION_LINK / DELEGATION_DEPTH_EXCEEDED
Each hop's scope is a subset of its parent's scope SCOPE_ESCALATION
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

Signature validity establishes who issued a chain; it does not establish that the issuer is trusted. A live callee therefore supplies its local trusted_root_issuers set when verifying a request and fails closed when the root is absent. Offline tooling may omit that set when it only needs to check a chain's internal structure, but structural verification alone does not authorize work.

Validity window

not_before / not_after bound when a credential may be used, as Unix epoch seconds, inclusive at both ends. Either bound may appear alone; an absent bound means unbounded on that side, which is exactly what every credential issued before these fields existed already says.

verify_chain checks every hop's window against a single evaluation time: at_time when the caller supplies one, the current time otherwise. Live authorization always evaluates now. Offline audit of recorded evidence should pass the time the action was decided (ca2a verify-chain --at-time), because a window that has lapsed by audit time says nothing about validity at decision time.

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.

Attenuation is the whole point

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.

Relationship to agent-manifest

These semantics mirror the signed A2A delegation chain implemented and tested in agent-manifest, including cross-manifest replay protection and HITL approval signing. cA2A's runtime calls that verifier on inbound peer requests; this module is the runtime-side model.