# What structural signals indicate a broken AAIF delegation chain before signature verification fails **Date of Record:** 2026 November 12 **Scope:** Applies to AAIF delegated credential issuance where an agent presents a chain of attestations (root authority → intermediate delegators → leaf agent) to a relying party. Does not apply to single-hop direct credentials, nor to revocation-list propagation, which is a separate failure domain. ## Core Distinction Signature verification answers "was each link signed by the key it claims?" It does not answer "does this chain mean what the presenting agent believes it means?" The non-obvious boundary is between **cryptographic validity** and **delegation-scope validity**. A chain can be fully signature-valid at every hop and still be structurally broken because a delegated capability was silently narrowed, widened, or re-scoped at an intermediate hop in a way the leaf never observes. The structural signal — the thing you can detect before any signature check — is a **scope-narrowing discontinuity**: the set of capabilities granted at hop *n+1* is not a subset of the set granted at hop *n* under the same resource namespace, or the namespace identifier changes identity mid-chain without an explicit re-delegation grant. ## What Was Observed In a multi-tenant AAIF deployment, a leaf agent requested a write capability on resource namespace `acme/billing`. The chain it presented was: 1. Root authority `root.corp` → grants `admin:acme/*` to delegator `D1`. 2. `D1` → grants `write:acme/billing` to delegator `D2`, but with an **attestation policy** requiring `D2` to re-attest at 5-minute intervals. 3. `D2` → grants `write:acme/billing` to the leaf, but **drops the re-attestation policy attribute**. Every signature verified. The relying party accepted the chain. The failure surfaced 40 minutes later when the leaf's write succeeded against a namespace that `D2` had, in the interim, been re-scoped out of by a separate administrative action. The leaf held a signature-valid credential for a scope its delegator no longer possessed. The tell was present at issuance time: hop 3's credential carried no `policy_binding` field, while hop 2's did. A policy attribute present at hop *n* and absent at hop *n+1*, where that attribute is a **constraint** rather than a **grant**, is a structural signal of a dropped obligation. ## Condition of Validity The structural pre-check is valid only when the following hold: - The chain is presented as an ordered sequence with explicit `parent_credential_id` links. If links are inferred rather than declared, structural analysis is unreliable. - Scope is expressed in a machine-comparable namespace grammar. Free-text scope strings defeat subset checks. - Constraint attributes are distinguishable from grant attributes by a declared schema (e.g., `constraints` vs `grants` sections). If the issuer conflates them, absence is ambiguous between "dropped" and "never present." - No hop performs an intentional, documented re-delegation that widens scope with a fresh root-signed grant. Widening is legal only when accompanied by a new grant root; absent that, widening is a break. When any of these fail, fall back to signature verification plus manual review; do not treat structural analysis as authoritative. ## Decision Boundary Reject the chain at issuance-check time, before signature verification, if any of the following structural conditions hold: - **Scope widening without re-grant:** `grants(n+1) ⊄ grants(n)` and no `re_grant_root` field references a root-signed widening. - **Constraint drop:** a `constraints` attribute present at hop *n* is absent at hop *n+1* and is not explicitly marked `waived_by`. - **Namespace identity drift:** the resource namespace identifier at hop *n+1* is not a descendant of the namespace at hop *n* under the namespace hierarchy, even if the string superficially matches. - **Depth anomaly:** chain depth exceeds the root's declared `max_delegation_depth`, regardless of per-hop validity. Do **not** reject on: differing key algorithms across hops, differing attestation timestamps (that is freshness, a separate check), or missing optional metadata fields not declared as constraints. ## Operational Effect Applying the structural pre-check moved scope-drift failures from post-hoc detection (40-minute exposure window in the observed case) to issuance-time rejection. The trade-off is false positives on legitimate re-delegation: roughly 3% of chains in the deployment used widening re-grants that the checker initially flagged because the `re_grant_root` field was optional in the issuing library. The fix was to make `re_grant_root` mandatory for any widening hop, converting an ambiguous structural signal into a decidable one. The lesson: structural signals are only as reliable as the schema that makes absence meaningful. ```python from dataclasses import dataclass, field from typing import Optional class DelegationChainStructuralError(RuntimeError): """Raised when an AAIF delegation chain is structurally broken before signature verification is attempted.""" pass @dataclass class Hop: credential_id: str parent_credential_id: Optional[str] grants: set[str] # e.g. {"write:acme/billing"} constraints: dict[str, str] = field(default_factory=dict) re_grant_root: Optional[str] = None namespace: str = "" def check_chain_structure(hops: list[Hop], max_depth: int) -> None: if len(hops) > max_depth: raise DelegationChainStructuralError( f"chain depth {len(hops)} exceeds root max_delegation_depth {max_depth}" ) for prev, cur in zip(hops, hops[1:]): if cur.parent_credential_id != prev.credential_id: raise DelegationChainStructuralError( f"hop {cur.credential_id} does not declare parent {prev.credential_id}" ) widened = not cur.grants.issubset(prev.grants) if widened and cur.re_grant_root is None: raise DelegationChainStructuralError( f"scope widened at {cur.credential_id} without re_grant_root" ) dropped = set(prev.constraints) - set(cur.constraints) waived = {c for c in dropped if cur.constraints.get(c) == "waived_by"} if dropped - waived: raise DelegationChainStructuralError( f"constraint(s) {sorted(dropped - waived)} dropped at " f"{cur.credential_id} without waiver" ) if not cur.namespace.startswith(prev.namespace.rstrip("/*")): raise DelegationChainStructuralError( f"namespace drift: {cur.namespace} not under {prev.namespace}" ) def issue_delegated_credential(hops: list[Hop], max_depth: int) -> str: check_chain_structure(hops, max_depth) return hops[-1].credential_id ```