Agent Card Reader

Decode A2A Agent Cards in your browser

Open the reader

Agent Card Reader / Agent card signatures

How A2A agent card signatures work

A card's signatures array holds JSON Web Signatures (RFC 7515). The payload is detached: it is the card itself, without the signatures field, with default values removed, canonicalized with the JSON Canonicalization Scheme (RFC 8785). Spec §8.4.

What gets signed

  1. Remove properties with default values (§8.4.1). Fields explicitly set stay, even false; empty repeated fields that are not required, such as "extensions": [], go.
  2. Exclude the signatures field.
  3. Canonicalize with RFC 8785: keys sorted, no insignificant whitespace, numbers and strings in their canonical form.

The signing input is ASCII(protected) || "." || BASE64URL(payload), exactly as for a compact JWS.

The signature object

FieldRequiredContents
protectedYesBase64url-encoded JSON header. Must include alg (for example ES256, RS256, EdDSA) and kid; typ should be JOSE; jku may point to a JWKS.
signatureYesBase64url-encoded signature bytes.
headerNoUnprotected header, as a JSON object.

Verifying a signature

Per §8.4.3: fetch the public key by kid from jku (or a trusted key store), rebuild the canonical payload, and verify. In a browser, WebCrypto does it without libraries:

const b64u = (s) => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')
  + '='.repeat((4 - s.length % 4) % 4)), (c) => c.charCodeAt(0));
const enc = (s) => btoa(String.fromCharCode(...new TextEncoder().encode(s)))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
// RFC 8785 for JSON without exotic numbers: sorted keys, no whitespace.
const jcs = (v) => v === null || typeof v !== 'object' ? JSON.stringify(v)
  : Array.isArray(v) ? `[${v.map(jcs)}]`
  : `{${Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + jcs(v[k]))}}`;

async function verifyCard(card, sig, jwk) {           // jwk: the key from jku, matched by kid
  const { signatures, ...unsigned } = card;            // default removal omitted for brevity
  const input = `${sig.protected}.${enc(jcs(unsigned))}`;
  const key = await crypto.subtle.importKey('jwk', jwk, { name: 'Ed25519' }, false, ['verify']);
  return crypto.subtle.verify({ name: 'Ed25519' }, key, b64u(sig.signature), new TextEncoder().encode(input));
}

For ES256 use { name: 'ECDSA', namedCurve: 'P-256' } to import and { name: 'ECDSA', hash: 'SHA-256' } to verify; for RS256, RSASSA-PKCS1-v1_5 with SHA-256.

Worked example: getbody.md

The live card at https://getbody.md/.well-known/agent-card.json carries one signature whose protected header decodes to:

{ "alg": "EdDSA", "jku": "https://getbody.md/.well-known/jwks.json", "kid": "getbody-a2a-1", "typ": "JOSE" }

The JWKS at that jku holds an Ed25519 key with that kid. The signature verifies over JCS of the card without default removal. The card's security field (the 0.3 name for requirements) contains an empty scopes list, "getbodyBearer": [], which default removal would drop. The reader therefore tries the spec's canonical form first, then plain JCS, and says which one matched. See it in the reader.

Common pitfalls

Based on the A2A specification (latest, 1.0). Updated 2026-10-14. Check a card now: open the reader.