署名付きリクエストは何を証明しますか?
自動化されたトラフィックの多くは user agent string で自分を名乗ります。その文字列は主張にすぎず、複製してもほとんどコストはかかりません。署名付きリクエストはこの状況を変えます。自動クライアントが秘密鍵で HTTP リクエストに署名し、そのクライアントの運用主体に結び付いた場所で一致する公開鍵を公開していれば、サーバーはそのリクエストが実際にその鍵を保持する者によって生成されたかを確認できます。
この基盤となる標準は HTTP Message Signatures で、RFC 9421 として公開されています。IETF Web Bot Auth working group は、これを自動クライアント向けの規約で拡張しています。つまり、エージェントが自分の鍵ディレクトリをどこで指し示すか、そしてどの request components を署名すべきか、という点です。2026年11月時点では、working group の protocol document は draft-ietf-webbotauth-httpsig-protocol であり、Signature-Agent ヘッダー、JSON Web Key Sets に基づく鍵ディレクトリ形式、そしてそれを配信するための well-known URI を定義しています。検証は少しの違いで誤りやすいため、どちらも細部まで理解する価値があります。
仕組みに入る前に、結果を正確に捉えてください。妥当な署名は、たとえば運用者のドメインのような公開済み識別子に関連付けられた鍵でリクエストが署名されており、かつ署名対象のリクエスト部分が改変されていないことを示します。しかし、誰がエージェントに指示したのか、人が権限を委任したのか、その行動が安全かどうかまでは示しません。アイデンティティの証拠と認可は別の問いであり、後者に答えるのはあなたのアプリケーションです。署名付き AI エージェントリクエストが何を証明するか に関するブログ記事では、その境界を例とともに解説しています。
関連するヘッダー
署名付きリクエストには通常、3つのヘッダーが含まれます。
Signature-Inputは (RFC 9421, Section 4.1) 各署名を記述します。ラベル、対象コンポーネントの一覧、そしてcreated、expires、keyid、alg、nonce、tagのようなパラメータです。Signatureは (RFC 9421, Section 4.2) 各ラベルの署名バイト列を base64 でエンコードしたものを structured field として格納します。Signature-Agentは (Web Bot Auth の規約に由来) クライアントの公開鍵がどこに公開されているかを指し示します。通常は、運用者のドメイン上の HTTPS URL です。
簡略化した例:
GET /api/catalog?page=2 HTTP/1.1
Host: shop.example
Signature-Agent: "https://agent.example"
Signature-Input: sig1=("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"
Signature: sig1=:Base64SignatureBytes==:この例は読みやすさのために簡略化しています。現在の working group draft では、Signature-Agent 自体が署名ラベルでキー付けされた structured field です (たとえば sig1="https://agent.example")。そのため、検証ライブラリがどちらの形式を期待するかを確認してください。
Step 1: structured fields を正しく解析する
Signature-Input と Signature は HTTP Structured Fields です (RFC 9651。これは RFC 8941 を廃止します)。文字列の分割で解析してはいけません。quoted strings、parameters、inner lists にはそれぞれ厳密な文法があり、雑なパーサーは誤検知による拒否と実際のセキュリティギャップの両方を生みます。
保守されている structured-field parser を使うか、できれば解析と検証をまとめて処理する保守済みの RFC 9421 ライブラリを使ってください。仕様をゼロから実装するのは学習としては有益ですが、本番運用には向きません。以下の手順は、そのようなライブラリが内部で何をしているかを示すものです。正しく設定し、出力を確認できるようにしてください。
Step 2: どの署名を評価するか選ぶ
1つのリクエストに複数の署名ラベルが含まれることがあります。あなたのポリシーに合うもの、たとえば tag が web-bot-auth であるラベルを選んでください。次のいずれかに該当する場合は、検証目的では拒否します (配信目的ではありません)。
- 必要なパラメータを持つ署名がない
- アルゴリズムが allowlist にない (たとえば
ed25519) - 対象コンポーネントに必要な最小セットが含まれていない
自動クライアントにとって妥当な最小構成は @method、@authority、@path に加え、そのヘッダーが存在する場合は signature-agent です。@authority がなければ、あるサイト向けに取得した署名を別のサイトに対して再利用できます。@path がなければ、endpoint をまたいで再利用できます。Web Bot Auth protocol draft は、エージェントに @authority または @target-uri を含めるよう求めており、authority だけを対象にした署名は、有効期限が切れるまでそのホスト上の任意の method や path に対して有効であると警告しています。
Step 3: 時刻パラメータを確認する
RFC 9421, Section 2.3 で定義されている時刻パラメータ created と expires を読み取ってください。そのうえで:
createdの時刻が、わずかな clock skew の許容範囲を超えて未来にある署名を拒否します。expiresを過ぎた署名を拒否します。expiresが欠けている場合は、createdに基づいて独自の最大経過時間を強制します。
短い有効期間は、取得済みのリクエストを再送できる時間を制限します。
Step 4: 鍵を見つける
Signature-Agent ヘッダーは、運用者の鍵ディレクトリを指します。HTTPS、そして HTTPS のみで取得してください。Web Bot Auth protocol draft では、そのディレクトリを JSON Web Key Set として説明しています。つまり、JWK 形式の鍵が http-message-signatures-directory well-known URI で提供されます。そのうえで、識別子が keyid パラメータと一致する鍵を見つけます。
type Jwk = { kid: string; kty: string; crv?: string; x?: string };
const keyCache = new Map<string, { keys: Jwk[]; fetchedAt: number }>();
const KEY_TTL_MS = 60 * 60 * 1000;
export async function getKey(directoryUrl: string, keyId: string): Promise<Jwk | null> {
const url = new URL(directoryUrl);
if (url.protocol !== "https:") return null;
const cached = keyCache.get(url.origin);
const fresh = cached && Date.now() - cached.fetchedAt < KEY_TTL_MS;
const keys = fresh ? cached.keys : await fetchKeys(url.origin);
return keys.find((k) => k.kid === keyId) ?? null;
}
async function fetchKeys(origin: string): Promise<Jwk[]> {
const res = await fetch(`${origin}/.well-known/http-message-signatures-directory`, {
headers: { accept: "application/http-message-signatures-directory+json, application/json" },
signal: AbortSignal.timeout(2000),
});
if (!res.ok) return [];
const body = (await res.json()) as { keys?: Jwk[] };
const keys = Array.isArray(body.keys) ? body.keys : [];
keyCache.set(origin, { keys, fetchedAt: Date.now() });
return keys;
}鍵の検出に関する実践的なルール:
- 鍵をキャッシュする - 有効期限を設けて、検証のたびにネットワーク呼び出しが発生しないようにします。
- 短いタイムアウトを設定する - 取得に失敗した場合は「verified」ではなく「verify できなかった」と扱います。
- 取得先の origin を制限する - 可能なら制限し、少なくとも private address ranges をブロックして server-side request forgery を防ぎます。protocol draft が security considerations で SSRF に触れているのは、まさにこの理由です。
- 識別は origin に結び付ける - user agent の自由記述名ではなく、記録する識別子はディレクトリの origin です。
Step 5: 署名ベースを再構築する
署名者は生のリクエストに署名したわけではありません。署名したのは signature base と呼ばれる正規化済みの文字列です (RFC 9421, Section 2.5)。これは、対象コンポーネントを順番に並べ、その後に @signature-params 行を付けて構成されます。検証側は、受信したリクエストから同じ文字列を再構築します。
上の例では、base は次のようになります。
"@method": GET
"@authority": shop.example
"@path": /api/catalog
"signature-agent": "https://agent.example"
"@signature-params": ("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"プロキシの背後で検証を壊す細部に注意してください。
@authorityは、クライアントが対象とした host でなければなりません。load balancer がHostを書き換える場合は、転送される元の値を使ってください。@pathは query string を除外します。クライアントが@queryを対象に含めた場合は、受信したとおりに正確に再構築してください。- Header 値は指定どおりに正規化されます。trim され、複数値は結合されます。優れたライブラリならこれを自動で行います。
Step 6: 署名を検証する
base と公開鍵があれば、検証は RFC 9421, Section 3.2 の手順に従う単一の呼び出しで完了します。Node.js で Ed25519 を使う場合は、組み込みの crypto module を利用します。
import { createPublicKey, verify } from "node:crypto";
export function verifyEd25519(jwk: Jwk, base: string, signatureB64: string): boolean {
const key = createPublicKey({ key: jwk as any, format: "jwk" });
return verify(null, Buffer.from(base, "utf8"), key, Buffer.from(signatureB64, "base64"));
}この確認に失敗した場合、リクエストは verified ではありません。それでも配信してかまいません。ただし、「verified」基準は付きません。
Step 7: リプレイを防ぐ
妥当な署名は、期限が切れるまで取得して再送できます。RFC 9421, Section 7.2.2 では signature replay を扱っており、Web Bot Auth protocol draft も独自の nonce ルールは追加せず、それに委ねています。この時間窓をほぼ閉じるには、次の2つの制御で十分です。
- 短い有効期限 - Step 3 で強制します。
- nonce の追跡 -
nonceがある場合は、署名の有効期限が切れるまでその key identifier とともに保存し、2回目の使用を拒否します。
const seen = new Map<string, number>();
export function acceptNonce(keyId: string, nonce: string, expiresAt: number): boolean {
const id = `${keyId}:${nonce}`;
const now = Date.now() / 1000;
for (const [k, exp] of seen) if (exp < now) seen.delete(k);
if (seen.has(id)) return false;
seen.set(id, expiresAt);
return true;
}複数インスタンスで構成されたデプロイでは、残り有効期間と同じ time to live を持つ Redis のような共有ストアに nonce を保存してください。
Step 8: basis とともに結果を記録する
検証は verdict ではなく evidence を生みます。それを、分析やポリシーで使える形式で記録してください。
const result = {
actor: "agent",
basis: "verified",
operator: "https://agent.example",
keyId: "kq1",
coveredComponents: ["@method", "@authority", "@path", "signature-agent"],
verifiedAt: new Date().toISOString(),
};検証に失敗した場合や実行できなかった場合も、理由とともに記録します。理由には、ヘッダー不足、未対応アルゴリズム、鍵未検出、期限切れ、再送済み、署名不一致などがあります。これらの理由は重要です。ある運用者からの "key not found" の急増は、攻撃ではなく鍵のローテーションを示していることがよくあります。
署名検証では何をログに記録すべきですか?
運用者の origin、key identifier、対象コンポーネント、時刻パラメータ、結果、失敗理由を記録してください。署名バイト列そのものや、リクエストボディ全体は記録しないでください。ログは、パターンを調査できるだけの期間は残しつつ、プライバシー上の約束に配慮できる程度に短く保ってください。
verified 署名はポリシーをどう変えるべきですか?
リクエストを検証できるようになったら、意味のある方法で差を付けられます。人と AI エージェント向けにアプリポリシーを設計する に関する記事では、より広いポリシーモデルを扱っています。
- verified な運用者には、未検証の自動化より高い rate limit を設定した read-only route へのアクセスを許可する
- full HTML page ではなく、JSON や Markdown のような構造化レスポンスを提供する
- 影響の大きい操作は通常の認可の下に置く。verified signature が示すのは、どの client が呼び出したかであって、account を変更してよいということではない
最初は observation mode から始めてください。自動トラフィックのうちどれだけが署名付きか、どの運用者が現れるか、そして検証がどの頻度で失敗するかを、いかなるポリシーもそれに依存する前に測定します。
Proof of Actor はどこに当てはまりますか?
署名検証は、複数ある evidence の1つにすぎません。Proof of Actor はこれを他の signals と組み合わせ、すべての結果で basis を見えるままに保ちます。そのため、verified な agent と、単に自称しただけの agent を取り違えることはありません。次に何を起こすかを決めるのは、あなたのアプリケーションです。
これを当社のエンジニアとともに本番アプリへ組み込むには、早期アクセスに申し込む をご利用ください。evidence で何が見えて、何が見えないのかを全体として理解するには、Detection & Limitations をお読みください。
Sources
- RFC 9421: HTTP Message Signatures: signature parameters、signature base、replay considerations を含む署名と検証の標準
- RFC 9651: Structured Field Values for HTTP:
Signature-InputとSignatureで使われる文法 - IETF Web Bot Auth working group: automated client authentication 作業の charter と status
- draft-ietf-webbotauth-httpsig-protocol:
Signature-Agent、JWKS key directory、そしてその well-known URI を定義する Internet-Draft (2026年11月時点で作業中) - RFC 7517: JSON Web Key (JWK): 公開済み鍵ディレクトリで使われる鍵形式
- RFC 8615: Well-Known URIs: 鍵ディレクトリのような well-known paths の登録方法
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA): 例で使われている Ed25519 アルゴリズム
- Node.js crypto documentation: Step 6 で使う
createPublicKeyとverify