# Which invariants prevent A2A v1.0 cross-org agent card federation from silently degrading into trust-laundering proxies **Date of Record:** 2026 November 14 **Scope:** Applies to A2A v1.0 agent-to-agent federation where an Agent Card is fetched across an organizational boundary (different `provider.organization`, different signing key trust root, or different DNS/well-known origin). Does not apply to intra-org card resolution, nor to card caching within a single trust domain where the signing key is pinned out-of-band. ## Core Distinction There are two structurally different things an Agent Card can be: a **signed capability declaration** (the card is a claim by an origin, verifiable against a key the consumer independently trusts) and a **fetched document** (the card is bytes obtained from a URL that someone told you about). A2A v1.0 permits both to flow through the same resolution path, and the protocol does not require the consumer to distinguish them at the point of use. The invariant that matters is not "the card is signed" but "the *chain of custody* from signing key to consuming agent is unbroken and each hop is a distinct trust decision." When a card is re-served — proxied, mirrored, cached, or re-hosted under a new origin — the signature still verifies against the *original* origin's key, but the consumer is now trusting the *proxy's* key material and origin. The signature is intact; the trust assertion is not. This is trust-laundering: a low-trust intermediary lends its key to a high-trust origin's claims, or vice versa, and the resulting artifact verifies cleanly while asserting something neither party intended. ## What Was Observed During a federated deployment (three orgs, A2A v1.0, cards served from `/.well-known/agent-card.json` per origin), Org B stood up a "card relay" to reduce cross-org fetch latency. The relay cached cards from Org A and Org C and re-served them under `relay.b.example` with the original `signatures` array untouched. Consumers in Org C fetched from the relay, verified the signature against Org A's published JWKS, and accepted the card. This worked — and that is the failure. The consumers were now in a position where: 1. They trusted `relay.b.example` to serve *the* card for Org A (availability and integrity of the mapping), and 2. They trusted Org A's signing key for the card *contents*. These are two independent trust decisions collapsed into one fetch. The relay could serve a *stale* card (Org A rotated its endpoint, revoked a capability, or changed `securitySchemes`) and the signature would still verify, because the signature attests to the card as it was, not as it is. We observed a 40-minute window where Org C routed tasks to a decommissioned Org A endpoint because the relay's cached card was validly signed and 40 minutes stale, while Org A's origin served the corrected card. No signature check failed. No error surfaced. Tasks were dispatched to a dead endpoint and retried against the relay's cached `skills` list, which still advertised a capability Org A had withdrawn. A second, subtler observation: a card fetched from a relay carries the relay's `provider` block *or* the origin's, depending on how the relay rewrites it. In our deployment the relay preserved the origin `provider` block, so downstream agents logged Org A as the provider while the actual transport peer was Org B. Audit trails attributed actions to the wrong organization. This is not a signature failure; it is a *provenance* failure that the signature was never designed to catch. ## Condition of Validity The invariant holds — and federation is safe — only when all of the following are true simultaneously: - The consumer resolves the card from the **same origin** that the signing key's `provider.organization` claims, verified by comparing the card's `provider.url` host and the JWKS `iss`/origin against the fetch origin. If these differ, the card is a *relayed* card and must be treated as a distinct trust object. - The card carries a **freshness bound** the consumer enforces independently of HTTP caching (`Cache-Control` is advisory and routinely stripped by relays). A signed card with no `expires`/`issuedAt` inside the signed payload is not a freshness assertion; it is a snapshot. - The consumer maintains a **separate trust anchor per origin**, not a global "any valid A2A signature" trust store. A single trust store that accepts any key in a federation-wide JWKS set collapses the per-origin decision and re-enables laundering. - The `securitySchemes` and `skills` arrays are covered by the signature (they are, in v1.0, when the signature is over the canonicalized card body — but only if the relay does not re-serialize, which strips or reorders fields and can invalidate or, worse, *re-validate against a different canonical form*). When any of these fails, the deployment is in the degraded regime and the signature check is a false comfort. ## Decision Boundary The boundary is: **does the consumer's trust decision bind to the origin, or to the bytes?** If the consumer's logic is "fetch card, verify signature, trust contents," it binds to bytes and is vulnerable to relay staleness and provenance confusion. If the consumer's logic is "resolve origin, fetch card *from that origin*, verify signature *against that origin's pinned key*, enforce signed freshness, then trust," it binds to origin and is safe. The relay is not the problem; the relay is a symptom that the consumer never distinguished origin-trust from byte-trust. The decision to allow relaying is therefore a decision to accept stale-capability and misattributed-provenance risk, and it must be made explicitly, logged, and bounded by a freshness window the consumer enforces — not delegated to the relay's cache headers. ## Operational Effect Enforcing the origin-binding invariant costs one extra origin comparison and one pinned-key lookup per federation hop, and it converts a silent degradation (stale-but-valid cards, misattributed audit trails) into a loud, catchable failure at resolution time. The practical rule: a card whose fetch origin does not match its signed `provider` origin is a *relayed* card, must be marked as such in the consumer's registry, must carry an enforced max-age the consumer checks against the signed `issuedAt`, and must never populate the same trust slot as a directly-fetched card. The code below encodes that boundary as a hard failure rather than a warning, because in our incident every warning was ignored and every signature passed. ```python import time from dataclasses import dataclass from urllib.parse import urlparse class AgentCardFederationError(RuntimeError): """Raised when a federated Agent Card violates origin-binding invariants.""" pass @dataclass class ResolvedCard: fetch_origin: str # host the card was actually fetched from provider_url: str # card.provider.url as signed signed_issued_at: float # epoch seconds, inside signed payload signature_verified: bool consumer_max_age_s: float def admit_federated_card(card: ResolvedCard, now: float | None = None) -> None: """ Enforce the origin-binding + signed-freshness invariant for a cross-org card. Raises AgentCardFederationError on any violation; returns None on success. """ now = time.time() if now is None else now if not card.signature_verified: raise AgentCardFederationError( f"card from {card.fetch_origin!r} failed signature verification" ) fetch_host = urlparse(card.fetch_origin).hostname provider_host = urlparse(card.provider_url).hostname if fetch_host != provider_host: # This is a relayed card. It is NOT admissible into the direct-trust slot. raise AgentCardFederationError( "relayed card rejected: fetch origin " f"{fetch_host!r} != signed provider origin {provider_host!r}; " "route through relay-trust slot with enforced freshness, not direct trust" ) age = now - card.signed_issued_at if age < 0: raise AgentCardFederationError( f"card from {fetch_host!r} has issuedAt in the future (skew {age:.0f}s)" ) if age > card.consumer_max_age_s: raise AgentCardFederationError( f"stale card from {fetch_host!r}: age {age:.0f}s exceeds " f"consumer max {card.consumer_max_age_s:.0f}s (signature still valid, " "but capability set may be withdrawn)" ) ```