Signer les requêtes sortantes avec Web Bot Auth

Le protocole Web Bot Auth (WBA) vous permet de prouver l'identité de votre agent ou de votre robot de manière cryptographique à l'aide de signatures de messages HTTP (RFC 9421).

Lorsque vous signez des requêtes sortantes, des services tels que Google Cloud Fraud Defense (l'évolution de reCAPTCHA) peuvent valider votre agent directement sans se fier uniquement aux adresses IP ou aux chaînes User-Agent.

Pour authentifier votre agent avec WBA, procédez comme suit :

  1. Générez une paire de clés Ed25519 et formatez la clé publique en tant que jeu de clés Web JSON (JWK).
  2. Hébergez votre répertoire de clés publiques sur un point de terminaison HTTPS connu.
  3. Signez chaque requête HTTP sortante avec votre clé privée.
  4. Facultatif : Signez l'en-tête expérimental Agent-Context pour isoler la réputation entre les locataires qui partagent une clé de signature.

Générer une paire de clés Ed25519

WBA nécessite une paire de clés asymétriques Ed25519 (RFC 8032). Vous gardez la clé privée secrète pour signer les requêtes HTTP sortantes et vous publiez la clé publique dans un ensemble de clés Web JSON (JWK) (RFC 7517).

Générer une paire de clés avec OpenSSL et jwker

Vous pouvez générer une paire de clés Ed25519 avec OpenSSL et convertir la clé publique en JWK à l'aide de l'outil Open Source jwker :

  1. Générez une clé privée Ed25519 au format PEM :

    openssl genpkey -algorithm ed25519 -out private-key.pem
    
  2. Extrayez la clé publique au format PEM :

    openssl pkey -in private-key.pem -pubout -out public-key.pem
    
  3. Si vous n'avez pas encore installé jwker, installez-le en suivant les instructions d'installation de jwker.

  4. Convertissez la clé publique PEM en fichier JWK à l'aide de jwker :

    jwker public-key.pem public-key.jwk
    

    Cette commande écrit un seul objet JWK dans public-key.jwk :

    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs"
    }
    
  5. Encapsulez l'objet JWK dans un tableau d'ensemble JWK keys (jwks.json) et ajoutez les champs alg, use et kid requis :

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

Documentation de référence sur les champs de l'ensemble JWK public

Chaque objet de clé publique de votre tableau JWK Set keys accepte les champs suivants :

Champ Valeur Description
kty "OKP" Type de clé. Doit être "OKP" (paire de clés d'octets), comme défini dans la RFC 8037.
crv "Ed25519" Courbe cryptographique. doit être "Ed25519"
alg "EdDSA" Algorithme destiné à être utilisé avec la clé. doit être "EdDSA"
use "sig" Utilisation prévue de la clé publique. Définissez cet élément sur "sig" (signature).
x "BASE64URL_PUBLIC_KEY" Clé publique Ed25519 brute de 32 octets, encodée en base64url sans marge (RFC 4648, section 5).
kid "KEY_ID" Identifiant de clé unique correspondant au paramètre keyid dans vos requêtes signées. Utilisez l'empreinte JWK SHA-256 RFC 7638 (base64url sans remplissage) pour assurer la compatibilité entre les fournisseurs.
nbf UNIX_SECONDS Facultatif. Code temporel Unix en secondes avant lequel la clé n'est pas valide.
exp UNIX_SECONDS Facultatif. Code temporel Unix en secondes après lequel la clé expire. Les vérificateurs rejettent les clés expirées.

Héberger votre répertoire de clés publiques

Les vérificateurs récupèrent vos clés publiques à partir d'un point de terminaison HTTPS connu sur le domaine de votre agent.

Exigences relatives aux points de terminaison et aux en-têtes

Pour héberger votre répertoire de clés, configurez votre serveur Web de sorte qu'il réponde aux exigences suivantes :

  • Chemin d'URL : diffusez le JSON du jeu de JWK sur https://YOUR_AGENT_DOMAIN/.well-known/http-message-signatures-directory.
  • Sécurité du transport : diffusez le point de terminaison via HTTPS sur le port par défaut 443. Ne redirigez pas les requêtes vers un autre hôte ou schéma.
  • Type de contenu : définissez l'en-tête de réponse Content-Type sur application/http-message-signatures-directory+json (ou application/json).
  • Mise en cache : définissez l'en-tête de réponse Cache-Control avec une directive max-age (par exemple, Cache-Control: public, max-age=3600).
  • Rotation des clés : avant de signer des requêtes avec une nouvelle clé privée, ajoutez sa clé JWK publique au tableau keys et attendez l'expiration de votre TTL Cache-Control. Supprimez les clés révoquées ou expirées du tableau keys.

L'exemple suivant montre une réponse 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"
    }
  ]
}

Signer une requête HTTP sortante

Pour authentifier une requête HTTPS sortante avec WBA, associez trois en-têtes obligatoires : Signature-Agent, Signature-Input et Signature.

En-têtes HTTP

  1. Signature-Agent : spécifie l'origine HTTPS de votre annuaire de clés sous forme de chaîne entre guillemets :

    Signature-Agent: "https://agent.example.com"
    
  2. Signature-Input : mappe un libellé de signature (tel que sig1) à une liste interne de composants HTTP couverts et de paramètres de métadonnées de signature :

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

    Incluez les composants et paramètres couverts suivants :

    • Composants couverts :
      • "@authority" : valeur obligatoire. Lie la signature à l'hôte de destination (par exemple, www.example.com).
      • "signature-agent" : valeur obligatoire. Lie la signature à votre en-tête Signature-Agent.
    • Paramètres de métadonnées :
      • created : code temporel Unix actuel en secondes (entier).
      • expires : code temporel Unix d'expiration en secondes (entier). Définissez expires sur 300 secondes (5 minutes) au maximum après created.
      • keyid : chaîne kid de votre clé publique à partir de votre ensemble JWK.
      • tag : doit être défini sur "web-bot-auth".
  3. Signature : mappe le même libellé de signature (sig1) à la signature Ed25519 encodée en base64, formatée en tant que séquence d'octets RFC 8941 (encadrée par des deux-points :) :

    Signature: sig1=:BASE64_ENCODED_SIGNATURE:
    

Construire la base de la signature

Avant de signer, créez la chaîne de base de signature canonique définie dans la section 2.5 de la norme RFC 9421. Chaque composant couvert apparaît sur sa propre ligne en minuscules, suivi de la ligne @signature-params sans saut de ligne 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"

Signez les octets UTF-8 de cette chaîne de base de signature à l'aide de votre clé privée Ed25519.

Exemple Go

L'exemple suivant charge le fichier private-key.pem généré par OpenSSL et signe un *http.Request sortant à l'aide du package crypto/ed25519 standard de Go et de 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
}

Inclure l'en-tête expérimental Agent-Context

Les plates-formes mutualisées (comme les fournisseurs de navigateurs hébergés ou les environnements d'exécution d'agents cloud) signent souvent les requêtes sortantes de plusieurs agents ou locataires clients à l'aide d'une seule clé de signature partagée. Par défaut, WBA n'identifie que le domaine de signature (Signature-Agent). Par conséquent, tout le trafic signé par cette clé partage la même réputation.

Lorsque vous incluez et signez l'en-tête Agent-Context, reCAPTCHA peut évaluer la réputation séparément pour différents produits, projets, utilisateurs ou sessions qui partagent une clé de signature commune sur votre plate-forme.

Formater l'en-tête Agent-Context

Mettez en forme Agent-Context en tant que dictionnaire de champs structurés RFC 8941, où chaque clé est un jeton en minuscules et chaque valeur est une chaîne ASCII entre guillemets :

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

Tous les champs sont facultatifs. N'incluez que les champs qui s'appliquent à votre architecture :

Champ Description Exemple
product Nom de l'application ou nom commercial de l'agent à l'origine de la requête. product="ResearchBot"
project Identifiant de compte, d'espace de travail ou de projet de locataire pseudonymisé sur la plate-forme hôte. project="prj_89a0f"
user Identifiant pseudonymisé (tel qu'un UUID opaque ou un hachage salé) du client ou de l'utilisateur final. Conservez cette valeur stable pour les requêtes d'un même utilisateur afin de renforcer la réputation, et n'incluez jamais d'informations permettant d'identifier l'utilisateur réelles. user="usr_29df3"
session Identifiant éphémère et opaque pour une tâche, une exécution de workflow ou une session de navigation uniques. session="sess_88b1"

Signer l'en-tête Agent-Context

reCAPTCHA n'utilise Agent-Context pour l'isolation de la réputation que si vous incluez "agent-context" dans votre signature WBA. Les en-têtes Agent-Context non signés sont ignorés.

Pour signer l'en-tête Agent-Context, procédez comme suit :

  1. Définissez l'en-tête Agent-Context sur votre *http.Request sortant avant d'appeler signRequestEd25519 :

    req.Header.Set("Agent-Context", `product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"`)
    
  2. Incluez "agent-context" dans la liste des composants Signature-Input et la chaîne de base de la signature (gérée automatiquement par l'exemple 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'exemple suivant montre une requête HTTP complète signée avec 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:

Étapes suivantes

La signature de vos requêtes avec WBA prouve le contrôle cryptographique du domaine de votre agent, mais les sites Web ont également besoin de métadonnées sur l'opérateur de votre agent et son objectif.