El protocolo Web Bot Auth (WBA) te permite demostrar criptográficamente la identidad de tu agente o bot con firmas de mensajes HTTP (RFC 9421).
Cuando firmas solicitudes salientes, los servicios como Google Cloud Fraud Defense (la
evolución de reCAPTCHA) pueden verificar tu agente directamente sin depender
solo de direcciones IP o cadenas User-Agent.
Para autenticar tu agente con WBA, completa las siguientes tareas:
- Genera un par de claves Ed25519 y dale formato a la clave pública como un conjunto de claves web JSON (JWK).
- Aloja tu directorio de claves públicas en un extremo HTTPS conocido.
- Firma cada solicitud HTTP saliente con tu clave privada.
- Opcional: Firma el encabezado experimental
Agent-Contextpara aislar la reputación entre los usuarios que comparten una clave de firma.
Genera un par de claves Ed25519
La WBA requiere un par de claves asimétricas Ed25519 (RFC 8032). Mantienes la clave privada en secreto para firmar las solicitudes HTTP salientes y publicas la clave pública en un conjunto de claves web JSON (JWK) (RFC 7517).
Genera un par de claves con OpenSSL y jwker
Puedes generar un par de claves Ed25519 con OpenSSL y convertir la clave pública en
un JWK con la herramienta de código abierto jwker:
Genera una clave privada Ed25519 en formato PEM:
openssl genpkey -algorithm ed25519 -out private-key.pemExtrae la clave pública en formato PEM:
openssl pkey -in private-key.pem -pubout -out public-key.pemSi aún no tienes instalado
jwker, instálalo siguiendo las instrucciones de instalación dejwker.Convierte la clave pública PEM en un archivo JWK con
jwker:jwker public-key.pem public-key.jwkEste comando escribe un solo objeto JWK en
public-key.jwk:{ "kty": "OKP", "crv": "Ed25519", "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs" }Une el objeto JWK dentro de un array
keysde conjunto de JWK (jwks.json) y agrega los camposalg,useykidobligatorios:{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "alg": "EdDSA", "use": "sig", "kid": "OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw", "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs" } ] }
Referencia del campo de conjunto de JWK público
Cada objeto de clave pública en tu array keys de JWK Set admite los siguientes
campos:
| Campo | Valor | Descripción |
|---|---|---|
kty |
"OKP" |
Tipo de clave. Debe ser "OKP" (par de claves de octeto), como se define en RFC 8037. |
crv |
"Ed25519" |
Curva criptográfica. Debe ser "Ed25519". |
alg |
"EdDSA" |
Es el algoritmo que se usará con la clave. Debe ser "EdDSA". |
use |
"sig" |
Es el uso previsto de la clave pública. Establece el valor en "sig" (firma). |
x |
"BASE64URL_PUBLIC_KEY" |
La clave pública Ed25519 sin procesar de 32 bytes, codificada con base64url sin relleno (RFC 4648, sección 5). |
kid |
"KEY_ID" |
Es el identificador de clave único que coincide con el parámetro keyid en tus solicitudes firmadas. Usa la huella digital JWK SHA-256 de RFC 7638 (base64url sin relleno) para la compatibilidad entre proveedores. |
nbf |
UNIX_SECONDS |
Es opcional. Marca de tiempo de Unix en segundos antes de la cual la clave no es válida. |
exp |
UNIX_SECONDS |
Es opcional. Es la marca de tiempo de Unix en segundos después de la cual vence la clave. Los verificadores rechazan las claves vencidas. |
Aloja tu directorio de claves públicas
Los verificadores recuperan tus claves públicas de un extremo HTTPS conocido en el dominio de tu agente.
Requisitos de extremos y encabezados
Para alojar tu directorio de claves, configura tu servidor web de modo que cumpla con los siguientes requisitos:
- Ruta de URL: Entrega el JSON del conjunto de JWK en
https://YOUR_AGENT_DOMAIN/.well-known/http-message-signatures-directory. - Seguridad de transporte: Entrega el extremo a través de HTTPS en el puerto predeterminado
443. No redirecciones las solicitudes a un host o esquema diferente. - Tipo de contenido: Establece el encabezado de respuesta
Content-Typeenapplication/http-message-signatures-directory+json(oapplication/json). - Almacenamiento en caché: Configura el encabezado de respuesta
Cache-Controlcon una directivamax-age(por ejemplo,Cache-Control: public, max-age=3600). - Rotación de claves: Antes de firmar solicitudes con una clave privada nueva, agrega su
JWK pública al array
keysy espera a que venza el TTL deCache-Control. Quita las claves revocadas o vencidas del arraykeys.
En el siguiente ejemplo, se muestra una respuesta HTTP de
GET /.well-known/http-message-signatures-directory:
HTTP/1.1 200 OK
Content-Type: application/http-message-signatures-directory+json
Cache-Control: public, max-age=3600
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"alg": "EdDSA",
"use": "sig",
"kid": "OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw",
"x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs"
}
]
}
Firma una solicitud HTTP saliente
Para autenticar una solicitud HTTPS saliente con WBA, adjunta tres encabezados
obligatorios: Signature-Agent, Signature-Input y Signature.
Encabezados HTTP
Signature-Agent: Especifica el origen HTTPS de tu directorio de claves como una cadena entre comillas:Signature-Agent: "https://agent.example.com"Signature-Input: Asigna una etiqueta de firma (comosig1) a una lista interna de componentes HTTP cubiertos y parámetros de metadatos de firma:Signature-Input: sig1=("@authority" "signature-agent");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"Incluye los siguientes componentes y parámetros cubiertos:
- Componentes cubiertos:
"@authority": Obligatorio. Vincula la firma al host de destino (por ejemplo,www.example.com)."signature-agent": Obligatorio. Vincula la firma a tu encabezadoSignature-Agent.
- Parámetros de metadatos:
created: Es la marca de tiempo de Unix actual en segundos (número entero).expires: Es la marca de tiempo de vencimiento de Unix en segundos (número entero). Estableceexpiresen un máximo de 300 segundos (5 minutos) después decreated.keyid: Es la cadenakidde tu clave pública de tu conjunto de JWK.tag: Debe ser"web-bot-auth".
- Componentes cubiertos:
Signature: Asigna la misma etiqueta de firma (sig1) a la firma Ed25519 codificada en Base64 con el formato de una secuencia de bytes RFC 8941 (unida con dos puntos:):Signature: sig1=:BASE64_ENCODED_SIGNATURE:
Crea la base de la firma
Antes de firmar, construye la cadena base de firma canónica definida en el
artículo 2.5 de la RFC 9421. Cada
componente cubierto aparece en su propia línea en minúsculas, seguido de la
línea @signature-params sin una línea nueva final:
"@authority": www.example.com
"signature-agent": "https://agent.example.com"
"@signature-params": ("@authority" "signature-agent");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"
Firma los bytes UTF-8 de esta cadena base de firma con tu clave privada Ed25519.
Ejemplo de Go
En el siguiente ejemplo, se carga el archivo private-key.pem generado por OpenSSL y
se firma un *http.Request saliente con el paquete crypto/ed25519 estándar de Go
y github.com/dunglas/httpsfv:
package main
import (
"crypto/ed25519"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"net/http"
"strings"
"time"
"github.com/dunglas/httpsfv"
)
// loadPrivateKey parses a PKCS#8 PEM-encoded Ed25519 private key
// generated by OpenSSL.
func loadPrivateKey(pemBytes []byte) (ed25519.PrivateKey, error) {
// Decode the PEM armor to get the raw DER bytes.
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
// OpenSSL generates private keys in PKCS#8 format by default.
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
// Confirm that the parsed key is an Ed25519 private key.
privKey, ok := key.(ed25519.PrivateKey)
if !ok {
return nil, fmt.Errorf("not an Ed25519 private key")
}
return privKey, nil
}
// signRequestEd25519 signs req with WBA headers.
func signRequestEd25519(
req *http.Request,
privKey ed25519.PrivateKey,
keyID string,
) error {
const (
// sigLabel links the Signature-Input and Signature entries.
sigLabel = "sig1"
// agentURL is the HTTPS origin where your JWK Set is hosted.
agentURL = "https://agent.example.com"
)
// 1. Set the Signature-Agent header as a quoted string.
sigAgentHeader := fmt.Sprintf("%q", agentURL)
req.Header.Set("Signature-Agent", sigAgentHeader)
// 2. List the HTTP components to cover in the signature.
// Always cover "@authority" (the target host) and "signature-agent".
authority := strings.ToLower(req.URL.Host)
items := []httpsfv.Item{
httpsfv.NewItem("@authority"),
httpsfv.NewItem("signature-agent"),
}
baseLines := []string{
fmt.Sprintf(`"@authority": %s`, authority),
fmt.Sprintf(`"signature-agent": %s`, sigAgentHeader),
}
// If the optional Agent-Context header is set, sign it too.
if agentCtx := req.Header.Get("Agent-Context"); agentCtx != "" {
items = append(items, httpsfv.NewItem("agent-context"))
line := fmt.Sprintf(`"agent-context": %s`, agentCtx)
baseLines = append(baseLines, line)
}
// 3. Set the signature metadata parameters.
// Set expires to 300 seconds (5 minutes) after created.
now := time.Now().Unix()
params := httpsfv.NewParams()
params.Add("created", now)
params.Add("expires", now+300)
params.Add("keyid", keyID)
params.Add("tag", "web-bot-auth")
// Serialize the covered components and parameters for Signature-Input.
innerList := httpsfv.InnerList{
Items: items,
Params: params,
}
sigParamsStr, err := httpsfv.Marshal(innerList)
if err != nil {
return err
}
// 4. Build the RFC 9421 signature base string.
// Join each covered component line and @signature-params with "\n".
// Don't add a trailing newline after the @signature-params line.
paramLine := fmt.Sprintf(`"@signature-params": %s`, sigParamsStr)
baseLines = append(baseLines, paramLine)
sigBase := strings.Join(baseLines, "\n")
// 5. Sign the UTF-8 bytes of the signature base with your private key.
sigBytes := ed25519.Sign(privKey, []byte(sigBase))
sigB64 := base64.StdEncoding.EncodeToString(sigBytes)
// 6. Attach the Signature-Input and Signature headers to the request.
req.Header.Set("Signature-Input", fmt.Sprintf("%s=%s", sigLabel, sigParamsStr))
req.Header.Set("Signature", fmt.Sprintf("%s=:%s:", sigLabel, sigB64))
return nil
}
Incluye el encabezado experimental Agent-Context
Las plataformas multiusuario (como los proveedores de navegadores alojados o los entornos de ejecución de agentes de nube)
suelen firmar las solicitudes salientes de varios agentes o usuarios de clientes
con una sola clave de firma compartida. De forma predeterminada, la WBA solo identifica el dominio de firma (Signature-Agent), por lo que todo el tráfico firmado por esa clave comparte la misma reputación.
Cuando incluyes y firmas el encabezado Agent-Context, reCAPTCHA
puede evaluar la reputación por separado en diferentes productos, proyectos, usuarios o
sesiones que comparten una clave de firma común en tu plataforma.
Asigna un formato al encabezado Agent-Context
Formatea Agent-Context como un
diccionario de campos estructurados RFC 8941
en el que cada clave es un token en minúsculas y cada valor es una cadena ASCII entre comillas:
Agent-Context: product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"
Todos los campos son opcionales. Incluye solo los campos que se aplican a tu arquitectura:
| Campo | Descripción | Ejemplo |
|---|---|---|
product |
Es el nombre comercial o de la aplicación del agente que realiza la solicitud. | product="ResearchBot" |
project |
Identificador seudónimo de la cuenta, el espacio de trabajo o el proyecto de usuario en la plataforma host. | project="prj_89a0f" |
user |
Identificador seudónimo (como un UUID opaco o un hash con sal) para el cliente o el usuario final. Mantén este valor estable en todas las solicitudes del mismo usuario para generar reputación y nunca incluyas IIP reales. | user="usr_29df3" |
session |
Identificador efímero y opaco para una sola tarea, ejecución de flujo de trabajo o sesión de navegación. | session="sess_88b1" |
Firma el encabezado Agent-Context
reCAPTCHA solo usa Agent-Context para el aislamiento de reputación cuando
incluyes "agent-context" en tu firma de WBA. Se ignoran los encabezados Agent-Context sin firma.
Para firmar el encabezado Agent-Context, completa los siguientes pasos:
Configura el encabezado
Agent-Contexten tu*http.Requestsaliente antes de llamar asignRequestEd25519:req.Header.Set("Agent-Context", `product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"`)Incluye
"agent-context"en la lista de componentesSignature-Inputy la cadena base de la firma (que el ejemplo de Go controla automáticamente):"@authority": www.example.com "signature-agent": "https://agent.example.com" "agent-context": product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1" "@signature-params": ("@authority" "signature-agent" "agent-context");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"
En el siguiente ejemplo, se muestra una solicitud HTTP completa firmada con Agent-Context:
GET /api/data HTTP/1.1
Host: www.example.com
Signature-Agent: "https://agent.example.com"
Agent-Context: product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"
Signature-Input: sig1=("@authority" "signature-agent" "agent-context");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"
Signature: sig1=:BASE64_ENCODED_SIGNATURE:
¿Qué sigue?
Firmar tus solicitudes con WBA demuestra el control criptográfico del dominio de tu agente, pero los sitios web también necesitan metadatos sobre quién opera tu agente y qué propósito tiene.
- Obtén más información sobre cómo los propietarios de sitios usan los indicadores de agentes verificados:
- Supervisa el tráfico del agente en el panel Descripción general del agente.
- Configura políticas de desafío con
la variable de condición
verified_bots.
- Lee las especificaciones del protocolo: