Ausgehende Anfragen mit Web Bot Auth signieren

Mit dem Web Bot Auth (WBA)-Protokoll können Sie die Identität Ihres Agenten oder Bots mithilfe von HTTP-Nachrichtensignaturen (RFC 9421) kryptografisch nachweisen.

Wenn Sie ausgehende Anfragen signieren, können Dienste wie Google Cloud Fraud Defense (die Weiterentwicklung von reCAPTCHA) Ihren Agenten direkt verifizieren, ohne sich nur auf IP-Adressen oder User-Agent-Strings zu verlassen.

Führen Sie die folgenden Aufgaben aus, um Ihren Agenten bei WBA zu authentifizieren:

  1. Generieren Sie ein Ed25519-Schlüsselpaar und formatieren Sie den öffentlichen Schlüssel als JSON Web Key (JWK)-Set.
  2. Hosten Sie Ihr Verzeichnis mit öffentlichen Schlüsseln an einem bekannten HTTPS-Endpunkt.
  3. Signieren Sie jede ausgehende HTTP-Anfrage mit Ihrem privaten Schlüssel.
  4. Optional: Signieren Sie den experimentellen Agent-Context-Header, um die Reputation zwischen Mandanten zu isolieren, die einen Signaturschlüssel gemeinsam nutzen.

Ed25519-Schlüsselpaar generieren

Für die WBA ist ein asymmetrisches Ed25519-Schlüsselpaar (RFC 8032) erforderlich. Sie halten den privaten Schlüssel geheim, um ausgehende HTTP-Anfragen zu signieren, und veröffentlichen den öffentlichen Schlüssel in einem JSON Web Key (JWK) Set (RFC 7517).

Schlüsselpaar mit OpenSSL und jwker generieren

Sie können ein Ed25519-Schlüsselpaar mit OpenSSL generieren und den öffentlichen Schlüssel mit dem Open-Source-Tool jwker in einen JWK konvertieren:

  1. Generieren Sie einen privaten Ed25519-Schlüssel im PEM-Format:

    openssl genpkey -algorithm ed25519 -out private-key.pem
    
  2. Extrahieren Sie den öffentlichen Schlüssel im PEM-Format:

    openssl pkey -in private-key.pem -pubout -out public-key.pem
    
  3. Wenn Sie jwker noch nicht installiert haben, installieren Sie es gemäß der Installationsanleitung für jwker.

  4. Konvertieren Sie den öffentlichen PEM-Schlüssel mit jwker in eine JWK-Datei:

    jwker public-key.pem public-key.jwk
    

    Mit diesem Befehl wird ein einzelnes JWK-Objekt in public-key.jwk geschrieben:

    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs"
    }
    
  5. Verpacken Sie das JWK-Objekt in ein JWK-Set-keys-Array (jwks.json) und fügen Sie die erforderlichen Felder alg, use und kid hinzu:

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

Feldreferenz für öffentliches JWK-Set

Jedes öffentliche Schlüsselobjekt im JWK-Satz-Array keys unterstützt die folgenden Felder:

Feld Wert Beschreibung
kty "OKP" Schlüsseltyp. Muss "OKP" (Oktett-Schlüsselpaar) sein, wie in RFC 8037 definiert.
crv "Ed25519" Kryptografische Kurve. Muss "Ed25519" lauten.
alg "EdDSA" Algorithmus, der für die Verwendung mit dem Schlüssel vorgesehen ist. Muss "EdDSA" lauten.
use "sig" Verwendungszweck des öffentlichen Schlüssels. Legen Sie "sig" (Signatur) fest.
x "BASE64URL_PUBLIC_KEY" Der 32-Byte-Ed25519-Rohschlüssel, codiert mit nicht aufgefülltem base64url (RFC 4648, Abschnitt 5).
kid "KEY_ID" Eindeutige Schlüssel-ID, die mit dem Parameter keyid in Ihren signierten Anfragen übereinstimmt. Verwenden Sie den SHA-256-JWK-Fingerabdruck (nicht aufgefülltes base64url) gemäß RFC 7638, um die Kompatibilität mit anderen Anbietern zu gewährleisten.
nbf UNIX_SECONDS Optional. Unix-Zeitstempel in Sekunden, vor dem der Schlüssel ungültig ist.
exp UNIX_SECONDS Optional. Unix-Zeitstempel in Sekunden, nach denen der Schlüssel abläuft. Prüfer lehnen abgelaufene Schlüssel ab.

