使用 Web Bot Auth 簽署傳出要求

Web Bot Auth (WBA) 通訊協定可讓您使用 HTTP 訊息簽章 (RFC 9421),以加密方式證明代理程式或機器人的身分。

簽署傳出要求後,Google Cloud Fraud Defense (reCAPTCHA 的進化版) 等服務就能直接驗證代理,不必只依賴 IP 位址或 User-Agent 字串。

如要使用 WBA 驗證代理,請完成下列工作:

  1. 產生 Ed25519 金鑰組,並將公開金鑰格式化為 JSON Web Key (JWK) Set。
  2. 在知名的 HTTPS 端點託管公開金鑰目錄。
  3. 使用私密金鑰簽署每個連出 HTTP 要求。
  4. 選用:簽署實驗性 Agent-Context 標頭,將共用簽署金鑰的租戶信譽區隔開來。

產生 Ed25519 金鑰組

WBA 需要 Ed25519 (RFC 8032) 非對稱金鑰組。您會將私密金鑰保密,用於簽署傳出的 HTTP 要求,並在 JSON Web Key (JWK) Set (RFC 7517) 中發布公開金鑰。

使用 OpenSSL 和 jwker 產生金鑰組

您可以使用 OpenSSL 產生 Ed25519 金鑰組,並透過開放原始碼 jwker 工具將公開金鑰轉換為 JWK:

  1. 產生 PEM 格式的 Ed25519 私密金鑰:

    openssl genpkey -algorithm ed25519 -out private-key.pem
    
  2. 擷取 PEM 格式的公開金鑰:

    openssl pkey -in private-key.pem -pubout -out public-key.pem
    
  3. 如果尚未安裝 jwker,請按照 jwker 安裝操作說明進行安裝。

  4. 使用 jwker 將 PEM 公開金鑰轉換為 JWK 檔案:

    jwker public-key.pem public-key.jwk
    

    這個指令會將單一 JWK 物件寫入 public-key.jwk:

    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs"
    }
    
  5. 將 JWK 物件包裝在 JWK Set keys 陣列 (jwks.json) 中,並新增必要的 alg、use 和 kid 欄位:

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

公開 JWK 集欄位參考資料

JWK Set keys 陣列中的每個公開金鑰物件都支援下列欄位:

欄位 值 說明
kty "OKP" 金鑰類型。必須是 "OKP" (八位元金鑰組),如 RFC 8037 所定義。
crv "Ed25519" 加密編譯曲線。必須為 "Ed25519"。
alg "EdDSA" 金鑰適用的演算法。必須為 "EdDSA"。
use "sig" 公開金鑰的預定用途。設為 "sig" (簽名)。
x "BASE64URL_PUBLIC_KEY" 32 位元組原始 Ed25519 公開金鑰,使用未填補的 base64url 編碼 (RFC 4648 第 5 節)。
kid "KEY_ID" 與已簽署要求中 keyid 參數相符的不重複金鑰 ID。請使用 RFC 7638 SHA-256 JWK 指紋 (未填充的 base64url),確保跨供應商相容性。
nbf UNIX_SECONDS 選用。Unix 時間戳記 (以秒為單位),代表金鑰生效前的時間。
exp UNIX_SECONDS 選用。金鑰到期時間,以 Unix 時間戳記 (秒) 表示。驗證者會拒絕過期的金鑰。

託管公開金鑰目錄

驗證者會從代理網域上知名的 HTTPS 端點擷取公開金鑰。

端點和標頭規定

如要託管金鑰目錄,請設定網路伺服器,使其符合下列規定:

  • 網址路徑:在 https://YOUR_AGENT_DOMAIN/.well-known/http-message-signatures-directory 提供 JWK Set JSON。
  • 傳輸安全性:透過 HTTPS 在預設通訊埠 443 上提供端點。請勿將要求重新導向至其他主機或配置。
  • 內容類型:將 Content-Type 回應標頭設為 application/http-message-signatures-directory+json (或 application/json)。
  • 快取:使用 max-age 指令 (例如 Cache-Control: public, max-age=3600) 設定 Cache-Control 回應標頭。
  • 金鑰輪替:使用新的私密金鑰簽署要求前,請將其公開 JWK 新增至 keys 陣列,並等待 Cache-Control 存留時間到期。從 keys 陣列中移除已撤銷或過期的金鑰。

以下範例顯示 GET /.well-known/http-message-signatures-directory 的 HTTP 回應:

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"
    }
  ]
}

簽署傳出 HTTP 要求

如要使用 WBA 驗證傳出 HTTPS 要求,請附加三個必要標頭:Signature-Agent、Signature-Input 和 Signature。

HTTP 標頭

  1. Signature-Agent:以引號括住的字串指定金鑰目錄的 HTTPS 來源:

    Signature-Agent: "https://agent.example.com"
    
  2. Signature-Input:將簽章標籤 (例如 sig1) 對應至涵蓋的 HTTP 元件和簽章中繼資料參數的內部清單:

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

    請加入下列涵蓋的元件和參數:

    • 涵蓋元件:
      • "@authority":必填,將簽章繫結至目的地主機 (例如 www.example.com)。
      • "signature-agent":必填,將簽名繫結至 Signature-Agent 標頭。
    • 中繼資料參數:
      • created:目前的 Unix 時間戳記,以秒為單位 (整數)。
      • expires:到期 Unix 時間戳記,以秒為單位 (整數)。將 expires 設為 created 後最多 300 秒 (5 分鐘)。
      • keyid:JWK 集中的公開金鑰 kid 字串。
      • tag:必須是 "web-bot-auth"。
  3. Signature:將相同的簽章標籤 (sig1) 對應至 Base64 編碼的 Ed25519 簽章,格式為 RFC 8941 位元組序列 (以 : 冒號包覆):

    Signature: sig1=:BASE64_ENCODED_SIGNATURE:
    

建構簽章基礎

簽署前,請先建構 RFC 9421 第 2.5 節定義的標準簽章基礎字串。每個涵蓋的元件都會以小寫形式顯示在各自的行中,後面接著 @signature-params 行,不含結尾換行:

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

使用 Ed25519 私密金鑰簽署這個簽章基礎字串的 UTF-8 位元組。

Go 範例

以下範例會載入 OpenSSL 產生的 private-key.pem 檔案,並使用 Go 的標準 crypto/ed25519 套件和 github.com/dunglas/httpsfv 簽署傳出的 *http.Request:

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
}

加入實驗性 Agent-Context 標頭

多租戶平台 (例如託管瀏覽器供應商或雲端代理執行階段) 通常會使用單一共用簽署金鑰,簽署多個客戶代理或租戶的外送要求。根據預設,WBA 只會識別簽署網域 (Signature-Agent),因此由該金鑰簽署的所有流量都會共用相同的信譽。

加入並簽署 Agent-Context 標頭後,reCAPTCHA 就能針對在平台上共用簽署金鑰的不同產品、專案、使用者或工作階段,分別評估信譽。

設定 Agent-Context 標頭格式

將 Agent-Context 格式化為 RFC 8941 結構化欄位字典,其中每個鍵都是小寫權杖,每個值都是以引號括住的 ASCII 字串:

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

所有欄位均為選填。請只納入適用於您架構的欄位:

欄位 說明 範例
product 提出要求的代理商應用程式或商業名稱。 product="ResearchBot"
project 主機平台上的匿名帳戶、工作區或租戶專案 ID。 project="prj_89a0f"
user 客戶或使用者的匿名 ID (例如不透明 UUID 或加鹽雜湊)。請確保同一位使用者發出的要求都使用相同的值,以建立信譽,且絕不包含實際的 PII。 user="usr_29df3"
session 單一工作、工作流程執行作業或瀏覽工作階段的暫時性不透明 ID。 session="sess_88b1"

簽署 Agent-Context 標頭

只有在 WBA 簽章中加入 "agent-context" 時,reCAPTCHA 才會使用 Agent-Context 隔離信譽。系統會忽略未簽署的 Agent-Context 標頭。

如要簽署 Agent-Context 標頭,請完成下列步驟:

  1. 在呼叫 signRequestEd25519 之前,請先在傳出的 *http.Request 上設定 Agent-Context 標頭:

    req.Header.Set("Agent-Context", `product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"`)
    
  2. 在 Signature-Input 元件清單和簽章基礎字串中加入 "agent-context" (由 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"
    

以下範例顯示使用 Agent-Context 簽署的完整 HTTP 要求:

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:

後續步驟

使用 WBA 簽署要求,可證明代理網域的密碼編譯控制權,但網站也需要代理操作者和用途的中繼資料。