Signed Agent Cards: protección con JWS en A2A v1.0
Cuando los agentes de IA se descubren entre sí en redes públicas recuperando un archivo agent-card.json, la confianza no puede depender de confiar en la red. Una tarjeta sin firmar está a un solo falseamiento de DNS o a un error de configuración de CDN de redirigir sistemas autónomos hacia endpoints maliciosos. En A2A v1.0, las Agent Cards firmadas resuelven esto con JSON Web Signatures.
Por qué las tarjetas no firmadas son vulnerables
En la especificación del protocolo A2A, el descubrimiento está diseñado para ser ligero, estático y sin autenticación: un agente cliente realiza una simple petición HTTP GET a /.well-known/agent-card.json.
Si bien esta simplicidad aceleró la adopción del ecosistema, expone una superficie de ataque a la que puede llamarse envenenamiento de Agent Card:
- Redirección de endpoints: un atacante que comprometa una entrada DNS o un proxy perimetral puede reescribir el campo
supportedInterfaces[].urly enviar en silencio prompts sensibles a un servidor que espía o suplanta. - Inyección de capacidades y de prompts: al manipular el array
skillso los camposdescription, los actores maliciosos pueden engañar a los agentes llamantes para que deleguen tareas financieras o administrativas de alto privilegio a un modelo no verificado. - Suplantación de identidad: sin verificación criptográfica, cualquiera puede alojar una tarjeta que afirme que
provider.organizationes una institución financiera o un proveedor de software consolidado.
Para defender las arquitecturas multiagente frente a estos riesgos, la especificación mantenida en el repositorio a2aproject/A2A permite que una tarjeta lleve un array signatures de objetos RFC 7515: JSON Web Signature (JWS) (sección 8.4 de la especificación).
Anatomía del array signatures
En una Agent Card de A2A v1.0, la propiedad de nivel superior signatures contiene un array de objetos AgentCardSignature. Se permiten varias entradas, y así funciona la rotación de claves: durante una rotación, la tarjeta lleva una firma de la clave antigua y otra de la nueva.
{
"name": "Treasury Reconciliation Agent",
"version": "1.2.0",
"supportedInterfaces": [
{
"url": "https://agents.acme-finance.com/a2a/v1",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"signatures": [
{
"protected": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJrZXktMSIsImprdSI6Imh0dHBzOi8vYWNtZS1maW5hbmNlLmNvbS8ud2VsbC1rbm93bi9qd2tzLmpzb24ifQ",
"signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ"
}
]
}
Cada entrada tiene los campos de un JWS aplanado, y no se requiere nada más:
-
protected(obligatorio): un objeto JSON codificado en Base64URL que contiene la cabecera protegida del JWS. Debe contener:alg: el algoritmo de firma, comoES256oRS256.kid: el ID de clave, que identifica la clave de firma.typ: debería establecerse enJOSE.
También puede contener
jku, la URL del JSON Web Key Set que guarda la clave pública. El ejemplo anterior se decodifica como{"alg":"ES256","typ":"JOSE","kid":"key-1","jku":"https://acme-finance.com/.well-known/jwks.json"}. -
signature(obligatorio): el valor de la firma codificado en Base64URL. -
header(opcional): un objeto JSON de parámetros de cabecera sin proteger, no codificado en Base64URL.
Firma de un agent-card.json con ES256
Lo que se firma no es el archivo tal como se sirve, sino su forma canónica. Según la sección 8.4.1 de la especificación, hay que:
- Eliminar el propio campo
signatures, para evitar una dependencia circular. - Respetar la presencia de campos de Protocol Buffer: descartar los campos que tengan un valor por defecto (un array vacío, una cadena vacía), salvo que sean obligatorios o campos optional establecidos explícitamente.
- Canonizar el resultado con el JSON Canonicalization Scheme (RFC 8785): claves de objeto ordenadas, sin espacios en blanco irrelevantes.
La entrada de firma del JWS es entonces BASE64URL(cabecera protegida) + "." + BASE64URL(tarjeta canónica). Esta es una versión en Node.js que sigue esos pasos sin dependencias. Ordenar las claves y usar JSON.stringify basta para producir la forma RFC 8785 en tarjetas ordinarias; recurra a una biblioteca JCS contrastada si la suya contiene números exóticos.
import crypto from 'node:crypto';
import fs from 'node:fs';
// RFC 8785 (JCS): keys sorted by UTF-16 code units, no whitespace. JSON.stringify
// already serializes strings and numbers the way JCS requires.
const canonicalize = value =>
Array.isArray(value)
? `[${value.map(canonicalize).join(',')}]`
: value !== null && typeof value === 'object'
? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonicalize(value[key])}`).join(',')}}`
: JSON.stringify(value);
// 1. Read the card and drop any existing signatures.
const card = JSON.parse(fs.readFileSync('agent-card.json', 'utf8'));
const { signatures, ...unsigned } = card;
// 2. Build the JWS protected header: alg, typ and kid are required.
const header = {
alg: 'ES256',
typ: 'JOSE',
kid: 'key-1',
jku: 'https://acme-finance.com/.well-known/jwks.json'
};
const protectedB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
const payloadB64 = Buffer.from(canonicalize(unsigned)).toString('base64url');
// 3. Sign with your P-256 private key. A JWS ES256 signature is the raw R||S
// pair (RFC 7518 section 3.4), not Node's default DER encoding.
const signature = crypto.sign('sha256', Buffer.from(`${protectedB64}.${payloadB64}`), {
key: fs.readFileSync('private-key.pem'),
dsaEncoding: 'ieee-p1363'
}).toString('base64url');
// 4. Attach the signature to the card.
const signedCard = { ...unsigned, signatures: [{ protected: protectedB64, signature }] };
fs.writeFileSync('agent-card.signed.json', JSON.stringify(signedCard, null, 2));
console.log('Agent Card signed with ES256');
Cómo los agentes clientes verifican las firmas
Cuando un agente cliente obtiene una Agent Card de un par no verificado, la verificación debe ocurrir antes de que inspeccione las capacidades o invoque las interfaces. La sección 8.4.3 de la especificación da los pasos:
- Extracción: tome una firma del array
signatures. Si no hay ninguna, trate la tarjeta como no verificada, o recházela, según su política de seguridad. - Obtención de la clave: obtenga la clave pública designada por
kid, desde el JWKS enjkuo desde un almacén de claves de confianza de proveedores conocidos. - Reconstrucción de la carga útil: elimine de la tarjeta recibida las propiedades con valor por defecto y el campo
signatures, y luego canonícela con RFC 8785. - Verificación: compruebe la firma sobre
BASE64URL(protected) + "." + BASE64URL(tarjeta canónica)con la clave pública.
Más allá de la mecánica, la especificación es explícita sobre la confianza: los clientes DEBERÍAN verificar al menos una firma antes de confiar en una tarjeta, las claves públicas DEBERÍAN obtenerse por HTTPS y las claves caducadas o revocadas NO DEBEN usarse. Se permiten varias firmas para que la rotación no rompa la verificación.
Vale la pena añadir dos políticas, aunque la especificación no las exija: rechazar los algoritmos por debajo de su nivel base de seguridad (siempre none, y los simétricos) y comprobar que el origen de la clave coincide con el provider.url que declara la tarjeta, para que la firma de una organización no pueda avalar el nombre de otra.
Rotación de claves y lista de verificación de seguridad
Siga estas pautas operativas al gestionar Agent Cards firmadas en producción:
- Desacople el ciclo de vida de las claves de los despliegues: nombre las claves con
kidy publíquelas en un JWKS (/.well-known/jwks.json) al que apuntejku, en lugar de codificar claves estáticas. Para rotar, añada la nueva clave al JWKS, firme con ambas durante un tiempo y luego retire la antigua. - Vuelva a firmar en cada cambio: la firma cubre toda la tarjeta. Haga de la firma un paso de compilación, para que el cambio de versión y la nueva firma se publiquen juntos.
- Imponga HTTPS en todas partes: cada interfaz listada en
supportedInterfacesy cada endpoint de claves debe servirse por HTTPS con un certificado válido. - Vincule el dominio al proveedor: mantenga alineados el dominio que aloja la Agent Card, el
provider.urly el origen del JWKS. - Auditoría previa a la publicación: pase su tarjeta completada por el validador antes de publicarla en su raíz pública, y sustituya cualquier firma de relleno que haya emitido el generador.
Preguntas frecuentes
¿Son obligatorias las firmas JWS en una Agent Card de A2A v1.0?
No. La especificación dice que las Agent Cards PUEDEN (MAY) estar firmadas y que los clientes DEBERÍAN (SHOULD) verificar al menos una firma antes de confiar en una tarjeta que las tenga. Aun así, conviene añadir firmas a cualquier agente expuesto en redes públicas o multiinquilino, porque sin ellas un cliente no puede saber si la tarjeta fue alterada en el camino.
¿Qué es el «envenenamiento de Agent Card»?
Es manipular la tarjeta de un agente para que quienes lo llaman confíen en lo equivocado: un atacante que controla una entrada DNS, una configuración de CDN o un bucket de almacenamiento comprometido reescribe la tarjeta para redirigir los endpoints de interfaz hacia infraestructura maliciosa o para inyectar descripciones de habilidades falsificadas.
¿Qué algoritmo de firma debo usar?
Los ejemplos de la especificación usan ES256 y RS256. ES256 (ECDSA sobre P-256 con SHA-256) es una buena opción por defecto: las firmas son pequeñas y el soporte en bibliotecas es amplio. No use nunca el algoritmo "none" y evite los algoritmos simétricos tipo HS256, porque los clientes verifican una Agent Card con una clave pública.
¿Dónde se deben alojar las claves públicas?
En un JSON Web Key Set servido por HTTPS bajo el propio dominio del proveedor, normalmente en /.well-known/jwks.json. La cabecera protegida nombra la clave con kid y puede apuntar al conjunto con jku; los clientes también pueden usar un almacén de claves de confianza con proveedores conocidos.
¿Cómo evalúa el validador de Agent Card las firmas?
Solo comprueba el formato. Cada entrada de signatures necesita una cabecera protegida en base64url que declare alg y kid (typ debería ser "JOSE") y una firma en base64url que no sea un marcador de posición. No verifica la firma en sí, porque eso requiere la clave pública del firmante y la forma canónica de la tarjeta; use su SDK o una biblioteca JWS para eso.
Referencias
Herramienta relacionada: Validador de Agent Card
Leer más guías
Siguiente paso
Compruebe la integridad de su Agent Card antes de desplegarla.
El validador inspecciona la estructura de su tarjeta, comprueba los campos alg, kid y typ de las cabeceras de firma JWS y advierte sobre firmas de relleno y endpoints de interfaz sin cifrar.