Öffentliches Schlüsselverzeichnis hosten

Prüfer rufen Ihre öffentlichen Schlüssel von einem bekannten HTTPS-Endpunkt in der Domain Ihres Agents ab.

Anforderungen an Endpunkte und Header

Konfigurieren Sie Ihren Webserver so, dass er die folgenden Anforderungen erfüllt, um Ihr Schlüsselverzeichnis zu hosten:

  • URL-Pfad: Stellen Sie die JWK-Set-JSON unter https://YOUR_AGENT_DOMAIN/.well-known/http-message-signatures-directory bereit.
  • Transportsicherheit: Stellen Sie den Endpunkt über HTTPS auf dem Standardport 443 bereit. Leiten Sie Anfragen nicht an einen anderen Host oder ein anderes Schema weiter.
  • Inhaltstyp: Legen Sie den Antwortheader Content-Type auf application/http-message-signatures-directory+json (oder application/json) fest.
  • Caching: Legen Sie den Antwortheader Cache-Control mit einer max-age-Anweisung fest (z. B. Cache-Control: public, max-age=3600).
  • Schlüsselrotation: Bevor Sie Anfragen mit einem neuen privaten Schlüssel signieren, fügen Sie den öffentlichen JWK dem keys-Array hinzu und warten Sie, bis die TTL von Cache-Control abgelaufen ist. Entfernen Sie widerrufene oder abgelaufene Schlüssel aus dem keys-Array.

Das folgende Beispiel zeigt eine HTTP-Antwort von 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"
    }
  ]
}

Ausgehende HTTP-Anfrage signieren

Wenn Sie eine ausgehende HTTPS-Anfrage mit WBA authentifizieren möchten, hängen Sie drei erforderliche Header an: Signature-Agent, Signature-Input und Signature.

HTTP-Header

  1. Signature-Agent: Gibt den HTTPS-Ursprung Ihres Schlüsselverzeichnisses als String in Anführungszeichen an:

    Signature-Agent: "https://agent.example.com"
    
  2. Signature-Input: Ordnet ein Signaturlabel (z. B. sig1) einer inneren Liste von abgedeckten HTTP-Komponenten und Signaturmetadatenparametern zu:

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

    Fügen Sie die folgenden abgedeckten Komponenten und Parameter hinzu:

    • Abgedeckte Komponenten:
      • "@authority": erforderlich. Bindet die Signatur an den Zielhost (z. B. www.example.com).
      • "signature-agent": erforderlich. Bindet die Signatur an den Signature-Agent-Header.
    • Metadatenparameter:
      • created: Aktueller Unix-Zeitstempel in Sekunden (Ganzzahl).
      • expires: Unix-Zeitstempel für den Ablauf in Sekunden (Ganzzahl). Legen Sie expires auf maximal 300 Sekunden (5 Minuten) nach created fest.
      • keyid: Der kid-String Ihres öffentlichen Schlüssels aus Ihrem JWK-Satz.
      • tag: Muss "web-bot-auth" sein.
  3. Signature: Ordnet dasselbe Signaturlabel (sig1) der base64-codierten Ed25519-Signatur zu, die als RFC 8941-Bytesequenz formatiert ist (in : Doppelpunkte eingeschlossen):

    Signature: sig1=:BASE64_ENCODED_SIGNATURE:
    

Signaturbasis erstellen

Erstellen Sie vor dem Signieren den kanonischen Signaturbasisstring, der in RFC 9421, Abschnitt 2.5 definiert ist. Jede abgedeckte Komponente wird in einer eigenen Zeile in Kleinbuchstaben angezeigt, gefolgt von der Zeile @signature-params ohne nachfolgenden Zeilenumbruch:

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

Signieren Sie die UTF-8-Bytes dieses Signaturbasisstrings mit Ihrem privaten Ed25519-Schlüssel.

Go-Beispiel

Im folgenden Beispiel wird die von OpenSSL generierte private-key.pem-Datei geladen und ein ausgehender *http.Request mit dem Standardpaket crypto/ed25519 von Go und github.com/dunglas/httpsfv signiert:

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
}

Experimentellen Agent-Context-Header einfügen

Mehrmandantenfähige Plattformen (z. B. gehostete Browseranbieter oder Cloud-Agent-Laufzeiten) signieren ausgehende Anfragen von mehreren Kunden-Agents oder Mandanten häufig mit einem einzigen freigegebenen Signaturschlüssel. Standardmäßig identifiziert die WBA nur die Signaturdomain (Signature-Agent). Daher hat der gesamte mit diesem Schlüssel signierte Traffic dieselbe Reputation.

Wenn Sie den Header Agent-Context einfügen und signieren, kann reCAPTCHA die Reputation separat für verschiedene Produkte, Projekte, Nutzer oder Sitzungen bewerten, die einen gemeinsamen Signaturschlüssel auf Ihrer Plattform verwenden.

Header „Agent-Context“ formatieren

Formatieren Sie Agent-Context als strukturiertes Feldverzeichnis gemäß RFC 8941, wobei jeder Schlüssel ein kleingeschriebenes Token und jeder Wert ein in Anführungszeichen gesetzter ASCII-String ist:

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

Alle Felder sind optional. Geben Sie nur die Felder an, die für Ihre Architektur gelten:

Feld Beschreibung Beispiel
product Anwendungs- oder Handelsname des Agenten, der die Anfrage stellt. product="ResearchBot"
project Pseudonyme Konto-, Arbeitsbereichs- oder Mandantenprojekt-ID auf der Hostplattform. project="prj_89a0f"
user Pseudonymisierte Kennung (z. B. eine intransparente UUID oder ein Salted Hash) für den Kunden oder Endnutzer. Dieser Wert sollte bei Anfragen desselben Nutzers gleich bleiben, um eine Bewertung aufzubauen. Er darf keine echten personenidentifizierbaren Informationen enthalten. user="usr_29df3"
session Kurzlebige, opake Kennung für eine einzelne Aufgabe, einen Workflow-Ausführung oder eine Browsersitzung. session="sess_88b1"

Header „Agent-Context“ signieren

reCAPTCHA verwendet Agent-Context nur dann für die Reputationsisolation, wenn Sie "agent-context" in Ihre WBA-Signatur aufnehmen. Nicht signierte Agent-Context-Header werden ignoriert.

So signieren Sie den Agent-Context-Header:

  1. Legen Sie den Agent-Context-Header für Ihre ausgehende *http.Request fest, bevor Sie signRequestEd25519 aufrufen:

    req.Header.Set("Agent-Context", `product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"`)
    
  2. Fügen Sie "agent-context" in die Komponentenliste und den Signaturbasisstring von Signature-Input ein (wird automatisch vom Go-Beispiel verarbeitet):

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

Das folgende Beispiel zeigt eine vollständige HTTP-Anfrage, die mit Agent-Context signiert ist:

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:

Nächste Schritte

Wenn Sie Ihre Anfragen mit WBA signieren, beweisen Sie die kryptografische Kontrolle über die Domain Ihres Agents. Websites benötigen jedoch auch Metadaten darüber, wer Ihren Agent betreibt und welchem Zweck er dient.