Web Bot Auth (WBA) 通訊協定可讓您使用 HTTP 訊息簽章 (RFC 9421),以加密方式證明代理程式或機器人的身分。
簽署傳出要求後,Google Cloud Fraud Defense (reCAPTCHA 的進化版) 等服務就能直接驗證代理,不必只依賴 IP 位址或 User-Agent 字串。
如要使用 WBA 驗證代理,請完成下列工作:
- 產生 Ed25519 金鑰組,並將公開金鑰格式化為 JSON Web Key (JWK) Set。
- 在知名的 HTTPS 端點託管公開金鑰目錄。
- 使用私密金鑰簽署每個連出 HTTP 要求。
- 選用:簽署實驗性
Agent-Context標頭,將共用簽署金鑰的租戶信譽區隔開來。
產生 Ed25519 金鑰組
WBA 需要 Ed25519 (RFC 8032) 非對稱金鑰組。您會將私密金鑰保密,用於簽署傳出的 HTTP 要求,並在 JSON Web Key (JWK) Set (RFC 7517) 中發布公開金鑰。
使用 OpenSSL 和 jwker 產生金鑰組
您可以使用 OpenSSL 產生 Ed25519 金鑰組,並透過開放原始碼 jwker 工具將公開金鑰轉換為 JWK:
產生 PEM 格式的 Ed25519 私密金鑰:
openssl genpkey -algorithm ed25519 -out private-key.pem擷取 PEM 格式的公開金鑰:
openssl pkey -in private-key.pem -pubout -out public-key.pem如果尚未安裝
jwker,請按照jwker安裝操作說明進行安裝。使用
jwker將 PEM 公開金鑰轉換為 JWK 檔案:jwker public-key.pem public-key.jwk這個指令會將單一 JWK 物件寫入
public-key.jwk:{ "kty": "OKP", "crv": "Ed25519", "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs" }將 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 標頭
Signature-Agent:以引號括住的字串指定金鑰目錄的 HTTPS 來源:Signature-Agent: "https://agent.example.com"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"。
- 涵蓋元件:
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 標頭,請完成下列步驟:
在呼叫
signRequestEd25519之前,請先在傳出的*http.Request上設定Agent-Context標頭:req.Header.Set("Agent-Context", `product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"`)在
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 簽署要求,可證明代理網域的密碼編譯控制權,但網站也需要代理操作者和用途的中繼資料。
- 瞭解網站擁有者如何使用「已驗證代理」信號:
- 閱讀通訊協定規格: