Firma le richieste in uscita con Web Bot Auth

Il protocollo Web Bot Auth (WBA) ti consente di dimostrare tramite crittografia l'identità del tuo agente o bot utilizzando le firme dei messaggi HTTP (RFC 9421).

Quando firmi le richieste in uscita, servizi come Google Cloud Fraud Defense (l'evoluzione di reCAPTCHA) possono verificare direttamente il tuo agente senza fare affidamento solo sugli indirizzi IP o sulle stringhe User-Agent.

Per autenticare l'agente con WBA, completa le seguenti attività:

  1. Genera una coppia di chiavi Ed25519 e formatta la chiave pubblica come set di chiavi web JSON (JWK).
  2. Ospita la directory delle chiavi pubbliche in un endpoint HTTPS noto.
  3. Firma ogni richiesta HTTP in uscita con la tua chiave privata.
  4. (Facoltativo) Firma l'intestazione sperimentale Agent-Context per isolare la reputazione tra i tenant che condividono una chiave di firma.

Genera una coppia di chiavi Ed25519

WBA richiede una coppia di chiavi asimmetriche Ed25519 (RFC 8032). La chiave privata viene mantenuta segreta per firmare le richieste HTTP in uscita, mentre la chiave pubblica viene pubblicata in un set di chiavi web JSON (JWK) (RFC 7517).

Genera una coppia di chiavi con OpenSSL e jwker

Puoi generare una coppia di chiavi Ed25519 con OpenSSL e convertire la chiave pubblica in un JWK utilizzando lo strumento open source jwker:

  1. Genera una chiave privata Ed25519 in formato PEM:

    openssl genpkey -algorithm ed25519 -out private-key.pem
    
  2. Estrai la chiave pubblica in formato PEM:

    openssl pkey -in private-key.pem -pubout -out public-key.pem
    
  3. Se non hai ancora installato jwker, installalo seguendo le istruzioni di installazione di jwker.

  4. Converti la chiave pubblica PEM in un file JWK utilizzando jwker:

    jwker public-key.pem public-key.jwk
    

    Questo comando scrive un singolo oggetto JWK in public-key.jwk:

    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs"
    }
    
  5. Aggrega l'oggetto JWK all'interno di un array JWK Set keys (jwks.json) e aggiungi i campi obbligatori alg, use e kid:

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

Riferimento per il campo Set di JWK pubblici

Ogni oggetto chiave pubblica nell'array JWK Set keys supporta i seguenti campi:

Campo Valore Descrizione
kty "OKP" Tipo di chiave. Deve essere "OKP" (Octet Key Pair) come definito nella RFC 8037.
crv "Ed25519" Curva crittografica. Deve essere "Ed25519".
alg "EdDSA" Algoritmo destinato all'utilizzo con la chiave. Deve essere "EdDSA".
use "sig" Uso previsto della chiave pubblica. Impostalo su "sig" (firma).
x "BASE64URL_PUBLIC_KEY" La chiave pubblica Ed25519 non elaborata a 32 byte, codificata utilizzando base64url senza spaziatura interna (RFC 4648 Sezione 5).
kid "KEY_ID" Identificatore univoco della chiave che corrisponde al parametro keyid nelle richieste firmate. Utilizza l'impronta JWK SHA-256 RFC 7638 (base64url senza spaziatura interna) per la compatibilità tra fornitori.
nbf UNIX_SECONDS Facoltativo. Timestamp Unix in secondi prima del quale la chiave non è valida.
exp UNIX_SECONDS Facoltativo. Timestamp Unix in secondi dopo il quale la chiave scade. I verificatori rifiutano le chiavi scadute.

Ospita la directory delle chiavi pubbliche

I verificatori recuperano le tue chiavi pubbliche da un endpoint HTTPS noto sul dominio del tuo agente.

Requisiti per endpoint e intestazioni

Per ospitare la directory delle chiavi, configura il server web in modo che soddisfi i seguenti requisiti:

  • Percorso dell'URL: pubblica il set di JWK JSON all'indirizzo https://YOUR_AGENT_DOMAIN/.well-known/http-message-signatures-directory.
  • Sicurezza del trasporto: l'endpoint deve essere accessibile tramite HTTPS sulla porta predefinita 443. Non reindirizzare le richieste a un host o uno schema diverso.
  • Tipo di contenuti: imposta l'intestazione della risposta Content-Type su application/http-message-signatures-directory+json (o application/json).
  • Memorizzazione nella cache: imposta l'intestazione della risposta Cache-Control con un'istruzione max-age (ad esempio, Cache-Control: public, max-age=3600).
  • Rotazione delle chiavi: prima di firmare le richieste con una nuova chiave privata, aggiungi la relativa JWK pubblica all'array keys e attendi la scadenza del TTL Cache-Control. Rimuovi le chiavi revocate o scadute dall'array keys.

