Menandatangani permintaan keluar dengan Web Bot Auth

Protokol Web Bot Auth (WBA) memungkinkan Anda membuktikan identitas agen atau bot secara kriptografis menggunakan Tanda Tangan Pesan HTTP (RFC 9421).

Saat Anda menandatangani permintaan keluar, layanan seperti Google Cloud Fraud Defense (evolusi reCAPTCHA) dapat memverifikasi agen Anda secara langsung tanpa hanya mengandalkan alamat IP atau string User-Agent.

Untuk mengautentikasi agen Anda dengan WBA, selesaikan tugas berikut:

  1. Buat pasangan kunci Ed25519 dan format kunci publik sebagai Set Kunci Web JSON (JWK).
  2. Hosting direktori kunci publik Anda di endpoint HTTPS yang dikenal.
  3. Tanda tangani setiap permintaan HTTP keluar dengan kunci pribadi Anda.
  4. Opsional: Tandatangani header Agent-Context eksperimental untuk mengisolasi reputasi di seluruh tenant yang menggunakan kunci penandatanganan yang sama.

Membuat pasangan kunci Ed25519

WBA memerlukan pasangan kunci asimetris Ed25519 (RFC 8032). Anda merahasiakan kunci pribadi untuk menandatangani permintaan HTTP keluar, dan Anda memublikasikan kunci publik dalam JSON Web Key (JWK) Set (RFC 7517).

Membuat pasangan kunci dengan OpenSSL dan jwker

Anda dapat membuat pasangan kunci Ed25519 dengan OpenSSL dan mengonversi kunci publik menjadi JWK menggunakan alat jwker open source:

  1. Buat kunci pribadi Ed25519 dalam format PEM:

    openssl genpkey -algorithm ed25519 -out private-key.pem
    
  2. Ekstrak kunci publik dalam format PEM:

    openssl pkey -in private-key.pem -pubout -out public-key.pem
    
  3. Jika Anda belum menginstal jwker, instal dengan mengikuti petunjuk penginstalan jwker.

  4. Konversikan kunci publik PEM ke file JWK menggunakan jwker:

    jwker public-key.pem public-key.jwk
    

    Perintah ini menulis satu objek JWK ke public-key.jwk:

    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs"
    }
    
  5. Gabungkan objek JWK di dalam array keys Set JWK (jwks.json) dan tambahkan kolom alg, use, dan kid yang diperlukan:

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

Referensi kolom Set JWK Publik

Setiap objek kunci publik dalam array keys JWK Set Anda mendukung kolom berikut:

Kolom Nilai Deskripsi
kty "OKP" Jenis kunci. Harus berupa "OKP" (Octet Key Pair) seperti yang ditentukan dalam RFC 8037.
crv "Ed25519" Kurva kriptografis. Harus berupa "Ed25519".
alg "EdDSA" Algoritma yang dimaksudkan untuk digunakan dengan kunci. Harus berupa "EdDSA".
use "sig" Tujuan penggunaan kunci publik. Tetapkan ke "sig" (tanda tangan).
x "BASE64URL_PUBLIC_KEY" Kunci publik Ed25519 mentah 32 byte, yang dienkode menggunakan base64url tanpa padding (RFC 4648 Bagian 5).
kid "KEY_ID" ID kunci unik yang cocok dengan parameter keyid dalam permintaan bertanda tangan Anda. Gunakan thumbprint JWK SHA-256 RFC 7638 (base64url tanpa padding) untuk kompatibilitas lintas vendor.
nbf UNIX_SECONDS Opsional. Stempel waktu Unix dalam detik yang menunjukkan waktu sebelum kunci tidak valid.
exp UNIX_SECONDS Opsional. Stempel waktu Unix dalam detik setelah masa berlaku kunci berakhir. Pemverifikasi menolak kunci yang sudah tidak berlaku.

Menghosting direktori kunci publik Anda

Pemverifikasi mengambil kunci publik Anda dari endpoint HTTPS yang dikenal di domain agen Anda.

Persyaratan endpoint dan header

Untuk menghosting direktori kunci, konfigurasikan server web Anda agar memenuhi persyaratan berikut:

  • Jalur URL: Tayangkan JSON Set JWK di https://YOUR_AGENT_DOMAIN/.well-known/http-message-signatures-directory.
  • Keamanan transport: Tayangkan endpoint melalui HTTPS di port default 443. Jangan mengalihkan permintaan ke host atau skema yang berbeda.
  • Jenis konten: Tetapkan header respons Content-Type ke application/http-message-signatures-directory+json (atau application/json).
  • Caching: Tetapkan header respons Cache-Control dengan perintah max-age (misalnya, Cache-Control: public, max-age=3600).
  • Rotasi kunci: Sebelum Anda menandatangani permintaan dengan kunci pribadi baru, tambahkan JWK publiknya ke array keys dan tunggu hingga TTL Cache-Control Anda berakhir. Hapus kunci yang dicabut atau tidak berlaku dari array keys.

Contoh berikut menunjukkan respons HTTP dari 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"
    }
  ]
}

Menandatangani permintaan HTTP keluar

Untuk mengautentikasi permintaan HTTPS keluar dengan WBA, lampirkan tiga header yang diperlukan: Signature-Agent, Signature-Input, dan Signature.

Header HTTP

  1. Signature-Agent: Menentukan origin HTTPS direktori kunci Anda sebagai string yang dikutip:

    Signature-Agent: "https://agent.example.com"
    
  2. Signature-Input: Memetakan label tanda tangan (seperti sig1) ke daftar dalam komponen HTTP yang tercakup dan parameter metadata tanda tangan:

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

    Sertakan komponen dan parameter yang tercakup berikut:

    • Komponen yang tercakup:
      • "@authority": Wajib diisi. Mengikat tanda tangan ke host tujuan (misalnya, www.example.com).
      • "signature-agent": Wajib diisi. Mengikat tanda tangan ke header Signature-Agent Anda.
    • Parameter metadata:
      • created: Stempel waktu Unix saat ini dalam detik (bilangan bulat).
      • expires: Stempel waktu Unix masa berlaku dalam detik (bilangan bulat). Tetapkan expires ke maksimal 300 detik (5 menit) setelah created.
      • keyid: String kid kunci publik Anda dari JWK Set Anda.
      • tag: Harus "web-bot-auth".
  3. Signature: Memetakan label tanda tangan yang sama (sig1) ke tanda tangan Ed25519 berenkode base64 yang diformat sebagai urutan byte RFC 8941 (digabungkan dalam titik dua :):

    Signature: sig1=:BASE64_ENCODED_SIGNATURE:
    

Membuat dasar tanda tangan

Sebelum menandatangani, buat string dasar tanda tangan kanonis yang ditentukan dalam RFC 9421 Bagian 2.5. Setiap komponen yang tercakup muncul di barisnya sendiri dalam huruf kecil, diikuti dengan baris @signature-params tanpa newline di akhir:

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

Tanda tangani byte UTF-8 dari string dasar tanda tangan ini menggunakan kunci pribadi Ed25519 Anda.

Contoh Go

Contoh berikut memuat file private-key.pem yang dihasilkan oleh OpenSSL dan menandatangani *http.Request keluar menggunakan paket crypto/ed25519 standar Go dan 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
}

Menyertakan header Agent-Context eksperimental

Platform multi-tenant (seperti penyedia browser yang dihosting atau runtime agen cloud) sering kali menandatangani permintaan keluar dari beberapa agen atau tenant pelanggan menggunakan satu kunci penandatanganan bersama. Secara default, WBA hanya mengidentifikasi domain penandatanganan (Signature-Agent), sehingga semua traffic yang ditandatangani oleh kunci tersebut memiliki reputasi yang sama.

Saat Anda menyertakan dan menandatangani header Agent-Context, reCAPTCHA dapat mengevaluasi reputasi secara terpisah di berbagai produk, project, pengguna, atau sesi yang menggunakan kunci penandatanganan yang sama di platform Anda.

Memformat header Agent-Context

Format Agent-Context sebagai RFC 8941 Structured Field Dictionary dengan setiap kunci berupa token huruf kecil dan setiap nilai berupa string ASCII yang dikutip:

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

Semua kolom bersifat opsional. Sertakan hanya kolom yang berlaku untuk arsitektur Anda:

Kolom Deskripsi Contoh
product Nama aplikasi atau nama komersial agen yang membuat permintaan. product="ResearchBot"
project ID akun, ruang kerja, atau project tenant dengan nama samaran di platform host. project="prj_89a0f"
user ID pseudonim (seperti UUID buram atau hash dengan salt) untuk pelanggan atau pengguna akhir. Jaga agar nilai ini tetap stabil di seluruh permintaan dari pengguna yang sama untuk membangun reputasi, dan jangan pernah menyertakan PII yang sebenarnya. user="usr_29df3"
session ID sementara dan buram untuk satu tugas, eksekusi alur kerja, atau sesi penjelajahan. session="sess_88b1"

Menandatangani header Agent-Context

reCAPTCHA hanya menggunakan Agent-Context untuk isolasi reputasi saat Anda menyertakan "agent-context" dalam tanda tangan WBA Anda. Header Agent-Context yang tidak ditandatangani akan diabaikan.

Untuk menandatangani header Agent-Context, selesaikan langkah-langkah berikut:

  1. Tetapkan header Agent-Context pada *http.Request keluar Anda sebelum memanggil signRequestEd25519:

    req.Header.Set("Agent-Context", `product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"`)
    
  2. Sertakan "agent-context" dalam daftar komponen Signature-Input dan string dasar tanda tangan (ditangani secara otomatis oleh contoh 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"
    

Contoh berikut menunjukkan permintaan HTTP lengkap yang ditandatangani dengan 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:

Langkah berikutnya

Menandatangani permintaan Anda dengan WBA membuktikan kontrol kriptografis atas domain agen Anda, tetapi situs juga memerlukan metadata tentang siapa yang mengoperasikan agen Anda dan apa tujuannya.