HTTP Message Signatures による Bots・Agents 身元認証のアイデア
はじめに
この Blog で紹介してある HTTP Message Signatures RFC 9421 を使ったリクエスト認証の提案(for automated traffic Architecture)draft-meunier-web-bot-auth-architecture-01 を試します。
要点は以下で、正規の Bot・Agent を偽装したアクセスを抑止するなどの効果を期待します。
- HTTP レイヤーで送信元の認証をする仕組み
- Bot や Agent の開発者はこれを利用することで身元を証明することができる
- リソース提供者は身元を証明された Bot や Agent のアクセスのみ許可することができる
エージェントは、AIアシスタント、検索インデックス作成、コンテンツ集約、自動テストなど、ビジネスやユーザーのワークフローでますます使用されるようになっている。 これらのエージェントは、いくつかの理由から、オリジンに対して自分自身を確実に識別する必要がある:
1. 自動化システムの透明性を要求する規制コンプライアンス
2. オリジンのリソース管理とアクセス制御
3. なりすましからの保護とレピュテーション管理
4. 人間トラフィックと自動トラフィック間のサービスレベルの差別化
IP許可リスト、User-Agent文字列、共有APIキーなどの現在の識別方法には、セキュリティ、拡張性、管理性において大きな限界がある。 本文書は、[HTTP-MESSAGE-SIGNATURES]を使用してエージェントが暗号的に自身を識別することを可能にするアーキテクチャを定義する。 ボットからのすべてのリクエストはプロバイダが所有する秘密鍵で署名されることを提案する。 こうすることで、すべてのオリジンはサービスのアイデンティティを検証することができる。
DeepL.com(無料版)で翻訳しました。
概要
全体の構成は下記のようになります。
- User
人やシステムなどのリソース利用者 - Agent
秘密鍵で署名付き HTTP リクエストを送る HTTP クライアント - Origin
公開鍵で署名を検証してリソースを提供する HTTP サーバー
認証の部分を抜き出すとこんな感じです。
実装例
Agent と Origin の部分に関して Cloudflare では web-bot-auth を公開し、コントリビュートを募集しています。
執筆時点(2025 年 5 月)では以下があります。
- Origin
Workers および Caddy plugin - Agent
ブラウザー拡張
テスト
準備
- Origin
Cloudflare のサンプルサイトが
https://http-message-signatures-example.research.cloudflare.com/
に公開されているので、こちらを使います。 - Agent
ブラウザー拡張をいれるのが少し面倒だったので、python で試しました。
example.py
import requests
import json
import base64
import time
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives import serialization
# ローカルの鍵一覧
# https://www.rfc-editor.org/rfc/rfc9421.html#name-example-ed25519-test-key
# test-key の kid をサンプルサイト用に変更
local_key_store = [
{
"kty": "OKP",
"crv": "Ed25519",
"kid": "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U",
"d": "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU",
"x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
},
]
# ヘルパー
def b64url_decode(val):
return base64.urlsafe_b64decode(val + '=' * (-len(val) % 4))
def b64_encode_bytes(b):
return base64.b64encode(b).decode("ascii")
def jwk_to_private_key(jwk):
return Ed25519PrivateKey.from_private_bytes(b64url_decode(jwk['d']))
def jwk_to_public_key_bytes(jwk):
private_key = jwk_to_private_key(jwk)
public_key = private_key.public_key()
return public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
# .well-known で公開鍵取得
# オリジンも同じ公開鍵で検証することを期待
res = requests.get(
"https://http-message-signatures-example.research.cloudflare.com/.well-known/http-message-signatures-directory",
headers={"Accept": "application/json"}
)
res.raise_for_status()
remote_keys = res.json()
# Key ID と公開鍵にマッチするローカル鍵を探す
# https://datatracker.ietf.org/doc/html/rfc7517#section-4.5
# https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2
selected_key = None
for remote in remote_keys.get("keys", []):
for local in local_key_store:
if remote.get("kid") == local.get("kid") and remote.get("x") == local.get("x"):
selected_key = local
break
if selected_key:
break
if not selected_key:
raise ValueError("No matching key found between remote directory and local key store")
# マッチした秘密鍵で必要事項に署名
# https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1
private_key = jwk_to_private_key(selected_key)
url = "https://http-message-signatures-example.research.cloudflare.com/"
url_obj = requests.utils.urlparse(url)
authority = url_obj.netloc
signature_agent = "http-message-signatures-example.research.cloudflare.com"
now = int(time.time())
created = now
expires = now + 3600
param = f'("@authority" "signature-agent");created={created};expires={expires};keyid="{selected_key["kid"]}";tag="web-bot-auth"'
base = f"\"@authority\": {authority}\n\"signature-agent\": {signature_agent}\n\"@signature-params\": {param}"
signature_bytes = private_key.sign(base.encode("utf-8"))
signature_b64 = b64_encode_bytes(signature_bytes)
headers = {
"User-Agent": "Python/requests Bot/1.0",
"Signature-Agent": signature_agent,
"Signature-Input": f"sig={param}",
"Signature": f"sig=:{signature_b64}:"
}
# リクエスト送信
response = requests.get(url, headers=headers)
print(response.text)
結果
レスポンスに入っているメッセージで検証の結果を確認します。
ブラウザー拡張を入れていないブラウザーから単にアクセスすると
Your browser does not support HTTP Message Signatures
の表示で、署名が付与されていないリクエストだとわかります。
一方、スクリプトの方はレスポンスが変わります。
You successfully authenticated as owning the test public key
の表示で、署名が付与され検証も成功したリクエストだとわかります。
<header class="success" id="top">
<h1>Identify Bots with HTTP Message Signatures</h1>
<h3>
You successfully authenticated as owning the test public key
</h3>
</header>
署名はされていても検証が失敗すると
The Signature you sent does not validate against test public key
のメッセージになりました。
なお /debug
にアクセスすると
- url = "https://http-message-signatures-example.research.cloudflare.com/"
+ url = "https://http-message-signatures-example.research.cloudflare.com/debug"
署名を含むリクエストヘッダーを見ることができます。
:
host: http-message-signatures-example.research.cloudflare.com
signature: sig=:QRbkRt/W5RYUzj/gIjSBPIrk8cCfUKkd+AyOAVPWwfi1Flcv/T/q3d5xdB/+ZSSYPlC2Pda8+y2JrakxenrpAw==:
signature-agent: http-message-signatures-example.research.cloudflare.com
signature-input: sig=("@authority" "signature-agent");created=1747702511;expires=1747706111;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";tag="web-bot-auth"
user-agent: Python/requests Bot/1.0
:
まとめ
HTTP のレイヤーで署名と検証ができることを確認しました。
今後のフィールドでの採用がどうなるか期待です。
Discussion