L'esempio seguente mostra una risposta HTTP da 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 richiesta HTTP in uscita

Per autenticare una richiesta HTTPS in uscita con WBA, allega tre intestazioni obbligatorie: Signature-Agent, Signature-Input e Signature.

Intestazioni HTTP

  1. Signature-Agent: specifica l'origine HTTPS della directory delle chiavi come stringa tra virgolette:

    Signature-Agent: "https://agent.example.com"
    
  2. Signature-Input: mappa un'etichetta di firma (ad esempio sig1) a un elenco interno di componenti HTTP coperti e parametri dei metadati della firma:

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

    Includi i seguenti componenti e parametri coperti:

    • Componenti coperti:
      • "@authority": obbligatorio. Associa la firma all'host di destinazione (ad esempio, www.example.com).
      • "signature-agent": obbligatorio. Associa la firma all'intestazione Signature-Agent.
    • Parametri dei metadati:
      • created: timestamp Unix corrente in secondi (numero intero).
      • expires: timestamp Unix di scadenza in secondi (numero intero). Imposta expires su un massimo di 300 secondi (5 minuti) dopo created.
      • keyid: la stringa kid della chiave pubblica dal set JWK.
      • tag: deve essere "web-bot-auth".
  3. Signature: mappa la stessa etichetta della firma (sig1) alla firma Ed25519 con codifica Base64 formattata come sequenza di byte RFC 8941 (racchiusa tra due punti :):

    Signature: sig1=:BASE64_ENCODED_SIGNATURE:
    

Costruisci la base della firma

Prima di firmare, crea la stringa di base della firma canonica definita nella sezione 2.5 di RFC 9421. Ogni componente coperto viene visualizzato su una riga separata in minuscolo, seguito dalla riga @signature-params senza nuova riga finale:

"@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 i byte UTF-8 di questa stringa di base della firma utilizzando la tua chiave privata Ed25519.

Esempio di Go

L'esempio seguente carica il file private-key.pem generato da OpenSSL e firma un *http.Request in uscita utilizzando il pacchetto crypto/ed25519 standard di 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
}

Includi l'intestazione Agent-Context sperimentale

Le piattaforme multi-tenant (come i provider di browser in hosting o i runtime degli agenti cloud) spesso firmano le richieste in uscita da più agenti o tenant dei clienti utilizzando una singola chiave di firma condivisa. Per impostazione predefinita, WBA identifica solo il dominio di firma (Signature-Agent), pertanto tutto il traffico firmato da quella chiave condivide la stessa reputazione.

Quando includi e firmi l'intestazione Agent-Context, reCAPTCHA può valutare la reputazione separatamente per prodotti, progetti, utenti o sessioni distinti che condividono una chiave di firma comune sulla tua piattaforma.

Formatta l'intestazione Agent-Context

Formatta Agent-Context come dizionario di campi strutturati RFC 8941 in cui ogni chiave è un token in minuscolo e ogni valore è una stringa ASCII tra virgolette:

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

Tutti i campi sono facoltativi. Includi solo i campi applicabili alla tua architettura:

Campo Descrizione Esempio
product Nome dell'applicazione o nome commerciale dell'agente che effettua la richiesta. product="ResearchBot"
project Identificatore pseudonimizzato dell'account, dello spazio di lavoro o del progetto tenant sulla piattaforma host. project="prj_89a0f"
user Identificatore pseudonimo (ad esempio un UUID opaco o un hash con salt) per il cliente o l'utente finale. Mantieni questo valore stabile tra le richieste dello stesso utente per creare reputazione e non includere mai PII reali. user="usr_29df3"
session Identificatore temporaneo e opaco per una singola attività, un'esecuzione del workflow o una sessione di navigazione. session="sess_88b1"

Firma l'intestazione Agent-Context

reCAPTCHA utilizza Agent-Context solo per l'isolamento della reputazione quando includi "agent-context" nella firma WBA. Le intestazioni Agent-Context non firmate vengono ignorate.

Per firmare l'intestazione Agent-Context, completa i seguenti passaggi:

  1. Imposta l'intestazione Agent-Context sulla *http.Request in uscita prima di chiamare signRequestEd25519:

    req.Header.Set("Agent-Context", `product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"`)
    
  2. Includi "agent-context" nell'elenco dei componenti Signature-Input e nella stringa di base della firma (gestita automaticamente dall'esempio 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"
    

L'esempio seguente mostra una richiesta HTTP completa firmata 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:

Passaggi successivi

La firma delle richieste con WBA dimostra il controllo crittografico del dominio del tuo agente, ma i siti web hanno anche bisogno di metadati su chi gestisce l'agente e per quale scopo.