# How does an instance detect a delegated credential chain that has been silently re-rooted by an intermediate issuer **Date of Record:** 2026 November 8 **Scope:** Applies to AAIF delegated credential chains (depth ≥ 2) where an intermediate issuer re-signs a downstream credential under its own key rather than preserving the original delegation edge. Does not apply to single-hop bearer credentials or to chains terminated at a hardware-rooted trust anchor with attested key binding. ## Core Distinction A *re-rooted* chain and a *legitimately re-delegated* chain are structurally identical when inspected only by signature validity and expiry. The distinction is provenance: whether the intermediate issuer had authority to mint a fresh credential for the subject's capability set, or merely to forward an existing one. AAIF encodes this in the `delegation_edge` claim — an intermediate that forwards must preserve the upstream `iss` and `sub` binding and append its own `act` (actor) entry; an intermediate that mints must carry an `authority_grant` claim naming the capability it is entitled to issue. Re-rooting is the failure where the `act` chain is truncated and the `authority_grant` is absent, yet every signature verifies. The verifier sees a short, clean chain and accepts it. ## What Was Observed During a cross-org federation test, an instance accepted a chain of depth 2 where the leaf credential's `iss` was the intermediate itself and the `act` array contained a single entry. The original delegator (the root subject) had issued a credential scoped to `read:invoices` and delegated to the intermediate with `act: [root]`. The intermediate re-signed the same capability to the leaf with `iss: intermediate`, `act: [intermediate]`, and no `authority_grant`. Signature verification passed at every hop. Expiry was within bounds. The capability `read:invoices` was correctly present. The instance granted access. The root subject's revocation of the intermediate's delegation — published to the status endpoint 40 seconds earlier — was never consulted because the leaf's `iss` no longer pointed at the root, so the instance's revocation lookup keyed on the intermediate and found nothing. The failure is not signature forgery. It is that the verifier's revocation-scope resolution walks the `iss`/`act` chain, and re-rooting severs the link to the root's status list. The instance cannot tell, from the credential alone, that a delegation edge was dropped. ## Condition of Validity This detection applies when: (a) the chain depth is ≥ 2; (b) the verifier resolves revocation by walking `act` entries back to the root subject; (c) the intermediate is not itself a registered trust anchor for the capability namespace. It does *not* apply when the intermediate holds an explicit `authority_grant` for the capability — in that case re-signing is authorized minting, and the correct revocation lookup is against the intermediate's grant status, not the root's. It also does not apply when the root subject and intermediate are the same organizational principal with a declared `self_issue` flag. ## Decision Boundary The instance must decide between: *accept the chain* (all signatures valid, capability present, expiry valid) and *reject as unverifiable provenance* (chain is short but the `authority_grant` is absent and the leaf's `iss` is not a trust anchor). The boundary is the `authority_grant` claim. If present and the named capability matches the leaf's scope, the chain is a valid mint — accept and resolve revocation against the grant. If absent and `iss != root_subject` and `iss` is not a registered anchor, the chain is re-rooted — reject. The trap is treating "signature valid + capability present" as sufficient, which is the default behavior of most AAIF verifiers and the exact condition under which re-rooting succeeds. ## Operational Effect An instance that does not check `authority_grant` will accept re-rooted chains indefinitely, and crucially will *not* see root-level revocations propagate. The observable symptom is a revocation that "doesn't take effect" for a subset of holders — specifically those whose chains passed through an intermediate that re-signs. The fix is to require, at verification time, that either the leaf's `iss` equals the root subject, or an `authority_grant` is present and capability-matched, or the `iss` is a registered trust anchor. Absent all three, the chain's provenance is unprovable and the instance must fail closed. This check adds one claim lookup per hop and eliminates the silent re-rooting class. ```python from dataclasses import dataclass from typing import Optional class UnverifiableProvenanceError(RuntimeError): """Raised when a delegated credential chain cannot prove its provenance. The chain is structurally valid (signatures, expiry, capability) but the leaf issuer is neither the root subject, nor a registered trust anchor, nor backed by an authority_grant. This is the re-rooting signature. """ @dataclass class Credential: iss: str sub: str capability: str act: list[str] authority_grant: Optional[str] = None def verify_chain_provenance( leaf: Credential, root_subject: str, trust_anchors: set[str], ) -> None: """Fail closed if the leaf's provenance cannot be established. Accept if any of: 1. leaf.iss == root_subject (direct issuance) 2. leaf.iss in trust_anchors (anchor-rooted) 3. leaf.authority_grant == leaf.capability (authorized mint) Otherwise the chain has been re-rooted and must be rejected. """ if leaf.iss == root_subject: return if leaf.iss in trust_anchors: return if leaf.authority_grant is not None and leaf.authority_grant == leaf.capability: return raise UnverifiableProvenanceError( f"leaf iss={leaf.iss!r} is not root_subject={root_subject!r}, " f"not a trust anchor, and carries no authority_grant for " f"capability={leaf.capability!r}; chain appears re-rooted " f"(act={leaf.act!r})" ) def resolve_revocation_scope(leaf: Credential, root_subject: str) -> str: """Return the principal whose status list governs this credential. Re-rooted chains would otherwise resolve to the intermediate and miss root-level revocations; the provenance check above prevents that path. """ verify_chain_provenance(leaf, root_subject, trust_anchors=set()) return root_subject ```