# Where must an instance pin the A2A v1.0 Agent Card signing key: discovery-time trust anchoring versus envelope-time verification **Date of Record:** 2026 November 11 **Scope:** Applies to A2A v1.0 deployments where an agent (the *delegator*) discovers a remote agent via its Agent Card and subsequently issues signed task-delegation envelopes to it. Does not apply to unsigned/loopback discovery, to A2A versions prior to v1.0, or to transports where the Agent Card is delivered in-band over an already-authenticated channel. ## Core Distinction The Agent Card is a *discovery artifact*; the delegation envelope is an *authorization artifact*. Both carry a key identifier, and both are frequently signed by the same principal. The non-obvious invariant is this: **the key that verifies the Agent Card at discovery time is not necessarily the key that must verify the delegation envelope at execution time, and pinning the former as the trust anchor for the latter is a boundary failure.** A2A v1.0 permits an Agent Card to advertise a `securitySchemes` entry and a `signatures` array whose `jku`/`kid` identify the card-signing key. Delegation envelopes carry their own detached JWS over the task payload, with a `kid` that *should* resolve within the delegator's trust store. Operators routinely conflate the two because in single-tenant deployments they coincide. The moment a provider rotates its card key, or fronts multiple task-execution keys behind one card key, the conflation produces either silent rejection of valid envelopes or — worse — acceptance of envelopes signed by a key that was only ever authorized to *describe* the agent, not to *act* for it. The distinction is between **identity attestation** (who is this agent, asserted by the card signer) and **delegation authority** (who is permitted to hand this agent work, asserted by the envelope signer). These are different trust decisions with different revocation lifetimes. ## What Was Observed In a multi-tenant deployment, a delegator cached the Agent Card at discovery and derived its envelope-verification key set from the card's `signatures[].protected.kid`. The provider rotated its *card-signing* key on a 90-day schedule but rotated its *task-execution* key on a 24-hour schedule. For the first 24 hours after each execution-key rotation, every delegation envelope failed verification because the cached card named a stale execution key; after the card's own 90-day rotation, the delegator rejected *all* envelopes for a window because the newly discovered card key was not yet in the pinned set. Two failure signatures were recorded: 1. **Stale-anchor rejection:** envelopes signed by a currently-valid execution key were rejected because the anchor was derived from a card cached past the execution key's lifetime. 2. **Cross-purpose acceptance:** during a migration window, a card signed by a key that had *only* card-signing authority was used to validate an envelope bearing the same `kid`, even though that key was never issued a delegation-authority constraint. The envelope's `aud` and `scope` claims were correct, so no downstream check caught it. The second is the dangerous one. It is silent: verification succeeds, the task executes, and the audit trail records a valid signature from a key that was never authorized for the action. ## Condition of Validity The conflation is harmless — and the pinning shortcut is acceptable — only when *all* of the following hold: - The card-signing key and the envelope-signing key are the same key material, by design and documented as such. - That key's rotation invalidates both the card and all outstanding envelopes atomically. - The key's certificate or JWK carries an explicit `key_ops` or A2A `purpose` extension distinguishing card attestation from task delegation, and the verifier checks it. - Discovery cache TTL is strictly less than the execution-key lifetime. If any condition fails, the anchor must be separated: the card key establishes *which envelope keys to trust* (via a signed key-set reference), and the envelope key establishes *authority to delegate*. The card should never be the envelope's verification anchor directly. ## Decision Boundary The boundary is crossed at the point where a verifier resolves an envelope `kid` by looking it up in the *discovered card* rather than in a *trust store populated by the card's attested key-set reference*. Concretely: - **Below the boundary (correct):** card signature verifies → extract attested `jwks_uri` or embedded key-set → load envelope keys from that set, each with a `purpose: delegation` constraint → verify envelope against those keys only. - **At the boundary (ambiguous):** card `kid` equals envelope `kid` and no `purpose` claim exists → must fail closed, because authority is unprovable. - **Above the boundary (incorrect):** envelope verified against the card's own signing key because it "matches." The decision must be made per-envelope, not per-session, because execution-key rotation can occur between two envelopes in the same task chain. ## Operational Effect Implementations that pin the card key as the envelope anchor exhibit a characteristic failure curve: near-zero errors until the first independent rotation, then a burst of rejections proportional to the ratio of execution-key lifetime to card-cache TTL, and a persistent low-rate cross-purpose acceptance that only surfaces in audit. The remediation is to treat the Agent Card as a *key-distribution* document, not a *key*, and to require an explicit delegation-purpose constraint on every key used to verify an envelope. Verifiers should refuse to verify an envelope whose `kid` resolves only through the card's own signature block. ```python from dataclasses import dataclass from typing import Mapping class EnvelopeTrustBoundaryError(RuntimeError): """Raised when a delegation envelope's key cannot be anchored to a delegation-purpose key set attested by the Agent Card.""" @dataclass(frozen=True) class KeyRecord: kid: str purpose: str # "card-attestation" | "delegation" def resolve_envelope_key( envelope_kid: str, card_signing_kid: str, attested_key_set: Mapping[str, KeyRecord], ) -> KeyRecord: """Resolve an envelope signing key strictly through the card-attested key set, never through the card's own signing key.""" if envelope_kid == card_signing_kid: raise EnvelopeTrustBoundaryError( f"envelope kid {envelope_kid!r} matches card-signing key; " "card attestation does not confer delegation authority" ) record = attested_key_set.get(envelope_kid) if record is None: raise EnvelopeTrustBoundaryError( f"envelope kid {envelope_kid!r} absent from attested key set" ) if record.purpose != "delegation": raise EnvelopeTrustBoundaryError( f"key {envelope_kid!r} has purpose {record.purpose!r}, " "not 'delegation'" ) return record def verify_delegation_envelope( envelope_kid: str, card_signing_kid: str, attested_key_set: Mapping[str, KeyRecord], signature_valid: bool, ) -> None: key = resolve_envelope_key(envelope_kid, card_signing_kid, attested_key_set) if not signature_valid: raise EnvelopeTrustBoundaryError( f"signature invalid for delegation key {key.kid!r}" ) ```