Assinar solicitações de saída com o Web Bot Auth

O protocolo Web Bot Auth (WBA) permite que você comprove criptograficamente a identidade do seu agente ou bot usando assinaturas de mensagens HTTP (RFC 9421).

Quando você assina solicitações de saída, serviços como o Google Cloud Fraud Defense (a evolução do reCAPTCHA) podem verificar seu agente diretamente sem depender apenas de endereços IP ou strings User-Agent.

Para autenticar seu agente com a WBA, conclua as seguintes tarefas:

  1. Gere um par de chaves Ed25519 e formate a chave pública como um conjunto de chaves JSON da Web (JWK).
  2. Hospede seu diretório de chaves públicas em um endpoint HTTPS conhecido.
  3. Assine cada solicitação HTTP de saída com sua chave privada.
  4. Opcional: assine o cabeçalho experimental Agent-Context para isolar a reputação entre locatários que compartilham uma chave de assinatura.

Gerar um par de chaves Ed25519

A WBA requer um par de chaves assimétricas Ed25519 (RFC 8032). Mantenha a chave privada em segredo para assinar solicitações HTTP de saída e publique a chave pública em um conjunto de chaves da Web JSON (JWK) (RFC 7517).

Gerar um par de chaves com OpenSSL e jwker

É possível gerar um par de chaves Ed25519 com o OpenSSL e converter a chave pública em um JWK usando a ferramenta de código aberto jwker:

  1. Gere uma chave privada Ed25519 no formato PEM:

    openssl genpkey -algorithm ed25519 -out private-key.pem
    
  2. Extraia a chave pública no formato PEM:

    openssl pkey -in private-key.pem -pubout -out public-key.pem
    
  3. Se você ainda não tiver o jwker instalado, siga as instruções de instalação do jwker.

  4. Converta a chave pública PEM em um arquivo JWK usando jwker:

    jwker public-key.pem public-key.jwk
    

    Esse comando grava um único objeto JWK em public-key.jwk:

    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs"
    }
    
  5. Envolva o objeto JWK em uma matriz de conjunto de JWK keys (jwks.json) e adicione os campos obrigatórios alg, use e kid:

    {
      "keys": [
        {
          "kty": "OKP",
          "crv": "Ed25519",
          "alg": "EdDSA",
          "use": "sig",
          "kid": "OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw",
          "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs"
        }
      ]
    }
    

Referência de campo do conjunto de JWKs público

Cada objeto de chave pública na matriz keys do conjunto de JWK é compatível com os seguintes campos:

Campo Valor Descrição
kty "OKP" Tipo de chave. Precisa ser "OKP" (par de chaves de octeto), conforme definido na RFC 8037.
crv "Ed25519" Curva criptográfica. Precisa ser "Ed25519".
alg "EdDSA" Algoritmo destinado ao uso com a chave. Precisa ser "EdDSA".
use "sig" Uso pretendido da chave pública. Defina como "sig" (assinatura).
x "BASE64URL_PUBLIC_KEY" A chave pública Ed25519 bruta de 32 bytes, codificada usando base64url sem preenchimento (RFC 4648, seção 5).
kid "KEY_ID" Identificador de chave exclusivo que corresponde ao parâmetro keyid nas suas solicitações assinadas. Use a impressão digital JWK SHA-256 RFC 7638 (base64url sem preenchimento) para compatibilidade entre fornecedores.
nbf UNIX_SECONDS Opcional. Carimbo de data/hora Unix em segundos antes do qual a chave não é válida.
exp UNIX_SECONDS Opcional. Carimbo de data/hora Unix em segundos após o qual a chave expira. Os verificadores rejeitam chaves expiradas.

Hospedar o diretório de chaves públicas

Os verificadores buscam suas chaves públicas em um endpoint HTTPS conhecido no domínio do seu agente.

Requisitos de endpoint e cabeçalho

Para hospedar o diretório de chaves, configure o servidor da Web para atender aos seguintes requisitos:

  • Caminho do URL: disponibilize o JSON do conjunto de JWKs em https://YOUR_AGENT_DOMAIN/.well-known/http-message-signatures-directory.
  • Segurança de transporte: disponibilize o endpoint por HTTPS na porta padrão 443. Não redirecione as solicitações para um host ou esquema diferente.
  • Tipo de conteúdo: defina o cabeçalho de resposta Content-Type como application/http-message-signatures-directory+json (ou application/json).
  • Armazenamento em cache: defina o cabeçalho de resposta Cache-Control com uma diretiva max-age (por exemplo, Cache-Control: public, max-age=3600).
  • Rotação de chaves: antes de assinar solicitações com uma nova chave privada, adicione a JWK pública dela à matriz keys e aguarde o TTL de Cache-Control expirar. Remova as chaves revogadas ou expiradas da matriz keys.

