# Under what conditions must an instance reject a delegated capability token whose proof-of-possession validates but whose delegation chain contains a non-canonical attenuation **Date of Record:** 2026 November 12 **Scope:** Applies to AAIF delegated capability tokens (root issuer → intermediate delegate → leaf subject) where every hop carries a proof-of-possession (PoP) signature over the request binding, and where attenuation is expressed as a monotone reduction of the capability set. Does not apply to single-hop tokens, to bearer tokens without PoP, or to chains where the root issuer is also the terminal verifier in a closed trust domain. ## Core Distinction There are two independent questions a verifier asks about a delegated capability token, and conflating them is the source of the failure mode described here: 1. **Is this token authentic and bound to the presenter?** — answered by signature verification over the token body plus a PoP check tying the presenter's key to the request (method, path, body hash, nonce, expiry). 2. **Is this token *derivable* from the root grant by a legal sequence of attenuations?** — answered by walking the delegation chain and confirming each hop is a strict, well-formed subset of its parent. The non-obvious boundary is this: **a token can pass (1) completely and still fail (2)**, and a verifier that short-circuits on PoP success will admit a capability that the root issuer never granted. The PoP signature proves *who is presenting*; it says nothing about *whether the presented scope is a legal descendant* of the root. These are orthogonal invariants and must be checked orthogonally. The specific trap is **non-canonical attenuation**: a hop that reduces the capability set in a way that is *semantically* a subset but *structurally* not the canonical attenuation of its parent — e.g., re-expanding a wildcard into an explicit enumeration that silently includes a resource the parent's wildcard excluded by a negative constraint, or normalizing a path (`/a/../b` → `/b`) at signing time in a way that changes which subtree the capability actually covers. ## What Was Observed A three-hop chain was constructed during a red-team exercise: - **Root:** `cap:{read,write} on tenant:acme/* except tenant:acme/billing/*`, signed by issuer key `K_root`. - **Hop 1 (delegate A):** attenuated to `read on tenant:acme/* except tenant:acme/billing/*`. Structurally a subset; canonical. - **Hop 2 (delegate B):** attenuated to `read on tenant:acme/reports/*`. Canonical subset of hop 1. - **Hop 3 (leaf):** presented as `read on tenant:acme/reports/*, tenant:acme/billing/2026-Q3`. This was produced by a delegate that took hop 2's positive scope and appended a resource *outside* the parent's positive scope, while carrying forward the parent's *exception* clause verbatim. Because the exception clause (`except tenant:acme/billing/*`) was copied but the positive scope was extended, the resulting set is **not** a subset of hop 2 — but the exception clause makes a naive "does the leaf's exception list match the parent's exception list?" check pass, and the PoP over the leaf body validates against delegate B's key. The verifier in the affected deployment checked: (a) every signature in the chain, (b) PoP binding for the presenter, (c) that the leaf's *exception* set was a superset of the parent's exception set (a common but wrong subset test). It did **not** check that the leaf's *positive* set was a subset of the parent's positive set after applying the parent's exception clause. The token was admitted; a read against `tenant:acme/billing/2026-Q3` succeeded. The failure is not a signature forgery and not a PoP replay. Every cryptographic check passed. The break is purely in the **set-inclusion invariant across the attenuation lattice**, and it is invisible to any check that treats attenuation as a bag of independent fields rather than as a constrained set expression. ## Condition of Validity The rejection obligation is valid — i.e., the verifier *must* reject — precisely when all of the following hold simultaneously: 1. The chain has length ≥ 2 (at least one intermediate delegate). Single-hop tokens cannot exhibit this failure because there is no parent to violate subset-hood against. 2. The parent hop expresses its scope using a construct with **negative or subtractive semantics** (exception lists, deny rules, negative globs, or path-normalization-sensitive patterns). Pure positive allow-lists with no subtraction are immune, because positive subset-hood is a simple containment test. 3. The child hop's **effective capability set**, computed as `(positive(child) \ exception(child))`, is not a subset of `(positive(parent) \ exception(parent))`. 4. The child's PoP and all chain signatures verify. (If they don't, you reject earlier for a different, more obvious reason.) Condition 3 is the load-bearing one and is the one routinely skipped. Note that checking `exception(child) ⊇ exception(parent)` is **necessary but not sufficient**; the correct test is effective-set inclusion, which requires materializing both sides. A verifier that only compares exception lists — or only compares positive lists — will admit the token. The obligation does **not** hold, and rejection would be incorrect, when the child's effective set is a genuine subset even though its syntactic form differs from the parent's canonical attenuation (e.g., wildcard `acme/*` legally narrowed to `acme/reports/*`). Canonicality of *form* is not required; subset-hood of *effective set* is. Rejecting on form alone produces false rejections and is a distinct bug. ## Decision Boundary The verifier's decision procedure, stated as a boundary: ``` admit iff sig_chain_ok and pop_binds(presenter, request) and for every adjacent pair (parent, child): effective(child) ⊆ effective(parent) ``` Where `effective(t) = positive(t) \ exception(t)`, computed **after** path canonicalization and **before** any wildcard expansion, and where wildcard expansion is performed against the same resource namespace on both sides of the comparison. Two boundary clarifications: - **Canonicalize before comparing, not before signing.** If the issuer signs a normalized form and the verifier normalizes again, double-normalization can flip a path into a subtree the parent excluded. The comparison must be performed on a single canonical form derived from the signed bytes, and that form must be the one the parent's exception was expressed against. - **Effective-set inclusion is monotone under chain extension.** If each adjacent pair satisfies inclusion, the leaf is a subset of the root by transitivity. Therefore a verifier may cache a single "effective root set" per chain and test the leaf against it, but only if it has already validated every adjacent pair — caching the root set without per-hop validation re-opens the hole, because a middle hop can be the illegal expansion and the leaf can be a legal subset of that illegal middle. The decision flips to **reject** the moment condition 3 fails, regardless of how clean the signatures and PoP are, and regardless of whether the leaf's *presented* scope would be harmless for the specific request being made. Authorization is decided against the grant lattice, not against the request's apparent innocuousness. ## Operational Effect The practical consequence is that PoP validation and attenuation validation must be separate, ordered gates, and the attenuation gate must materialize effective sets rather than compare fields. In the observed incident, the fix was a single additional check — effective-set inclusion per adjacent hop — inserted *after* PoP and *before* dispatch. It added one set-difference per hop and eliminated the class of failure entirely, including the wildcard-re-expansion and path-normalization variants that share the same root cause. The residual operational cost is that effective-set materialization requires the verifier to understand the resource namespace well enough to compute `positive \ exception` correctly, including wildcard and glob semantics. A verifier that treats scopes as opaque strings cannot perform this check and must either (a) refuse chains longer than one hop, or (b) restrict attenuation to a canonical positive-only form at issuance time so that subset-hood reduces to string containment. Both are acceptable hardening postures; silently skipping the check is not. The invariant to carry forward: **PoP answers "may this presenter act?"; attenuation answers "may this scope exist?" A token must pass both, and the second is not implied by the first.** ```python from dataclasses import dataclass from typing import FrozenSet class AttenuationViolation(RuntimeError): """Raised when a delegated capability token's effective scope is not a subset of its parent's effective scope, even though signatures and proof-of-possession validate.""" pass @dataclass(frozen=True) class Capability: positive: FrozenSet[str] exception: FrozenSet[str] def effective(self) -> FrozenSet[str]: return self.positive - self.exception @dataclass(frozen=True) class Hop: capability: Capability signature_ok: bool pop_ok: bool def verify_chain(chain: list[Hop]) -> None: if not chain: raise AttenuationViolation("empty delegation chain") for i, hop in enumerate(chain): if not hop.signature_ok: raise AttenuationViolation(f"hop {i}: signature invalid") if not hop.pop_ok: raise AttenuationViolation(f"hop {i}: proof-of-possession invalid") # Effective-set inclusion per adjacent pair. This is the gate that PoP # success does NOT imply and that field-wise exception comparison misses. for i in range(1, len(chain)): parent = chain[i - 1].capability.effective() child = chain[i].capability.effective() if not child.issubset(parent): illegal = child - parent raise AttenuationViolation( f"hop {i}: effective scope not a subset of parent; " f"illegal capabilities {sorted(illegal)}" ) def admit_request(chain: list[Hop], requested: str) -> bool: verify_chain(chain) leaf = chain[-1].capability.effective() if requested not in leaf: return False return True ```