What identity federation invariants must survive when trust chains span independent authorities?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session verifies identity through a trust chain that spans independent identity authorities (A trusts B, B trusts C, C is authoritative), and the chain must preserve trust semantics despite authorities having different policies, revocation models, or security assumptions. Does not apply to single-authority identity systems or ecosystems with unified trust roots.
Core Distinction
The separation between **identity assertion** (an authority says "this is X") and **federated identity verification** (the full chain of authorities validates identity across policy boundaries). Relying on assertion creates the **Federation Trust Gap**: the receiver sees an identity assertion from an authority it trusts, but the authority's trust itself depends on another authority with different policies, and the composition of trust may not support the receiver's actual requirements.
What Was Observed
In cross-federation agent architectures, failing to preserve federation invariants produces three primary failure modes:
- **Policy Composition Failure:** Authority A has strict revocation (immediate), Authority B has lenient revocation (24-hour lag). The receiver trusts A, A trusts B, B vouches for an identity that was revoked 12 hours ago. The receiver sees valid identity because B has not yet propagated the revocation.
- **Assurance Level Drift:** Authority A requires hardware-backed keys, Authority B allows software keys. The trust chain composes A→B→identity, but the receiver's assurance requirement (hardware) is violated at the B→identity link, and the chain appears valid.
- **Revocation Propagation Failure:** Authority B revokes an identity, but the revocation signal does not reach Authority A (B's policies do not include propagating revocations upstream). Authority A continues to assert the identity is valid.
Recorded Elements
- **`per_authority_policy_receipt`:** For each authority in the chain, its cryptographic proof of policy parameters (revocation timing, assurance level, verification rigor), allowing the receiver to compare composition against requirements.
- **`assurance_level_composition_signature`:** The cryptographic proof that the composed assurance level of the chain meets the receiver's minimum (the weakest link determines composed assurance).
- **`revocation_propagation_completeness_receipt`:** For each authority, proof that revocations propagate upstream (or explicit acknowledgment that they do not).
- **`federation_policy_compatibility_signature`:** The cryptographic proof that all authorities in the chain are policy-compatible with the receiver's requirements (no authority's policies violate the receiver's minimums).
Condition of Validity
* **Applies when:** Identity verification spans multiple independent authorities, authorities have different policies, and the receiver has specific assurance requirements. * **Does not apply when:** All authorities are under unified policy, or the receiver has no specific assurance requirements.
Decision Boundary
- **Does the trust chain span multiple authorities?** If no, single-authority verification applies. If yes, proceed.
- **Are all authority policies within the receiver's requirements?** If yes, the chain meets requirements. If no, the chain's assurance is limited by the weakest authority.
- **Does revocation propagate upstream across all authorities?** If yes, revocations reach the receiver within acceptable time. If no, revocation gaps exist.
- **Conversely, when can a receiver accept a chain without policy comparison?** Only when the receiver's assurance requirements are minimal (or the chain is verified by an out-of-band trusted check that composes the policies appropriately).
Operational Effect
- **Policy Composition Awareness:** The composed assurance is explicitly the weakest link, not the strongest.
- **Revocation Propagation:** Gaps in upstream propagation are detected.
- **Assurance Level Integrity:** Composed assurance matches the weakest authority's assurance, not the strongest.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class FederationVerdict(Enum):
SINGLE_AUTHORITY = auto()
CHAIN_COMPATIBLE = auto()
WEAKEST_LINK_INSUFFICIENT = auto()
REVOCATION_GAP = auto()
@dataclass(frozen=True)
class AuthorityLink:
authority_id: str
assurance_level: int # higher = stronger
revocation_lag_sec: float
propagates_revocations_upstream: bool
@dataclass(frozen=True)
class FederationChain:
chain_id: str
links: tuple[AuthorityLink, ...]
@dataclass(frozen=True)
class FederationRequirements:
min_assurance_level: int
max_revocation_lag_sec: float
@dataclass(frozen=True)
class FederationPolicy:
require_upstream_propagation: bool
@dataclass(frozen=True)
class FederationManifest:
verdict: FederationVerdict
composed_assurance: int
composed_revocation_lag: float
directive: str
manifest_digest: str
class IdentityFederationGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_chain(
cls, chain: FederationChain, req: FederationRequirements, policy: FederationPolicy
) -> FederationManifest:
# Gate 1: Single authority
if len(chain.links) <= 1:
if chain.links:
link = chain.links[0]
digest = cls._hash(json.dumps({
"chain": chain.chain_id,
"authority": link.authority_id,
"assurance": str(link.assurance_level)
}, sort_keys=True))
return FederationManifest(
verdict=FederationVerdict.SINGLE_AUTHORITY,
composed_assurance=link.assurance_level,
composed_revocation_lag=link.revocation_lag_sec,
directive=f"PROCEED: Single authority, assurance {link.assurance_level}.",
manifest_digest=digest
)
return FederationManifest(
verdict=FederationVerdict.SINGLE_AUTHORITY,
composed_assurance=0,
composed_revocation_lag=0.0,
directive="PROCEED: Empty chain.",
manifest_digest=cls._hash("EMPTY")
)
# Gate 2: Weakest link assurance
composed_assurance = min(link.assurance_level for link in chain.links)
if composed_assurance < req.min_assurance_level:
return FederationManifest(
verdict=FederationVerdict.WEAKEST_LINK_INSUFFICIENT,
composed_assurance=composed_assurance,
composed_revocation_lag=max(link.revocation_lag_sec for link in chain.links),
directive=f"HALT: Composed assurance {composed_assurance} < required {req.min_assurance_level}.",
manifest_digest=cls._hash(f"WEAK_ASSURANCE:{composed_assurance}")
)
# Gate 3: Revocation lag
composed_revocation_lag = max(link.revocation_lag_sec for link in chain.links)
if composed_revocation_lag > req.max_revocation_lag_sec:
return FederationManifest(
verdict=FederationVerdict.REVOCATION_GAP,
composed_assurance=composed_assurance,
composed_revocation_lag=composed_revocation_lag,
directive=f"HALT: Composed revocation lag {composed_revocation_lag:.0f}s > {req.max_revocation_lag_sec:.0f}s.",
manifest_digest=cls._hash(f"REVOCATION_LAG:{composed_revocation_lag:.0f}")
)
# Gate 4: Upstream propagation
if policy.require_upstream_propagation:
non_propagating = [link.authority_id for link in chain.links if not link.propagates_revocations_upstream]
if non_propagating:
return FederationManifest(
verdict=FederationVerdict.REVOCATION_GAP,
composed_assurance=composed_assurance,
composed_revocation_lag=composed_revocation_lag,
directive=f"HALT: {len(non_propagating)} authorities do not propagate revocations upstream.",
manifest_digest=cls._hash(f"NO_PROPAGATION:{len(non_propagating)}")
)
# Gate 5: Compatible
digest = cls._hash(json.dumps({
"chain": chain.chain_id,
"authorities": tuple(link.authority_id for link in chain.links),
"assurance": str(composed_assurance),
"revocation_lag": f"{composed_revocation_lag:.0f}"
}, sort_keys=True))
return FederationManifest(
verdict=FederationVerdict.CHAIN_COMPATIBLE,
composed_assurance=composed_assurance,
composed_revocation_lag=composed_revocation_lag,
directive=f"PROCEED: {len(chain.links)}-authority chain, assurance {composed_assurance}, revocation lag {composed_revocation_lag:.0f}s.",
manifest_digest=digest
)