O exemplo a seguir mostra uma resposta 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"
    }
  ]
}

Assinar uma solicitação HTTP de saída

Para autenticar uma solicitação HTTPS de saída com a WBA, anexe três cabeçalhos obrigatórios: Signature-Agent, Signature-Input e Signature.

Cabeçalhos HTTP

  1. Signature-Agent: especifica a origem HTTPS do diretório de chaves como uma string entre aspas:

    Signature-Agent: "https://agent.example.com"
    
  2. Signature-Input: mapeia um rótulo de assinatura (como sig1) para uma lista interna de componentes HTTP cobertos e parâmetros de metadados de assinatura:

    Signature-Input: sig1=("@authority" "signature-agent");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"
    

    Inclua os seguintes componentes e parâmetros cobertos:

    • Componentes cobertos:
      • "@authority": obrigatório. Vincula a assinatura ao host de destino (por exemplo, www.example.com).
      • "signature-agent": obrigatório. Vincula a assinatura ao cabeçalho Signature-Agent.
    • Parâmetros de metadados:
      • created: carimbo de data/hora Unix atual em segundos (número inteiro).
      • expires: carimbo de data/hora Unix de validade em segundos (número inteiro). Defina expires como no máximo 300 segundos (5 minutos) após created.
      • keyid: a string kid da chave pública do conjunto de JWK.
      • tag: precisa ser "web-bot-auth".
  3. Signature: mapeia o mesmo rótulo de assinatura (sig1) para a assinatura Ed25519 codificada em base64 formatada como uma sequência de bytes RFC 8941 (envolto em dois pontos :):

    Signature: sig1=:BASE64_ENCODED_SIGNATURE:
    

Criar a base da assinatura

Antes de assinar, construa a string de base de assinatura canônica definida na Seção 2.5 da RFC 9421. Cada componente coberto aparece na própria linha em letras minúsculas, seguido pela linha @signature-params sem nova linha 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"

Assine os bytes UTF-8 dessa string de base de assinatura usando sua chave privada Ed25519.

Exemplo de Go

O exemplo a seguir carrega o arquivo private-key.pem gerado pelo OpenSSL e assina um *http.Request de saída usando o pacote crypto/ed25519 padrão do Go e 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
}

Incluir o cabeçalho experimental Agent-Context

As plataformas multilocatárias (como provedores de navegadores hospedados ou ambientes de execução de agentes de nuvem) costumam assinar solicitações de saída de vários agentes ou locatários de clientes usando uma única chave de assinatura compartilhada. Por padrão, a WBA identifica apenas o domínio de assinatura (Signature-Agent), então todo o tráfego assinado por essa chave compartilha a mesma reputação.

Quando você inclui e assina o cabeçalho Agent-Context, o reCAPTCHA pode avaliar a reputação separadamente em diferentes produtos, projetos, usuários ou sessões que compartilham uma chave de assinatura comum na sua plataforma.

Formatar o cabeçalho Agent-Context

Formate Agent-Context como um dicionário de campos estruturados RFC 8941, em que cada chave é um token em letras minúsculas e cada valor é uma string ASCII entre aspas:

Agent-Context: product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"

Todos os campos são opcionais. Inclua apenas os campos que se aplicam à sua arquitetura:

Campo Descrição Exemplo
product Nome comercial ou do aplicativo do agente que faz a solicitação. product="ResearchBot"
project Identificador de conta, espaço de trabalho ou projeto de locatário pseudônimo na plataforma host. project="prj_89a0f"
user Identificador pseudônimo (como um UUID opaco ou hash com sal) para o cliente ou usuário final. Mantenha esse valor estável em todas as solicitações do mesmo usuário para criar reputação e nunca inclua PIIs reais. user="usr_29df3"
session Identificador temporário e opaco para uma única tarefa, execução de fluxo de trabalho ou sessão de navegação. session="sess_88b1"

Assinar o cabeçalho Agent-Context

O reCAPTCHA só usa Agent-Context para isolamento de reputação quando você inclui "agent-context" na assinatura do WBA. Os cabeçalhos Agent-Context não assinados são ignorados.

Para assinar o cabeçalho Agent-Context, siga estas etapas:

  1. Defina o cabeçalho Agent-Context no *http.Request de saída antes de chamar signRequestEd25519:

    req.Header.Set("Agent-Context", `product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"`)
    
  2. Inclua "agent-context" na lista de componentes Signature-Input e na string de base de assinatura (processada automaticamente pelo exemplo em Go):

    "@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"
    

O exemplo a seguir mostra uma solicitação HTTP completa assinada com 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:

A seguir

Assinar as solicitações com o WBA comprova o controle criptográfico do domínio do seu agente, mas os sites também precisam de metadados sobre quem opera o agente e qual é a finalidade dele.