¿Qué demuestra una solicitud firmada?
La mayor parte del tráfico automatizado se identifica con una cadena de user agent. Esa cadena es una afirmación, y copiarla no cuesta nada. Las solicitudes firmadas cambian el panorama. Cuando un cliente automatizado firma sus HTTP requests con una clave privada y publica la clave pública correspondiente en una ubicación vinculada a su operador, tu servidor puede comprobar que la solicitud realmente fue producida por alguien que posee esa clave.
El estándar que hay detrás de esto es HTTP Message Signatures, publicado como RFC 9421. El IETF Web Bot Auth working group se apoya en él con convenciones para clientes automatizados: cómo un agente indica su directorio de claves y qué componentes de la request debe firmar. A noviembre de 2026, el documento de protocolo del grupo de trabajo es draft-ietf-webbotauth-httpsig-protocol, que define el encabezado Signature-Agent, un formato de directorio de claves basado en JSON Web Key Sets y un URI bien conocido para servirlo. Merece la pena entender ambos en detalle, porque la verificación es fácil de hacer mal de forma sutil.
Antes de entrar en la mecánica, sé preciso sobre el resultado. Una firma válida muestra que la request fue firmada por una clave asociada a un identificador publicado, como el dominio de un operador, y que las partes firmadas de la request no fueron alteradas. No muestra quién dio instrucciones al agente, si una persona le delegó autoridad o si la acción es segura. La evidencia de identidad y la autorización son preguntas separadas, y tu aplicación responde a la segunda. Nuestra entrada de blog sobre qué demuestran las solicitudes firmadas de agentes de IA recorre ese límite con ejemplos.
Los encabezados implicados
Una solicitud firmada suele llevar tres encabezados:
Signature-Input(RFC 9421, Section 4.1) describe cada firma: una etiqueta, la lista de componentes cubiertos y parámetros comocreated,expires,keyid,alg,nonceytag.Signature(RFC 9421, Section 4.2) transporta los bytes de la firma para cada etiqueta, codificados en base64, como un structured field.Signature-Agent(de las convenciones de Web Bot Auth) señala dónde se publican las claves públicas del cliente, normalmente una URL HTTPS en el dominio del operador.
Un ejemplo simplificado:
GET /api/catalog?page=2 HTTP/1.1
Host: shop.example
Signature-Agent: "https://agent.example"
Signature-Input: sig1=("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"
Signature: sig1=:Base64SignatureBytes==:El ejemplo está simplificado para facilitar la lectura. En el borrador actual del grupo de trabajo, Signature-Agent es a su vez un structured field indexado por la etiqueta de firma (por ejemplo sig1="https://agent.example"), así que comprueba qué forma espera tu biblioteca de verificación.
Paso 1: Analiza correctamente los structured fields
Signature-Input y Signature son HTTP Structured Fields (RFC 9651, que deja obsoleto RFC 8941). No los analices separando cadenas. Las cadenas entre comillas, los parámetros y las listas internas tienen una gramática precisa, y un analizador descuidado crea tanto falsos rechazos como brechas de seguridad reales.
Usa un analizador de structured fields mantenido o, mejor aún, una biblioteca RFC 9421 mantenida que gestione a la vez el análisis y la verificación. Implementar la especificación desde cero es un buen ejercicio de aprendizaje y una mala elección para producción. Los pasos siguientes describen lo que hace una biblioteca así, para que puedas configurarla correctamente y revisar su salida.
Paso 2: Elige qué firma evaluar
Una request puede llevar más de una etiqueta de firma. Selecciona la que coincida con tu policy, por ejemplo la etiqueta cuyo tag sea web-bot-auth. Rechaza la request a efectos de verificación, no de servicio, si:
- ninguna firma tiene los parámetros que necesitas,
- el algoritmo no está en tu allowlist, por ejemplo
ed25519, o - los componentes cubiertos no incluyen el conjunto mínimo que requieres.
Un mínimo razonable para clientes automatizados es @method, @authority y @path, además de signature-agent cuando ese encabezado está presente. Sin @authority, una firma capturada para un sitio podría reproducirse contra otro. Sin @path, podría reutilizarse entre endpoints. El borrador del protocolo Web Bot Auth exige que los agentes cubran @authority o @target-uri, y advierte que una firma que cubre solo la authority verifica contra cualquier method o path en ese host hasta que expire.
Paso 3: Comprueba los parámetros de tiempo
Lee created y expires, los parámetros de tiempo definidos en RFC 9421, Section 2.3. Luego:
- Rechaza las firmas cuya hora
createdesté en el futuro más allá de una pequeña tolerancia de desfase del reloj. - Rechaza las firmas que ya hayan pasado
expires. - Si falta
expires, impón tu propia antigüedad máxima basada encreated.
Las ventanas de validez cortas limitan durante cuánto tiempo puede reproducirse una request capturada.
Paso 4: Descubre la clave
El encabezado Signature-Agent apunta al directorio de claves del operador. Obtenlo por HTTPS, y solo por HTTPS. El borrador del protocolo Web Bot Auth describe el directorio como un JSON Web Key Set, con claves en formato JWK, servido desde el well-known URI de http-message-signatures-directory en ese origen. Después busca la clave cuyo identificador coincida con el parámetro keyid.
type Jwk = { kid: string; kty: string; crv?: string; x?: string };
const keyCache = new Map<string, { keys: Jwk[]; fetchedAt: number }>();
const KEY_TTL_MS = 60 * 60 * 1000;
export async function getKey(directoryUrl: string, keyId: string): Promise<Jwk | null> {
const url = new URL(directoryUrl);
if (url.protocol !== "https:") return null;
const cached = keyCache.get(url.origin);
const fresh = cached && Date.now() - cached.fetchedAt < KEY_TTL_MS;
const keys = fresh ? cached.keys : await fetchKeys(url.origin);
return keys.find((k) => k.kid === keyId) ?? null;
}
async function fetchKeys(origin: string): Promise<Jwk[]> {
const res = await fetch(`${origin}/.well-known/http-message-signatures-directory`, {
headers: { accept: "application/http-message-signatures-directory+json, application/json" },
signal: AbortSignal.timeout(2000),
});
if (!res.ok) return [];
const body = (await res.json()) as { keys?: Jwk[] };
const keys = Array.isArray(body.keys) ? body.keys : [];
keyCache.set(origin, { keys, fetchedAt: Date.now() });
return keys;
}Reglas prácticas para descubrir claves:
- Almacena en caché las claves con una vida útil acotada para que la verificación no añada una llamada de red a cada request.
- Establece un timeout corto y trata una obtención fallida como "no se pudo verificar", nunca como "verificado".
- Restringe de qué orígenes obtienes las claves si puedes, o al menos bloquea rangos de direcciones privadas para evitar server-side request forgery. El borrador del protocolo menciona server-side request forgery en sus consideraciones de seguridad precisamente por este motivo.
- Vincula la identidad al origen, no al nombre en texto libre del user agent. El identificador que registras es el origen del directorio.
Paso 5: Reconstruye la base de la firma
El firmante no firmó la request en bruto. Firmó una cadena canónica llamada signature base (RFC 9421, Section 2.5), construida a partir de los componentes cubiertos en orden, seguida por la línea @signature-params. Tu verificador reconstruye la misma cadena a partir de la request que recibió.
Para el ejemplo anterior, la base tiene este aspecto:
"@method": GET
"@authority": shop.example
"@path": /api/catalog
"signature-agent": "https://agent.example"
"@signature-params": ("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"Fíjate en los detalles que rompen la verificación detrás de proxies:
@authoritydebe ser el host al que el cliente dirigió la request. Si un load balancer reescribeHost, usa el valor original que reenvía.@pathexcluye la cadena de consulta. Si el cliente cubrió@query, reconstruyela exactamente como se recibió.- Los valores de encabezado se canonalizan según se especifica: recortados, con múltiples valores combinados. Una buena biblioteca hace esto por ti.
Paso 6: Verifica la firma
Con la base y la clave pública, la verificación es una sola llamada, siguiendo los pasos de RFC 9421, Section 3.2. Para Ed25519 en Node.js, usando el módulo integrado crypto:
import { createPublicKey, verify } from "node:crypto";
export function verifyEd25519(jwk: Jwk, base: string, signatureB64: string): boolean {
const key = createPublicKey({ key: jwk as any, format: "jwk" });
return verify(null, Buffer.from(base, "utf8"), key, Buffer.from(signatureB64, "base64"));
}Si la comprobación falla, la request no está verificada. Aun así, puede servirse. Simplemente no obtiene la base "verificado".
Paso 7: Evita la repetición
Una firma válida puede capturarse y reenviarse hasta que expire. RFC 9421, Section 7.2.2 cubre la repetición de firmas, y el borrador del protocolo Web Bot Auth remite a ello en lugar de añadir sus propias reglas de nonce. Dos controles cierran la mayor parte de esa ventana:
- Caducidad corta, aplicada en el Paso 3.
- Seguimiento de nonce. Cuando está presente un
nonce, guárdalo con su identificador de clave hasta que la firma expire y rechaza un segundo uso.
const seen = new Map<string, number>();
export function acceptNonce(keyId: string, nonce: string, expiresAt: number): boolean {
const id = `${keyId}:${nonce}`;
const now = Date.now() / 1000;
for (const [k, exp] of seen) if (exp < now) seen.delete(k);
if (seen.has(id)) return false;
seen.set(id, expiresAt);
return true;
}En un despliegue con varias instancias, guarda los nonces en un almacén compartido como Redis con un time to live igual a la validez restante.
Paso 8: Registra el resultado con su base
La verificación produce evidencia, no un veredicto. Regístrala en una forma que tus analíticas y policies puedan usar:
const result = {
actor: "agent",
basis: "verified",
operator: "https://agent.example",
keyId: "kq1",
coveredComponents: ["@method", "@authority", "@path", "signature-agent"],
verifiedAt: new Date().toISOString(),
};Cuando la verificación falla o no puede ejecutarse, regístralo también, con el motivo: encabezados ausentes, algoritmo no compatible, clave no encontrada, caducada, repetida o discrepancia de firma. Estos motivos son valiosos. Un pico de "key not found" de un operador a menudo significa que rotó las claves, no que te esté atacando.
¿Qué deberías registrar de la verificación de firmas?
Registra el origen del operador, el identificador de clave, los componentes cubiertos, los parámetros de tiempo, el resultado y el motivo del fallo. No registres los bytes completos de la firma ni los cuerpos completos de la request. Conserva los registros el tiempo suficiente para estudiar patrones y lo bastante corto para respetar tus compromisos de privacidad.
¿Cómo debería cambiar tu policy una firma verificada?
Una vez que las requests pueden verificarse, puedes tratarlas de forma distinta de manera significativa. Nuestra entrada sobre diseñar policies de aplicación para humanos y agentes de IA cubre el modelo de policy más amplio:
- Permite a operadores verificados entrar en rutas de solo lectura con un rate limit más alto que la automatización no verificada.
- Ofréceles respuestas estructuradas, como JSON o Markdown, en lugar de páginas HTML completas.
- Mantén las acciones de alto impacto detrás de tu autorización normal. Una firma verificada te dice qué cliente llama, no que pueda modificar una cuenta.
Empieza en modo observación. Mide cuánta parte de tu tráfico automatizado está firmada, qué operadores aparecen y con qué frecuencia falla la verificación, antes de que cualquier policy dependa de ello.
Dónde encaja Proof of Actor
La verificación de firmas es una fuente de evidencia entre varias. Proof of Actor la combina con otras señales y mantiene visible la base en cada resultado, de modo que un agente verificado nunca se confunde con uno que solo se declaró a sí mismo. Tu aplicación decide qué ocurre a continuación.
Para incorporarlo en una aplicación de producción con nuestros ingenieros, solicita acceso anticipado. Para obtener la imagen completa de lo que la evidencia puede y no puede mostrar, lee Detection & Limitations.
Fuentes
- RFC 9421: HTTP Message Signatures: el estándar de firma y verificación, incluidos los parámetros de firma, la signature base y las consideraciones sobre repetición
- RFC 9651: Structured Field Values for HTTP: la gramática usada por
Signature-InputySignature - IETF Web Bot Auth working group: carta y estado del trabajo de autenticación de clientes automatizados
- draft-ietf-webbotauth-httpsig-protocol: Internet-Draft que define
Signature-Agent, el directorio de claves JWKS y su well-known URI (trabajo en curso a noviembre de 2026) - RFC 7517: JSON Web Key (JWK): el formato de clave usado en directorios de claves publicados
- RFC 8615: Well-Known URIs: cómo se registran rutas well-known como el directorio de claves
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA): el algoritmo Ed25519 usado en el ejemplo
- Node.js crypto documentation:
createPublicKeyyverifyusados en el Paso 6