Firma solicitudes salientes con Web Bot Auth

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:

  1. Genera un par de claves Ed25519 y dale formato a la clave pública como un conjunto de claves web JSON (JWK).
  2. Aloja tu directorio de claves públicas en un extremo HTTPS conocido.
  3. Firma cada solicitud HTTP saliente con tu clave privada.
  4. Opcional: Firma el encabezado experimental Agent-Context para 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:

  1. Genera una clave privada Ed25519 en formato PEM:

    openssl genpkey -algorithm ed25519 -out private-key.pem
    
  2. Extrae la clave pública en formato PEM:

    openssl pkey -in private-key.pem -pubout -out public-key.pem
    
  3. Si aún no tienes instalado jwker, instálalo siguiendo las instrucciones de instalación de jwker.

  4. Convierte la clave pública PEM en un archivo JWK con jwker:

    jwker public-key.pem public-key.jwk
    

    Este comando escribe un solo objeto JWK en public-key.jwk:

    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs"
    }
    
  5. Une el objeto JWK dentro de un array keys de conjunto de JWK (jwks.json) y agrega los campos alg, use y kid obligatorios:

    {
      "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-Type en application/http-message-signatures-directory+json (o application/json).
  • Almacenamiento en caché: Configura el encabezado de respuesta Cache-Control con una directiva max-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 keys y espera a que venza el TTL de Cache-Control. Quita las claves revocadas o vencidas del array keys.

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

  1. Signature-Agent: Especifica el origen HTTPS de tu directorio de claves como una cadena entre comillas:

    Signature-Agent: "https://agent.example.com"
    
  2. Signature-Input: Asigna una etiqueta de firma (como sig1) 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 encabezado Signature-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). Establece expires en un máximo de 300 segundos (5 minutos) después de created.
      • keyid: Es la cadena kid de tu clave pública de tu conjunto de JWK.
      • tag: Debe ser "web-bot-auth".
  3. 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:

  1. Configura el encabezado Agent-Context en tu *http.Request saliente antes de llamar a signRequestEd25519:

    req.Header.Set("Agent-Context", `product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"`)
    
  2. Incluye "agent-context" en la lista de componentes Signature-Input y 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.