Web Bot Auth でアウトバウンド リクエストに署名する

Web Bot Auth(WBA)プロトコルを使用すると、HTTP メッセージ署名(RFC 9421)を使用して、エージェントまたはボットの ID を暗号的に証明できます。

送信リクエストに署名すると、Google Cloud Fraud Defense(reCAPTCHA の進化版)などのサービスは、IP アドレスや User-Agent 文字列のみに依存することなく、エージェントを直接検証できます。

WBA でエージェントを認証するには、次のタスクを完了します。

  1. Ed25519 鍵ペアを生成し、公開鍵を JSON Web Key(JWK)セットとしてフォーマットします。
  2. 既知の HTTPS エンドポイントで公開鍵ディレクトリをホストします。
  3. 秘密鍵を使用して各アウトバウンド HTTP リクエストに署名します。
  4. 省略可: 試験運用版の Agent-Context ヘッダーに署名して、署名鍵を共有するテナント間でレピュテーションを分離します。

Ed25519 鍵ペアを生成する

WBA には、Ed25519(RFC 8032)非対称鍵ペアが必要です。秘密鍵は、送信 HTTP リクエストに署名するために秘密にしておきます。公開鍵は、JSON Web Key(JWK)セット(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 セットの 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 セットの keys 配列内の各公開鍵オブジェクトは、次のフィールドをサポートしています。

フィールド 値 説明
kty "OKP" 鍵のタイプ。RFC 8037 で定義されている "OKP"(オクテット鍵ペア)にする必要があります。
crv "Ed25519" 暗号曲線。"Ed25519" を指定します。
alg "EdDSA" 鍵で使用するアルゴリズム。"EdDSA" を指定します。
use "sig" 公開鍵の使用目的。"sig"(署名)に設定します。
x "BASE64URL_PUBLIC_KEY" 32 バイトの未加工の Ed25519 公開鍵。パディングなしの base64url(RFC 4648 セクション 5)を使用してエンコードされます。
kid "KEY_ID" 署名付きリクエストの keyid パラメータと一致する一意の鍵識別子。クロスベンダー互換性を確保するには、RFC 7638 SHA-256 JWK サムプリント(パディングなしの base64url)を使用します。
nbf UNIX_SECONDS 省略可。鍵が無効になる前の Unix タイムスタンプ(秒単位)。
exp UNIX_SECONDS 省略可。鍵の有効期限が切れるまでの Unix タイムスタンプ(秒単位)。検証者は期限切れの鍵を拒否します。

公開鍵ディレクトリをホストする

検証ツールは、エージェントのドメインにある既知の HTTPS エンドポイントから公開鍵を取得します。

エンドポイントとヘッダーの要件

鍵ディレクトリをホストするには、次の要件を満たすようにウェブサーバーを構成します。

  • URL パス: https://YOUR_AGENT_DOMAIN/.well-known/http-message-signatures-directory で JWK セット JSON を提供します。
  • トランスポート セキュリティ: デフォルト ポート 443 で HTTPS 経由でエンドポイントを処理します。リクエストを別のホストまたはスキームにリダイレクトしないでください。
  • コンテンツ タイプ: Content-Type レスポンス ヘッダーを application/http-message-signatures-directory+json(または application/json)に設定します。
  • キャッシュ保存: max-age ディレクティブ(Cache-Control: public, max-age=3600 など)を使用して Cache-Control レスポンス ヘッダーを設定します。
  • 鍵のローテーション: 新しい秘密鍵で署名付きリクエストに署名する前に、公開 JWK を keys 配列に追加し、Cache-Control TTL が期限切れになるまで待ちます。取り消された鍵または期限切れの鍵を 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 の 3 つの必須ヘッダーを添付します。

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)を、RFC 8941 バイト シーケンス(: コロンでラップ)としてフォーマットされた base64 エンコードの Ed25519 署名にマッピングします。

    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 ホスト プラットフォーム上の仮名化されたアカウント、ワークスペース、またはテナント プロジェクトの識別子。 project="prj_89a0f"
user お客様またはエンドユーザーの仮名化 ID(不透明な UUID やソルトハッシュなど)。評価を構築するために、同じユーザーからのリクエスト間でこの値を安定させ、実際の PII は含めないでください。 user="usr_29df3"
session 単一のタスク、ワークフロー実行、またはブラウジング セッションの一時的な不透明な識別子。 session="sess_88b1"

Agent-Context ヘッダーに署名する

reCAPTCHA は、WBA 署名に "agent-context" が含まれている場合にのみ、レピュテーションの分離に 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 でリクエストに署名すると、エージェントのドメインの暗号化制御が証明されますが、ウェブサイトには、エージェントを運営しているユーザーとその目的に関するメタデータも必要です。