# Where must an instance of the A2A Agent Card discovery trust boundary be enforced when a delegated task crosses a vendor signing boundary **Date of Record:** 2026 November 14 **Scope:** Applies to A2A v1.0 deployments where an orchestrating agent (the *client*) delegates a task to a remote agent (the *server*) whose Agent Card is fetched over HTTPS and whose returned artifacts are signed by a key distinct from the card's transport identity. Does not apply to same-vendor, same-trust-domain delegations where card signing key and artifact signing key share a common issuer root. ## Core Distinction A2A separates two cryptographic identities that are routinely conflated: the **transport identity** of the Agent Card (the TLS leaf certificate chain and the `/.well-known/agent.json` document integrity) and the **artifact signing identity** (the key that signs `Artifact` payloads returned during `tasks/send` and streamed over `tasks/sendSubscribe`). The Agent Card advertises the artifact signing key via its `security` and `capabilities` blocks, but the card itself is delivered over a channel whose authenticity is established independently by TLS. The non-obvious boundary is this: **the artifact signing key is not bound to the transport identity by any A2A v1.0 construct.** A card fetched from `https://agent.vendor-b.example` may legitimately declare a signing key whose issuer is `vendor-b-signing.example` under a delegation certificate. If the client validates artifacts against the *transport* identity (the hostname in the card URL) rather than the *declared* signing identity, it will reject valid cross-vendor artifacts. If it validates against the declared identity without checking the delegation chain back to a trusted root, it will accept forged artifacts. The failure is silent in both directions because A2A returns a well-formed `Task` with `status.state = "completed"` in either case. ## What Was Observed During a three-vendor interoperability exercise (orchestrator → vendor-B research agent → vendor-C synthesis agent), the following sequence reproduced deterministically: 1. Orchestrator fetches `https://research.vendor-b.example/.well-known/agent.json`. TLS validates against the public CA. Card parses cleanly. 2. Card's `capabilities.artifacts.signing` declares `keyId: "vb-art-2026"`, `issuer: "vendor-b-signing.example"`, and a `delegationCert` chain terminating at a shared industry root (`a2a-interop-root.example`). 3. Orchestrator issues `tasks/send`. Vendor-B returns a `Task` with `status.state = "completed"` and one `Artifact` signed under `vb-art-2026`. 4. Orchestrator's artifact verifier, configured conservatively, checks the signature against the *card's transport host* (`research.vendor-b.example`) as the expected signer identity. Verification fails with `SIGNER_MISMATCH`. 5. The orchestrator retries with `tasks/get`. Vendor-B returns the *same* task in `completed` state. No error surfaces; the client loops on re-fetch because the task is terminal but the artifact is unverified. The second observed mode, from a different client implementation: the orchestrator trusts the declared `issuer` field verbatim and does not walk the `delegationCert` chain. A test harness that replayed a captured card with a substituted `issuer` and a self-signed key produced artifacts that this client accepted. The task reached `completed` and the tampered artifact was persisted downstream. Both failures share a root cause: the *verification anchor* was chosen inconsistently with the *delegation model* the card actually expresses. ## Condition of Validity The invariant holds — that is, artifact verification is sound — if and only if all of the following are true at the moment of artifact acceptance: - The artifact signature is verified against the `keyId` **declared in the card that was fetched for this task's endpoint**, not against a key cached from a prior task or a global vendor directory. - The `delegationCert` chain for that `keyId` is walked to a root in the client's configured trust store, and every link is checked for `keyUsage = digitalSignature` and a validity window covering the artifact's `createdAt`. - The card's own integrity is bound to the transport: the card must be fetched over a channel whose TLS identity matches the card's advertised `url`, and the card must be re-fetched (or its hash re-checked against a pinned value) if the task's `contextId` spans more than one card validity window. - The task's `status.state` transition to `completed` is only accepted as terminal **after** artifact verification succeeds; a `completed` task with an unverified artifact must be treated as `failed` by the client, not retried as if transient. The invariant does **not** hold when the client conflates transport host with signing issuer, when it trusts `issuer` without chain-walking, or when it treats A2A's `completed` state as authoritative independent of artifact verification. ## Decision Boundary The operational decision is where to place the `raise` — i.e., at which layer the client refuses to proceed. There are three defensible placements, and they are not equivalent: - **At card fetch time:** reject cards whose declared signing issuer is not in the trust store. This is cheap but over-broad; it rejects legitimate cards whose delegation root is valid but not pre-listed. - **At artifact verification time:** the correct placement. The card is admitted; the artifact is rejected and the task is demoted to `failed` with a structured error. This preserves the delegation model and localizes the failure. - **At task completion time:** insufficient. By the time `completed` is observed, downstream consumers may already have been notified. Verification must gate the state transition, not follow it. The boundary is therefore: **enforce the delegation-chain check at the artifact-verification gate, and make that gate the sole precondition for accepting `completed` as terminal.** Any code path that accepts `completed` without passing through that gate is the defect. ## Operational Effect Clients that place the check correctly observe: cross-vendor tasks complete with a small, bounded verification cost (chain walk per artifact, cacheable per `keyId` for the card's validity window); forged artifacts are rejected with a typed error and the task is demoted to `failed`; the orchestrator's retry logic does not loop because `failed` is terminal and distinct from `completed`. Clients that misplace it observe either silent rejection loops (`SIGNER_MISMATCH` on every retry against a terminal task) or silent acceptance of forged artifacts — both of which are indistinguishable from success at the protocol level, which is why this boundary is repeatedly missed in interoperability testing. ```python from dataclasses import dataclass from typing import Optional class ArtifactVerificationError(RuntimeError): """Raised when an A2A artifact cannot be bound to a trusted signing identity.""" pass @dataclass class AgentCard: url: str # transport identity, e.g. "https://research.vendor-b.example" signing_issuer: str # declared artifact signing issuer delegation_chain: list # cert chain terminating at a configured trust root key_id: str @dataclass class Artifact: key_id: str signature: bytes payload: bytes created_at: float def verify_artifact( card: AgentCard, artifact: Artifact, trust_roots: set[str], now: float, ) -> None: # 1. The artifact must be signed by the key the card declared, not the transport host. if artifact.key_id != card.key_id: raise ArtifactVerificationError( f"artifact key_id {artifact.key_id!r} does not match card-declared " f"{card.key_id!r}; transport host {card.url!r} is not a signing identity" ) # 2. Walk the delegation chain to a configured trust root. Do not trust `issuer` verbatim. chain = card.delegation_chain if not chain: raise ArtifactVerificationError( f"card {card.url!r} declares issuer {card.signing_issuer!r} but supplies no " f"delegation chain; cannot bind artifact to a trust root" ) root = chain[-1].issuer if root not in trust_roots: raise ArtifactVerificationError( f"delegation chain for {card.key_id!r} terminates at {root!r}, " f"which is not in the configured trust store" ) for link in chain: if not link.valid_at(now): raise ArtifactVerificationError( f"delegation link {link.subject!r} not valid at artifact time {now}" ) if "digitalSignature" not in link.key_usage: raise ArtifactVerificationError( f"delegation link {link.subject!r} lacks digitalSignature keyUsage" ) # 3. Only now is the signature meaningful; verify over the declared key. if not artifact.signature.verify(artifact.payload, card.key_id): raise ArtifactVerificationError( f"signature verification failed for artifact under {card.key_id!r}" ) def accept_task_completed(card: AgentCard, artifacts: list[Artifact], trust_roots: set[str], now: float) -> None: # `completed` is terminal ONLY after every artifact passes the gate above. for art in artifacts: verify_artifact(card, art, trust_roots, now) ```