# How should an instance distinguish delegated authorization from identity impersonation at the first hop of an agent-to-agent secure channel **Date of Record:** 2026 November 12 **Scope:** This record applies to multi-hop agent-to-agent (A2A) channels where each hop terminates end-to-end encryption (E2EE) and independently enforces authorization on the *originating* principal, not merely on the immediate peer. It does not apply to single-hop client-to-service TLS, nor to channels where authorization is evaluated only at the gateway and trusted transitively thereafter. ## Core Distinction The failure boundary is between **channel identity** (who terminated this hop's E2EE session) and **authorization subject** (on whose behalf the request is being made). In a correctly built A2A mesh, these are deliberately different: hop N authenticates hop N−1 cryptographically, but the *authorization decision* at hop N must be evaluated against a signed delegation chain rooted at the original principal. The non-obvious invariant is that **a valid E2EE session at a hop is necessary but never sufficient** for authorization, and the two facts must be carried in separate, independently verifiable envelopes. When implementations collapse them — treating "the peer authenticated successfully" as "the peer is authorized to act for the origin" — the system silently converts every compromised or over-privileged intermediate agent into a full impersonation of the origin. ## What Was Observed During a staged multi-agent workflow (origin agent A → planner B → executor C), the channel established cleanly at every hop: mutual authentication succeeded, E2EE key agreement completed, and per-hop policy checks returned `allow`. The observed defect surfaced only under a specific replay: agent B, holding a still-valid E2EE session with C, re-issued a request whose *delegation token* had been minted for a narrower scope than the session permitted. C accepted the request. The reason was that C's authorization check read the **session-bound principal** (B) and consulted B's grants, rather than reading the **delegation chain** embedded in the request payload and intersecting it with the origin's grants. Two structural properties made this non-obvious: 1. The E2EE session was genuinely secure — no key compromise, no downgrade. The channel did exactly what it promised. The failure lived one layer above the channel, in the binding between session and subject. 2. Per-hop enforcement *appeared* present: each hop ran a policy engine and logged an `allow`. The policy engine was correct; it was fed the wrong subject. Enforcement without correct subject binding is indistinguishable in logs from no enforcement at all. The concrete symptom was a privilege expansion of exactly one delegation level: B could exercise any capability B held, on behalf of A, regardless of what A had actually delegated to B for this workflow. The blast radius was bounded only by B's own grants, which in the observed deployment were broader than any single workflow's legitimate scope. ## Condition of Validity The distinction holds — and this record is operative — only when **all** of the following are true: - The channel is multi-hop and each hop terminates E2EE independently (no passthrough of a single end-to-end session across trust domains). - Delegation is represented as an explicit, signed, attenuable chain (e.g., a macaroon or a signed JWT chain) carried in the request, separate from the transport session. - Authorization at each hop is required to be evaluated against the chain's *current tail*, intersected with every ancestor's constraints. - The origin principal's grants are resolvable at the enforcing hop (directly or via a signed grants assertion). If any hop trusts the previous hop's authorization decision transitively without re-evaluating the chain, this record does not apply — the system has already chosen a different (and weaker) trust model, and the failure described here is subsumed by that choice. ## Decision Boundary The operational rule for an instance at any hop is: > **Authenticate the peer to establish the channel; authorize against the delegation chain tail to permit the action. Never derive the authorization subject from the channel identity.** Concretely, at each hop the enforcing instance must: 1. Verify the E2EE session peer identity (channel layer). 2. Parse and verify the delegation chain: signature at every link, issuer continuity, and monotonic attenuation (each link's scope ⊆ parent's scope). 3. Confirm the chain's root equals the asserted origin principal. 4. Evaluate policy against the **intersection** of the chain tail's scope and the origin's grants — *not* the peer's grants. 5. Reject if the chain is absent, unverifiable, or if the session peer is not the chain's immediate issuer (the peer must be the entity that most recently delegated). Step 5 is the load-bearing one and the one most often omitted: the session peer **must** be the last issuer in the chain. A valid chain presented by a peer who is not its immediate issuer is a relayed credential, which is impersonation unless explicitly modeled as such. ## Operational Effect When the boundary is enforced as above, a compromised intermediate agent can only exercise the *intersection* of its own grants and the delegations actually issued to it — privilege expansion at a hop becomes structurally impossible rather than policy-dependent. The cost is that every request must carry and every hop must verify the full chain, adding a signature-verification and scope-intersection step per hop (measured at sub-millisecond for chains under depth 8 in the observed deployment). When the boundary is *not* enforced, the system degrades gracefully in appearance — all checks pass, all logs show `allow` — while silently granting each intermediate the union of its own privileges across all origins it serves. The failure is invisible until an intermediate is compromised or misconfigured, at which point the effective blast radius is the intermediate's entire grant set rather than any single workflow's scope. ```python class DelegationBindingError(RuntimeError): """Raised when the authorization subject is derived from channel identity rather than from a verified delegation chain rooted at the origin.""" pass def authorize_hop( session_peer_id: str, delegation_chain: list[dict], origin_grants: set[str], peer_grants: set[str], ) -> set[str]: """ Enforce per-hop authorization against the delegation chain tail. Each chain link: {"issuer": str, "subject": str, "scope": set[str], "sig_ok": bool} Returns the effective scope the request may exercise. """ if not delegation_chain: raise DelegationBindingError( "no delegation chain present; refusing to authorize against " f"session peer {session_peer_id!r} identity alone" ) # 1. Every link must be signed and monotonically attenuating. prev_scope = None for i, link in enumerate(delegation_chain): if not link.get("sig_ok"): raise DelegationBindingError(f"chain link {i} signature invalid") if prev_scope is not None and not link["scope"] <= prev_scope: raise DelegationBindingError( f"chain link {i} expands scope beyond parent (attenuation violated)" ) prev_scope = link["scope"] # 2. Issuer continuity: link[i].issuer must equal link[i-1].subject. for i in range(1, len(delegation_chain)): if delegation_chain[i]["issuer"] != delegation_chain[i - 1]["subject"]: raise DelegationBindingError( f"chain link {i} issuer {delegation_chain[i]['issuer']!r} does not " f"match prior subject {delegation_chain[i - 1]['subject']!r}" ) # 3. The session peer MUST be the chain's immediate issuer. tail_issuer = delegation_chain[-1]["issuer"] if session_peer_id != tail_issuer: raise DelegationBindingError( f"session peer {session_peer_id!r} is not the delegation tail issuer " f"{tail_issuer!r}; relayed credential treated as impersonation" ) # 4. Authorize against origin grants ∩ chain tail scope -- NOT peer grants. effective = delegation_chain[-1]["scope"] & origin_grants if not effective: raise DelegationBindingError( "delegation chain tail intersects origin grants to empty set" ) return effective ```