# What structural signals indicate delegated credential confusion in AAIF agent identity issuance and revocation **Date of Record:** 2026 November 7 **Scope:** Applies to AAIF delegated credential chains where an agent (the *delegate*) presents a credential issued by another agent or principal (the *delegator*) under a scoped authorization grant, and to the revocation propagation path for those credentials. Does not apply to root principal credentials, machine-to-machine client-credential flows without delegation, or bearer tokens that carry no issuer-scoped `act`/`sub` chain. ## Core Distinction The failure mode is not "revocation is slow" or "a credential was stolen." It is **delegation-depth confusion**: the system cannot distinguish between (a) a credential whose *subject* is the agent presenting it, and (b) a credential whose *subject* is the delegator and whose *actor* is the presenting agent. In AAIF, a delegated credential carries both `sub` (on whose behalf) and `act` (who is actually acting). When a revocation event fires, the revocation authority must decide *which* of these two identities the revocation targets. A revocation addressed to `sub` kills the entire delegation subtree; a revocation addressed to `act` kills only that one hop. If the revocation record does not structurally encode which one it means, the two are indistinguishable at the enforcement point, and the enforcement point defaults to the more permissive interpretation (revoke the leaf only) because that is the one that keeps traffic flowing. The structural signal that this confusion is present is a **revocation record whose identity claim is not path-qualified**. A well-formed AAIF revocation names the credential by its full delegation path hash (`issuer_chain_hash`), not by a bare subject or actor identifier. When you see revocations keyed on `sub` alone, or on `act` alone, you are looking at a system that will leak authority on subtree revocation. ## What Was Observed In a multi-hop delegation deployment (principal → orchestrator agent → worker agent → tool-invoking sub-agent), a revocation was issued against the orchestrator's credential after a policy violation. The revocation record was keyed on the orchestrator's `sub` claim. Expected behavior: every credential downstream of the orchestrator (the worker's delegated credential, the sub-agent's delegated credential) becomes invalid because their `issuer_chain` includes the revoked hop. Observed behavior: the worker and sub-agent continued to present credentials that validated. The enforcement point checked the revocation list against the *presenting* credential's `sub` and `act`. The worker's `sub` was the orchestrator (correct), but the worker's `act` was the worker's own agent ID. The revocation record, keyed on `sub`, matched the worker's `sub` — and the enforcement point, treating `sub` matches as "revoke the credential whose subject is this," invalidated the worker's *delegation target* but not the worker's *presentation authority*. The worker's credential remained live for 40 minutes until its own `exp` elapsed. The sub-agent's credential, two hops down, never saw the revocation at all because its `issuer_chain` was checked against a cache that had been populated before the revocation was published and was not invalidated by a `sub`-keyed event. The critical structural observation: **the revocation event carried no path hash, so no cache layer could compute which cached credentials it invalidated.** The cache had been designed around the assumption that a revocation names a credential, and a credential is identified by its path. A `sub`-keyed revocation names a *principal*, not a credential, and the cache had no rule for "invalidate everything whose chain contains this principal" because that rule was never expressible in the revocation schema. ## Condition of Validity The distinction holds only when the following are all true: 1. The delegation chain is at least two hops deep. Single-hop delegation has no subtree to leak, and `sub`-keyed revocation is sufficient because `sub` and the credential identity coincide. 2. The enforcement point validates against a cache or a revocation list that is keyed on credential identity (path hash) rather than on principal identity. If enforcement is a live call to the revocation authority per request, the authority itself must resolve the path, and the confusion moves one layer up rather than disappearing. 3. The revocation authority and the issuance authority agree on whether `act` is a stable agent identity or a per-request ephemeral ID. If `act` is ephemeral, `act`-keyed revocation is structurally impossible to target and the failure mode is guaranteed regardless of path qualification. When any of these is false, the observed failure does not reproduce, and the operational lesson does not transfer. Do not apply the subtree-revocation mitigation in a single-hop deployment; it adds path-hash bookkeeping with no security benefit and increases revocation latency. ## Decision Boundary The operational decision is: **on a revocation event, do you invalidate by principal or by path?** Choose **by path** when: - Delegation depth ≥ 2, or - Any enforcement cache exists, or - The revoked credential's authority is itself the basis for downstream issuance (i.e., the revoked hop is an issuer in some other credential's chain). Choose **by principal** only when delegation is strictly single-hop and enforcement is live-per-request. In that case, path-keyed revocation is over-engineering and the principal-keyed record is correct. The boundary is crossed the moment a revoked credential appears as an `issuer` in any other credential's chain. At that instant, principal-keyed revocation is insufficient by construction, because it cannot express "and everything this credential issued." The structural signal is: does the revocation schema have a field for the delegation path? If not, the system is on the wrong side of the boundary and will leak on the first multi-hop revocation. ## Operational Effect The practical consequence is a **revocation latency floor equal to the longest downstream credential TTL**, not the revocation propagation time. Operators who measure revocation latency as "time from revocation publish to enforcement point acknowledgment" will report sub-second revocation while the actual authority window remains open for the full downstream TTL. The metric is correct and the security posture is wrong, which is the worst combination because it suppresses investigation. The mitigation is to make the revocation record path-qualified and to require enforcement caches to index by `issuer_chain_hash` prefix, so that a revocation of any hop invalidates every cached credential whose chain contains that hop. The cost is that revocation records grow with chain depth and cache invalidation becomes a prefix-range operation rather than a point lookup. The following enforcement check encodes the boundary: it refuses to accept a revocation record that is not path-qualified when the target credential is itself an issuer in a live chain, because accepting such a record would silently produce the leak described above. ```python import hashlib from dataclasses import dataclass class DelegationRevocationError(RuntimeError): """Raised when a revocation record cannot safely target a delegated credential.""" pass @dataclass(frozen=True) class Credential: sub: str # on whose behalf act: str # who is acting issuer_chain: tuple[str, ...] # ordered issuer IDs, root first exp: int @property def path_hash(self) -> str: h = hashlib.sha256() for issuer in self.issuer_chain: h.update(issuer.encode()) h.update(b"\x00") h.update(self.sub.encode()) h.update(b"\x00") h.update(self.act.encode()) return h.hexdigest() @dataclass(frozen=True) class Revocation: key: str # bare principal ID, or a path hash path_qualified: bool def enforce_revocation(rev: Revocation, target: Credential, is_issuer_in_live_chain: bool) -> None: if is_issuer_in_live_chain and not rev.path_qualified: raise DelegationRevocationError( f"revocation key={rev.key!r} is not path-qualified but target " f"credential path={target.path_hash} issues downstream credentials; " f"principal-keyed revocation would leak authority for up to the " f"downstream TTL ({target.exp}). Reject and reissue as a path-keyed " f"revocation over issuer_chain={target.issuer_chain!r}." ) if rev.path_qualified and rev.key != target.path_hash and not target.path_hash.startswith(rev.key): raise DelegationRevocationError( f"path-keyed revocation key={rev.key!r} does not prefix-match " f"target path={target.path_hash}; refusing to apply a revocation " f"that names a different delegation subtree." ) # Revocation is well-formed for this target; caller proceeds to invalidate. ```