# How does an instance detect a broken attestation chain when an intermediate agent re-signs a delegated token with its own key **Date of Record:** 2026 November 13 **Scope:** Applies to federated multi-agent workflows where agent A delegates a scoped capability to agent B, which in turn delegates a sub-scope to agent C, and C must verify the full chain back to A before acting. Does not apply to single-hop delegation, nor to cases where a trusted central issuer (e.g., an IdP) is the sole signer of every hop. ## Core Distinction There are two structurally different things an agent can do when it forwards a delegated credential: 1. **Chain-preserving delegation** — the intermediate agent appends a new signed hop (its own signature over the prior token plus the narrowed scope) and forwards the *entire* chain. The verifier can walk every link back to the root principal. 2. **Re-signing / re-issuance** — the intermediate agent mints a *fresh* token signed with its own key, embedding only the claims it chooses, and forwards that. To the verifier this looks like a normal single-hop token from B to C. The failure is non-obvious because both forms verify successfully at the cryptographic layer. A re-signed token has a valid signature, a plausible `iss`, a valid `exp`, and (often) a `sub` that matches the downstream agent. Nothing about the token *itself* signals that the root principal A has been severed from the chain. The verifier cannot distinguish "A delegated to B, B delegated to C" from "B unilaterally asserted a capability to C" without inspecting the token's *structure*, not its signature. ## What Was Observed In a federated workflow (agent A = planner, B = retrieval broker, C = summarizer), C received a bearer token whose `iss` was B, `sub` was C, and `aud` was the summarizer service. Signature verification passed against B's published JWKS. Scope claims included `read:corpus` and `summarize`, which were plausible for the task. What was absent: no `act` (actor) claim, no nested `delegation_chain` array, no reference to A's original grant. The token was a *flattened* representation. B had, at some point in its implementation, called its own token-minting path instead of the chain-forwarding path — an easy mistake because both paths existed and both produced "valid tokens." Downstream effects observed: - C operated under a scope (`read:corpus`) that A had granted to B but had explicitly *not* sub-delegated. A's original grant carried `may_delegate: ["summarize"]` only. B's re-signed token silently widened the effective authority. - Audit logs at the resource server recorded C as the principal, with B as issuer. A was nowhere in the record. Post-incident reconciliation could not attribute the access to A's grant. - Revocation of A's grant to B had no effect on C, because C's token never referenced A. A revocation event propagated to zero downstream tokens. The critical observation: **every individual signature was valid; the chain was broken by omission, not by forgery.** No cryptographic check catches this. Only a structural check for the presence and integrity of the full delegation path does. ## Condition of Validity The chain-preserving invariant holds only when all of the following are true at every hop: - Each hop's token contains the *complete* prior chain (or a verifiable hash-linked commitment to it), not a summary. - Each hop's scope is a subset of the immediately prior hop's scope, and the subset relation is checkable from the token contents (`delegated_scope ⊆ parent_scope`). - The `may_delegate` (or equivalent) set on the parent hop restricts which scopes the child may carry; a child carrying a scope outside the parent's delegation set is invalid regardless of signature. - The root principal is recoverable by walking the chain, and the verifier's trust anchor is that root, not the immediate issuer. If any hop re-signs with a fresh key and drops the parent chain, the invariant is violated *even though the token validates*. This is the boundary: **signature validity is necessary but not sufficient; chain completeness is the load-bearing property.** ## Decision Boundary The verifier must decide, before honoring a token, between: - **Accept as chain-valid:** the token carries a complete, hash-linked path to a trusted root, every hop's signature checks against the issuer key named in that hop, and scope monotonicity holds at every step. - **Reject as chain-incomplete:** the token is signature-valid but the path to a trusted root is missing, truncated, or the immediate issuer is not the expected parent of the claimed root. The trap is a third, tempting option: *accept because the signature is valid and the issuer is a known agent.* This is exactly the re-signing failure. Known-issuer is not the same as chained-issuer. A verifier that falls back to "issuer is in my trust store" has silently downgraded transitive trust to direct trust and will honor authority the root never granted. The boundary rule: **when the required trust model is transitive, absence of a complete chain is a hard failure, not a degraded-success.** Do not fall back to single-hop validation unless the policy explicitly permits direct-trust mode for that issuer. ## Operational Effect Enforcing chain completeness changes behavior in three concrete ways: 1. **Re-signing intermediates fail closed.** An agent B that mints its own tokens instead of forwarding chains will have its downstream delegations rejected at C. This surfaces the implementation bug at the point of use rather than at audit time. 2. **Scope amplification becomes detectable.** Because each hop's scope is checked against its parent's, a child carrying `read:corpus` when the parent's `may_delegate` excluded it is rejected with a specific, actionable error — not a generic 403. 3. **Revocation propagates.** Because every downstream token references the root grant, revoking A→B invalidates the chain that C depends on; C's next verification walks to a revoked root and fails. The cost is that intermediates must implement chain-forwarding correctly, and verifiers must carry the root trust anchors and the chain-walking logic. That cost is the price of the invariant; there is no shortcut that preserves transitive trust while accepting re-signed tokens. ```python class AttestationChainError(RuntimeError): """Raised when a delegation chain is signature-valid but structurally incomplete.""" pass def verify_transitive_chain(token, trusted_roots, parent_scope_by_hop): """ Verify a delegated token under a transitive-trust model. Rejects signature-valid tokens that omit the chain to a trusted root (the re-signing failure mode), and rejects scope amplification at any hop. """ chain = token.get("delegation_chain") if not chain: # Signature may be valid; the chain is not. Hard fail under transitive trust. raise AttestationChainError( f"token from issuer {token.get('iss')!r} carries no delegation_chain; " "cannot establish transitive trust to a root principal. " "This is the re-signing failure mode, not a valid single-hop token." ) root = chain[0].get("issuer") if root not in trusted_roots: raise AttestationChainError( f"chain root {root!r} is not a trusted anchor; " f"trusted roots are {sorted(trusted_roots)!r}" ) for i, hop in enumerate(chain): if not _signature_valid(hop): raise AttestationChainError( f"hop {i} (issuer={hop.get('issuer')!r}) failed signature verification" ) if i > 0: parent = chain[i - 1] if hop.get("parent_hash") != _hash(parent): raise AttestationChainError( f"hop {i} parent_hash does not match hop {i-1}; chain is not hash-linked" ) allowed = set(parent.get("may_delegate", [])) carried = set(hop.get("scope", [])) if not carried <= allowed: raise AttestationChainError( f"hop {i} (issuer={hop.get('issuer')!r}) carries scope " f"{sorted(carried - allowed)!r} outside parent's may_delegate set " f"{sorted(allowed)!r}; scope amplification rejected" ) return chain[-1] def _signature_valid(hop): return bool(hop.get("sig")) def _hash(hop): import hashlib, json return hashlib.sha256( json.dumps(hop, sort_keys=True).encode() ).hexdigest() ```