> ## Documentation Index
> Fetch the complete documentation index at: https://adcp-docs-ja.pier1.co.jp/llms.txt
> Use this file to discover all available pages before exploring further.

# セキュリティ

> AdCP セキュリティガイド: 金融オペレーションのリスク分類、Webhook HMAC 検証、リプレイ防止、アクセス制御、本番デプロイの認証情報管理。

<Warning>
  **本番利用で重要**

  AdCP は金銭的なコミットメントと機微なキャンペーンデータを扱う可能性があります。実際の広告予算を管理する実装は、本書に概説するセキュリティ対策を実装しなければなりません。
</Warning>

<Note>
  ***なぜ*を探していますか？** このページは規範的な実装リファレンス — コンフォーマントなエージェントが従うルールです。脅威モデル、層状防御の物語、ブランド IT と CISO 向けのチェックリストは、[Security Model](/docs/building/concepts/security-model) を参照してください。
</Note>

## 概要

AdCP は次のような高リスク環境で動作します:

* **金銭取引**: 実際の広告費が動く
* **複数主体の信頼**: 認証済みエージェント、パブリッシャー、オーケストレーター間の連携が必要
* **機微なデータ**: 1P シグナル、未公開クリエイティブ、競合ターゲティング戦略を含む
* **非同期オペレーション**: 複数のシステムとプロトコルにまたがる

## リスク分類

### 高リスクオペレーション（金融）

これらのオペレーションは実際の広告予算をコミットします:

| Operation          | Risk                                     | Primary Threat                 |
| ------------------ | ---------------------------------------- | ------------------------------ |
| `create_media_buy` | Creates financial commitments            | Budget fraud, credential theft |
| `update_media_buy` | Modifies budgets and campaign parameters | Unauthorized modifications     |

**要件:**

* 短命な認証情報 — 漏洩したトークンの影響範囲に見合ったサイズにする。支出をコミットできるトークンには 1 時間以内が妥当なデフォルト。相当な閾値を超える支出をコミットできる、または組織境界をまたぐトークンには 15 分以内が適切。最小の数字をデフォルトにするのではなく、選択したウィンドウを文書化して正当化する。
* トランザクション整合性のためのリクエスト署名
* 大規模予算向けの多要素認証または承認ワークフロー
* 改ざん不可能なログによる完全な監査証跡

### 中リスクオペレーション（データアクセス）

これらのオペレーションは機微なビジネスデータにアクセスします:

| Operation                | Risk                                           |
| ------------------------ | ---------------------------------------------- |
| `get_media_buy_delivery` | Exposes performance metrics and spend data     |
| `list_creatives`         | Access to creative assets                      |
| `sync_creatives`         | Uploads potentially sensitive creative content |

### 低リスクオペレーション（ディスカバリー）

これらのオペレーションは公開アクセス可能です:

| Operation               | Risk                       |
| ----------------------- | -------------------------- |
| `get_adcp_capabilities` | Agent capability discovery |
| `get_products`          | Public inventory discovery |
| `list_creative_formats` | Public format catalog      |

## Webhook セキュリティ

AdCP 3.0 は Webhook 署名を [AdCP RFC 9421 プロファイル](#webhook-callbacks)に統一します — セラーはオペレーターの `brand.json` の `agents[].jwks_uri` を通じて公開した鍵でアウトバウンド Webhook に署名し、バイヤーはその JWKS に対して検証します。パブリッシャーの `adagents.json` がそのセラーに `signing_keys[]` をピン留めする場合、そのピンが権威的です。秘密はワイヤーを渡らず、アイデンティティはインバウンドリクエストと同じ方法で暗号学的に確立されます。

**9421 Webhook 署名は 3.0 でベースライン必須です。** Webhook を発行するセラーは、バイヤーが `push_notification_config.authentication` または `accounts[].notification_configs[].authentication` を設定して下記のレガシースキームに明示的にオプトインしない限り、[Webhook callbacks](#webhook-callbacks) プロファイルに従って署名しなければなりません（MUST）。

### レガシー HMAC-SHA256 フォールバック（非推奨、4.0 で削除）

9421 プロファイルをまだ採用していないレシーバーと相互運用する必要のあるバイヤーは、`push_notification_config.authentication.credentials` または `accounts[].notification_configs[].authentication.credentials` を設定してオプトインしてもよい（MAY）。バイヤーのリクエストに `authentication` が存在する場合、セラーは [Push Notifications](/docs/building/by-layer/L3/webhooks#legacy-hmac-sha256-fallback) で定義されたセマンティクスを使って HMAC-SHA256 で署名します。レガシースキームは 3.x 専用の互換性の便宜です。セラーはサポートを断ってもよく（MAY）、AdCP 4.0 で削除されます。

セラーがサポートを選択した場合のレガシースキームの規範ルール:

* **アルゴリズム**: HMAC-SHA256 のみ
* **署名メッセージ**: `{unix_timestamp}.{raw_http_body_bytes}` — JSON を決して再シリアライズしない
* **バイト等価性の不変条件**: HMAC は、パースされた JSON 値ではなく生のバイト上で計算されます。署名者と検証者はワイヤー上のバイトを直接比較しなければなりません（MUST）。ペイロードを再パース・再シリアライズすると — ライブラリが一致しコンパクトセパレーターを使っていても — 署名されたバイトを再現する保証はありません。キー順序、ユニコードエスケープポリシー、数値表現がシリアライザー間で発散するためです（具体例は下記「正準化されない側面」を参照）。このスキームは正準的な JSON 形式を定義しません。下記の「正準的なワイヤー形式」と「検証者の入力」ルールは、署名者側と検証者側でそれぞれ最も一般的なバイトドリフトの失敗を狭めますが、バイトレベルの発散を排除しません。
* **正準的なワイヤー形式**: `{raw_http_body_bytes}` は、署名者が HTTP ボディとしてワイヤーに載せるバイトとバイト単位で同一でなければなりません（MUST）。署名者が JSON 値をシリアライズしてボディを構築する場合、JSON のコンパクトセパレーター `","`（項目セパレーター）と `":"`（キーセパレーター）を使わなければなりません（MUST）— トークン間に空白なし。言語レベルのシリアライザー JavaScript `JSON.stringify`、Go `encoding/json` `json.Marshal`、Ruby `JSON.generate`、Java Jackson `writeValueAsString` はデフォルトでコンパクト出力を生成します。それらをラップする HTTP クライアント（axios、`json.Marshal` されたボディを持つ Go `net/http`、`JSON.generate` を持つ Ruby `Net::HTTP`、Jackson を持つ Java OkHttp）はそのデフォルトを継承します。Python では `httpx` はコンパクトセパレーターでシリアライズしますが、stdlib `json.dumps` はデフォルトで `", "` / `": "` になり、`separators` kwarg なしでペイロードを `json.dumps` に渡す HTTP クライアント（`requests(json=...)`、`aiohttp`）は空白入りのボディを発行します — それらのパスの署名者は `separators=(",", ":")` を明示的に渡さなければなりません（MUST）。この列挙は網羅的ではありません。署名者はこのリストに頼るのではなく、HTTP クライアントの実際のワイヤー上のシリアライズを検証しなければなりません（MUST、例: プロキシやフックでリクエストボディをキャプチャ）。署名は、署名者がシリアライズしたオブジェクトではなく、レシーバーが見るバイトをカバーします。
* **正準化されない側面**: キー順序、ユニコードエスケープポリシー、数値表現はこのスキームで正準化されません。特に数値については言語デフォルトが発散し（`JSON.stringify(1.0)` → `1`、Python `json.dumps(1.0)` → `1.0`、Go `json.Marshal(1.0)` → `1`。`0.1` のような浮動小数点や科学記法も同様の崖に当たる）、あるライブラリでシリアライズして送信前に別のライブラリで再パース・再シリアライズする署名者は、コンパクトセパレーターでも署名者-検証者ドリフトを生じさせ得ます — 上記のバイト等価性の不変条件が、このスキームを成立させる唯一のものです。
* **重複オブジェクトキー**: 署名者は重複オブジェクトキーを発行してはならず（MUST NOT）、シリアライズ前に上流呼び出し元からの重複キー入力を拒否しなければなりません（MUST）。署名者側の MUST は要となります。この失敗モードを捕捉できる唯一の場所だからです: 重複キーペイロードを黙って畳み込む署名者は、呼び出し元の意図と異なるセマンティクスを持つ暗号学的にクリーンな署名済みフレームを発行し、検証者はワイヤーから上流の発散を検出できません — 署名されたバイトは正常に見えます。署名者側のコンフォーマンスはワイヤー上で検証不能で、ランタイム検出ではなく帯域外の監査 / 相互運用テストで強制されることが期待されます（この形状は署名仕様では日常的で、COSE と JOSE は同じパターンを使います）。検証者は、HMAC 検証が成功した後、重複オブジェクトキーを含むボディを拒否しなければならず（MUST）、構造化された不正ボディエラー（署名不一致エラーとは別 — 署名は有効。ボディが不正）を返します。RFC 8259 §4 に従い、JSON オブジェクト内の名前は「一意であるべき（SHOULD）」であり、非一意の名前を持つオブジェクトを受け取るソフトウェアの動作は予測不能です — したがって同じ HMAC 有効バイトをパースする 2 つの検証者は、パースされた値について一致しないことがあります。これはパーサー差分攻撃クラスです（cf. CVE-2017-12635。ある CouchDB パーサーが同じ署名済みボディから `roles=[]` を読み、別のパーサーが `roles=["_admin"]` を読んだ）。レガシー HMAC Webhook スキームで運ばれるすべてのボディは状態変更通知（クリエイティブステータス、メディアバイステータス、ガバナンス遷移）なので、MUST はこのスキームに無条件で適用されます。検出は重複キーを露出するパーサーを使わなければなりません（MUST）— それらを黙って破棄する last-wins/first-wins のデフォルトはこの要件を満たしません。署名者入力検証と検証者ボディチェックの両方についての言語ごとの strict-parse エスケープハッチ: 正準の非網羅的列挙（デフォルトで strict に*見える*だけでデータキー重複を黙って畳み込むライブラリを含む）は [Webhook 検証者チェックリストのステップ 14](#webhook-callbacks)を参照してください。検証者側のコンフォーマンスフィクスチャは `static/test-vectors/webhook-hmac-sha256.json` の `duplicate-keys-conflicting-values`（`expected_verifier_action: "reject-malformed"`）です。署名者側のコンフォーマンスフィクスチャは同じファイルの `signer_side.rejection_vectors` にあります: `signer-upstream-duplicate-key-rejection`（トップレベル）、`signer-upstream-duplicate-key-deep-nested`（署名者のチェックがトップレベルキーだけでなくネストされたオブジェクトに再帰することを検証）、`signer-upstream-duplicate-key-array-contained`（署名者のチェックが配列内のオブジェクトに降りることを検証 — オブジェクトには再帰するが配列メンバーには再帰しない手書きバリデーターの盲点）、`signer-upstream-duplicate-key-three-deep`（ウォーカーが浅い固定深度で止まらないことを検証）。正例フィクスチャ `signer-upstream-clean-input` が `signer_side.positive_vectors` にあり、すべてを拒否する署名者が負例フィクスチャを些細にパスしないようにします — 相互運用ハーネスは、重複キー入力の拒否とクリーン入力の受け入れの両方をアサートしなければなりません（MUST）。上流入力の拒否をログやエラーレスポンスで表面化する署名者は、[Webhook 検証者チェックリストのステップ 14b](#webhook-callbacks)で定義された同じキー名サニタイズルール（最初の非印字文字で `<sanitized:N>` に切り詰め、32 バイト以下の最後の UTF-8 コードポイントに切り詰め、数を 4 で上限）を適用しなければなりません（MUST）— 署名者側のチャネルは検証者側のチャネルと同じ攻撃者制御バイト形状を持ち、信頼の方向が逆になっているだけです。**エラー識別子は規範的、エラーオブジェクトの内部は非規範的。** 署名者がエラーで拒否を表面化する場合、エラー識別子（判別ユニオンのエラーコード文字列、型付き throw イディオムの例外クラス名、直和型のタグ）は正確に `duplicate_key_input` でなければなりません（MUST、大文字小文字を区別、接頭辞・接尾辞なし）— マルチ SDK 統合が `if (error.code === 'duplicate_key_input') { ... }` を書き、どの SDK がフレームに署名したかに関わらずディスパッチが機能するように。エラーキャリアの内部形状（サニタイズされたキーリストのフィールド名、オーバーフローマーカー文字列、型付き例外コンストラクター引数）は実装依存です。クラッシュ / フェイルクローズする検証者はコンフォーマントだが最適でない（リクエストは黙って受け入れられないが、送信者は actionable なエラーコードを受け取らない）。検証者は代わりに構造化された不正ボディエラーを返すべきです（SHOULD）。非コンフォーマントな失敗モード — 署名検証者のパースがダウンストリームのビジネスロジックのパースと発散する黙った受け入れ — は現在禁止されています。ペイロードをビジネスロジックに渡す前に重複キーを検出しない検証者はこのスキームに準拠しません。
* **検証者の入力**: 検証者は、いかなる JSON パースや再シリアライズの前にキャプチャした、ワイヤー上で受信した生の HTTP ボディバイトを使わなければなりません（MUST）。すべての現代的な HTTP フレームワークはパース前の生ボディフックを公開します（Express `express.raw()`、FastAPI `Request.body()`、aiohttp `Request.read()`、`json.Unmarshal` 前の Go `io.ReadAll(r.Body)`）。生キャプチャフックは同じルート上のいかなる JSON パースミドルウェアの前に実行しなければなりません（MUST）。検証者が実行される前にリクエストボディを消費するグローバルにマウントされた `express.json()` または FastAPI `BaseModel` ボディバインディングは、署名されたバイトではなく再文字列化されたペイロード上で検証者を動作させます — これは一般的なデプロイミスです。検証者はパースされたペイロードを再シリアライズして署名されたバイトを再構築すべきではありません（SHOULD NOT）: 再シリアライズは、キー順序・ユニコードエスケープ・数値フォーマットが異なる署名者に対して黙って失敗し、検証者が表面化すべき署名者のバグを隠します。生バイトを本当にキャプチャできない検証者は、再シリアライズされた近似を受け入れるのではなく、フェイルクローズしてインフラのギャップを表面化しなければなりません（MUST）。
* **タイムスタンプソース**: 署名メッセージ内の `{unix_timestamp}` は、`X-ADCP-Timestamp` ヘッダーで送られた正確な ASCII 整数でなければなりません（MUST）。署名者と検証者はいかなるボディフィールドからもそれを導出してはなりません（MUST NOT）。
* **タイミングセーフ比較**: 定数時間比較を使わなければなりません（MUST、例: `timingSafeEqual`）
* **リプレイウィンドウ**: `|current_time - timestamp| > 300` 秒のリクエストを拒否
* **最小シークレット長**: 32 バイト
* **ヘッダー形式**: `X-ADCP-Signature: sha256=<hex digest>` と `X-ADCP-Timestamp: <unix seconds>`。ボディレベルの `signature` フィールドは便宜的なコピーであり、ヘッダーより信頼してはなりません（MUST NOT）。

**検証順序**（レガシースキーム）:

1. `X-ADCP-Signature` または `X-ADCP-Timestamp` ヘッダーが欠落していれば拒否
2. タイムスタンプが非数値なら拒否
3. タイムスタンプが 5 分ウィンドウ外なら拒否
4. HMAC を計算して比較

**シークレットローテーション**（レガシースキーム）:

* レシーバーはローテーション中、現在と以前の両方のシークレットからの署名を受け入れなければなりません（MUST）
* ローテーションウィンドウはリプレイウィンドウ（5 分）を超えるべきではありません（SHOULD NOT）
* パブリッシャーはローテーション時に即座に新しいシークレットで署名を開始します

### Webhook URL 検証（SSRF）

バイヤー、セラー、またはガバナンスエージェントが他者にフェッチさせるために提供する任意の URL は SSRF ベクトルです。これには `push_notification_config.url`、`accounts[].notification_configs[].url`、`accounts[].governance_agents[].url`（セラーが `check_governance` を呼ぶときにフェッチ）、コレクションリストの `webhook_url`、TMP プロバイダーの `endpoint`、`adagents.json` の `authoritative_location`、`reporting_bucket.setup_instructions` が含まれます。

`sync_accounts.accounts[].notification_configs[]` を通じて登録されるアカウントレベルの Webhook サブスクライバーも、アクティベーション前にエンドポイント所有権の証明を必要とします。SSRF 検証はセラーが内部ネットワークアドレスを呼んでいないことを証明します。バイヤーがパブリック HTTPS エンドポイントを制御することは証明しません。セラーは、新規または変更されたアクティブなサブスクライバーをアクティブとして扱う前に、RFC 9421 署名付きのアクティベーションチャレンジまたは同等の制御証明を完了しなければならず（MUST）、レシーバーはチャレンジをエコーする前にセラーアイデンティティ、配信認証メタデータ、イベントタイプセットを検証しなければなりません（MUST）。一時停止されたサブスクライバー（`active: false`）は非アクティブ中のアウトバウンド証明チャレンジのみスキップできます。セラーは書き込み時に URL パース、HTTPS、ホスト名正規化、予約範囲拒否を依然として強制しなければならず（MUST）、一時停止されたサブスクライバーは再アクティベートされるまで発火を受け取ってはなりません（MUST NOT）。標準チャレンジペイロードとレスポンス形状は [sync\_accounts endpoint proof of control](/docs/accounts/tasks/sync_accounts#endpoint-proof-of-control) で定義されています。

相手方が制御する URL へのアウトバウンドフェッチの前に、フェッチャーは次を行わなければなりません（MUST）:

1. **本番で非 HTTPS URL を拒否する。**
2. **ホスト名を解決し**、解決された IP がいずれかの予約範囲に入る場合フェッチを拒否する:
   * IPv4: RFC 1918（`10.0.0.0/8`、`172.16.0.0/12`、`192.168.0.0/16`）、RFC 6598 CGNAT（`100.64.0.0/10`）、ループバック（`127.0.0.0/8`）、リンクローカル（`169.254.0.0/16` — AWS/GCP/Azure/Alibaba のインスタンスメタデータで使われる `169.254.169.254` を明示的に含む）、ブロードキャスト（`255.255.255.255`）、`0.0.0.0/8`、マルチキャスト（`224.0.0.0/4`）。
   * IPv6: ループバック（`::1`）、ユニークローカル（`fc00::/7`）、リンクローカル（`fe80::/10`）、IPv4 マップ（`::ffff:0:0/96` — 予約 IPv4 を IPv6 にマップする最も一般的なバイパス）、マルチキャスト（`ff00::/8`）、AWS IMDSv2 の fd00:ec2::254 アドレス。
3. **接続を検証済み IP にピン留めする。** DNS ベースのフィルタリングだけでは DNS リバインディングに脆弱です: 攻撃者は検証時にパブリック IP を、接続時にプライベート IP を提供します。フェッチャーは接続をピン留めしなければなりません（MUST）。**推奨**: (a) 検証済み IP を TCP connect 呼び出しに直接渡し、`Host:` ヘッダーを URL から設定する。**フォールバック**（HTTP クライアントが事前解決 IP を受け付けられない場合のみ）: (b) いかなるリクエストボディを送る前に、ソケットのハンドシェイク後ピアアドレスを予約範囲リストに対して検証する。注: (b) は最初のボディバイトが出荷される前に発火するピアアドレスフックをクライアントライブラリが公開することに依存します。多くの一般的なライブラリはそうしないため、(b) を選ぶ実装はテストでフックを検証しなければなりません（MUST）。ピン留めなしの DNS 再解決は不十分です。
4. **相手方制御の URL をフェッチする際、リダイレクトの追従を拒否する**（30x レスポンスは、オリジンが最初のチェックをバイパスした予約アドレスにリダイレクトすることを許す）。2 つの制限された例外があり、各ホップでステップ 1-3 を再検証しチェーンに上限を設ける: [brand.json 解決](#buyer-identity-resolution)（1 リダイレクト、チェーンなし）と、初回の `/.well-known/adagents.json` フェッチ（標準の apex→www ホスティングが解決するよう、**同一登録可能ドメイン**リダイレクトのみ追従 — `apex ↔ www`、HTTPS 保持、3 ホップ以下、最初に要求されたドメインに固定 — [managed networks](/docs/governance/property/managed-networks#why-not-http-redirects) を参照）。`adagents.json` の `authoritative_location` 参照は例外を取りません: その 2 番目のホップでのリダイレクトは拒否しなければなりません（MUST）。
5. **レスポンスサイズとタイムアウトに上限を設ける。** 推奨: 5 MB ボディ上限、10 秒接続、10 秒読み取り。唯一の例外は、マネージドネットワーク間接パターンでの参照解決された権威的ファイル — ポインターファイルの `authoritative_location` がネットワークオリジンにリダイレクトした後の 2 番目のホップのみ — で、パブリッシャーネットワーク横断でファンアウトするため推奨 20 MB 上限を使います。ポインターファイル自体は 5 MB のままです。[managed networks security](/docs/governance/property/managed-networks#security-considerations) を参照。
6. **URL を提供したエージェントにフェッチエラーをエコーしない。** 詳細なエラーメッセージ（接続拒否 vs タイムアウト vs TLS 失敗）は、内部ネットワークトポロジーを探るサイドチャネルです。

#### 宛先ポート: デフォルトで寛容

パブリッシャーは、相手方が供給する URL（`push_notification_config.url`、コレクションリスト `webhook_url`、TMP プロバイダー `endpoint` など）に対して、デフォルトで宛先ポート許可リストを強制すべきではありません（SHOULD NOT）。URL 契約は `format: "uri"` のみで、プロトコルはポートを制約しません。バイヤーは正当に非標準 TLS ポートで Webhook レシーバーをホストします — Tomcat デフォルト `:9443`、Spring Boot デフォルト `:4443`、パスルーティングのマルチテナントゲートウェイ、テナントごとのポート付きサブドメイン切り出し — そしてデフォルトのポート許可リストは、パブリッシャーオペレーターにリストの拡張を頼む以外の手段なく、それらを黙って拒否します。

プロトコルが依拠する SSRF ガードは、上記ステップ 2-3 の **IP 範囲チェック + DNS リバインディング耐性のある接続ピン**であり、ポートフィルタリングではありません。予約範囲チェックは現実的な SSRF 脅威（`10.0.0.0/8`、`127.0.0.0/8`、`169.254.169.254` などの内部サービスへのトラフィック密輸）をカバーします。ルーティング可能なパブリック IP の上でのポートフィルタリングは、コスト（コンフォーマントなバイヤーの拒否）が通常その利益を上回る限界的な防御です。

多層防御として宛先ポート許可リストを望むオペレーター — 例えば、パブリッシャーの egress ファイアウォールがすでにアウトバウンドポートを制限するロックダウンされたエンタープライズ環境 — は、`{443, 8443}` を妥当なハードモードの出発点として、SDK またはデプロイ設定で明示的にオプトインすべきです（SHOULD）。`DEFAULT_ALLOWED_PORTS` 定数を出荷する SDK は、それを「制限なし」にデフォルトしなければならず（MUST）、`{443, 8443}` をデフォルトとしてではなくオプトインプロファイルとして表面化します。ハードモードをアクティブにするセラーは、バイヤーが最初の Webhook 配信時に制約を発見する前に統合をサイズできるよう、オペレーター向けドキュメントに許可ポートセットを文書化しなければなりません（MUST）。

ワイヤーレベルの URL 契約は **`format: "uri"` を超えて制約されません**。ハードモードのポートフィルタリングはオペレーター側のポリシー選択であり、プロトコル側の要件ではありません。

機能固有のセキュリティセクションは、これらのルールを独自のライフサイクルとコンテンツ処理要件で拡張します:

* [オフラインレポートバケット](/docs/media-buy/media-buys/optimization-reporting#security-considerations-for-offline-delivery) — IAM 層のプレフィックススコープ、アカウントステータス変更時の認証情報失効。
* [コレクションリスト](/docs/governance/collection/tasks/collection_lists#security-considerations) — `auth_token` スコープと失効、配信 ID 検証、Webhook 署名の規範ルール。
* [マネージドネットワークの `authoritative_location`](/docs/governance/property/managed-networks#security-considerations) — バリデーターのフェッチセマンティクス、変更検出、関係終了。
* [TMP プロバイダー登録](/docs/trusted-match/specification#provider-registration-security) — 動的登録認証、ルーター-プロバイダー間認証、`/health` の情報漏洩ルール。

## 認証のベストプラクティス

### 認証情報の保管

```javascript theme={null}
// Use secure key management systems
// Never commit credentials to version control
// Use environment variables or secret managers

// Example: Secure credential retrieval
async function getCredentials(agentId) {
  // Retrieve from secure storage (AWS KMS, Vault, etc.)
  const encrypted = await secretManager.get(`agent/${agentId}/apiKey`);
  return decrypt(encrypted);
}
```

### トークンの有効期限

高リスクオペレーションには短命なトークンを使います:

```javascript theme={null}
const TOKEN_LIFETIMES = {
  discovery: 3600,     // 1 hour for read operations
  financial: 900,      // 15 minutes for financial operations
  refresh: 86400       // 24 hours for refresh tokens
};

function validateToken(token, operationType) {
  const decoded = jwt.verify(token, secret);
  const maxAge = TOKEN_LIFETIMES[operationType] || TOKEN_LIFETIMES.discovery;

  if (Date.now() - decoded.iat > maxAge * 1000) {
    throw new Error('Token expired for this operation type');
  }

  return decoded;
}
```

## エージェントとアカウントの分離

あらゆる状態 — メディアバイ、クリエイティブ、冪等性キャッシュエントリ、セッション ID、ガバナンストークン — は、それを所有する[アカウント](/docs/reference/glossary#a)にスコープされます。クロスアカウント読み取りは、存在を漏らすのではなく汎用の「not found」を返さなければなりません（MUST）。認証済み[エージェント](/docs/reference/glossary#a)は、セラーが*誰が呼んでいるか*を知る手段です。リクエストの `account` は*その呼び出しが作用している請求関係*です。分離には両方のチェックが必要です。

セールスエージェントは次を行わなければなりません（MUST）:

1. **作成時にバインド** — 各オブジェクト（メディアバイ、クリエイティブ、セッションなど）を、それを作成したリクエストで使われたアカウントに恒久的に関連付ける。
2. **アクセス時に検証** — 後続の各読み取りまたは変更で、認証済みエージェントがオブジェクトのバインドされたアカウントへのアクセス権を持つことを検証する。
3. **フェイルクローズ** — 検証が失敗した場合、汎用エラーを返す（ステータス 403 または 404 が許容されるが、ボディは「未認可」を「not found」と区別したりアカウントを名指ししたりしてはならない）。決してリソースクエリにフォールスルーしない。

これらのルールが強制する請求関係モデルは [Accounts & Security — Data Isolation](/docs/media-buy/advanced-topics/accounts-and-security#data-isolation) を、[Account](/docs/reference/glossary#a) と [Agent](/docs/reference/glossary#a) の正式な定義はグロッサリーを参照してください。

### 二段階パターン

スキーマが `account` を要求するすべてのアカウントスコープリクエストは、明示的な `AccountRef`（アカウント ID 名前空間では `account_id`、バイヤー宣言アカウントでは `{brand, operator}` 自然キー）を運びます。セラーは、欠落した必須 `account` を認証情報が示すデフォルトで黙って置き換えてはなりません（MUST NOT）。`account` が任意のタスクでは、省略セマンティクスはタスクローカルで、そのタスクが文書化しなければなりません。正しい分離は、順に実行される 2 つのチェックです:

1. **認可プリチェック** — リクエストの `account` は認証済みエージェントの認可セット内になければなりません（MUST）。403 または汎用の「not found」でフェイルクローズ（決して「あなたはそのアカウントに認可されていません」ではない — それは存在の漏洩です）。
2. **リソースクエリ** — リクエストの `account_id` を主キー制約としてフィルタリング。認可セット全体ではなく、このリクエストが作用している特定のアカウントのみで。

```javascript theme={null}
// Two-step: precheck request account is authorized, then scope the query to it.
// authorizedAccountIds is a Set<string> populated once at auth-time, not an Array.
// Set.has() is O(1); Array.includes() is O(n) and scans element-by-element, which
// on large authorized-account sets introduces a timing difference between early
// and late matches that a caller can probe across requests.
async function getMediaBuy(mediaBuyId, requestAccountId, authAgent) {
  // Step 1: auth precheck
  if (!authAgent.authorizedAccountIds.has(requestAccountId)) {
    // Generic error - don't reveal whether the account exists
    throw new NotFoundError("Media buy not found");
  }

  // Step 2: resource query scoped to the specific account
  const mediaBuy = await db.mediaBuys.findOne({
    id: mediaBuyId,
    account_id: requestAccountId  // Primary filter
  });

  if (!mediaBuy) {
    // Generic error - same shape as the precheck failure
    throw new NotFoundError("Media buy not found");
  }

  return mediaBuy;
}
```

by-ID ルックアップで*全体の*認可セットでフィルタリングするのは退行です: アカウント A の下で発行された `get_media_buy(X)` は、両方がエージェントの認可セット内にあれば、アカウント B が所有するバイに対して成功してしまいます。リクエストが供給する `account_id` が、ルックアップを呼び出し元の*表明された*意図に結び付けるものです。

### 行レベルセキュリティ

最も一般的な分離の失敗は、**結合またはネストされた関係を介した IDOR** です: クエリが主テーブルを `account_id` でスコープするが、同じプリンシパルでフィルタリングされなかった関連テーブル（ラインアイテム、クリエイティブ、配信行）から結合または返す。1 つのハンドラーのバグが壁を突き破れないよう、ハンドラーコードだけでなくデータ層でプリンシパルごとに防御します:

```sql theme={null}
-- PostgreSQL example
-- app.current_account is set by the auth layer AFTER the precheck above succeeds
CREATE POLICY account_isolation ON media_buys
  USING (account_id = current_setting('app.current_account')::uuid);

ALTER TABLE media_buys ENABLE ROW LEVEL SECURITY;
```

**リストエンドポイント**（明示的なアカウントフィルターなしの `get_media_buys`）では、RLS は認証時に設定されるセッション変数を介してエージェントの認可セットにスコープします:

```sql theme={null}
CREATE POLICY account_isolation_list ON media_buys
  FOR SELECT
  USING (account_id = ANY(current_setting('app.authorized_accounts')::uuid[]));
```

### クライアント側の分離: クロスプリンシパルのツールコール混同

上記のルールはサーバー側の強制です。正当だが侵害されたエージェントが呼び出し元であっても、セラーのデータを保護します。**クライアント側の相棒**は、プリンシパル X が供給したテキストにプリンシパル Y の権限を使うツールコールを駆動させないというバイヤーエージェントの義務です。

LLM 駆動のバイヤーエージェントは通常、複数のプリンシパルの認証情報を同時に保持します: 複数のセラー（セラーごとに 1 つの認証情報セット）と、エージェンシーエージェント内では複数のブランドアカウント。エージェントが処理する任意の信頼できない文字列 — セラーが返すプロダクト説明、ブリーフから継承されたキャンペーン名、エラーエンベロープの拒否理由、Webhook イベントボディ — は、それらのプリンシパルの*1 つ*から供給されたテキストです。エージェントのプランニングループが単一の LLM コンテキストからそれらすべてにわたってツールを呼べる場合、セラー X のテキストに注入されたプロンプトが、エージェントにセラー Y のエンドポイントで `create_media_buy` を呼ばせたり、ブランド A の予算をブランド B のインベントリに使わせたりできます。これはツールコール粒度での[混乱した代理人](https://en.wikipedia.org/wiki/Confused_deputy_problem)問題です: 攻撃者はサンドボックスを脱出する必要がありません — エージェント自身の正当な権限が損害を与えます。

LLM 駆動の AdCP エージェントを運用するオペレーターは、少なくとも次の制御を適用しなければなりません（MUST）:

1. **テキストにその起源プリンシパルをタグ付けする。** LLM コンテキストがネットワークから取り込むすべての文字列（ツール結果、Webhook ボディ、レジストリドキュメント、クリエイティブメタデータ）は、それを生成した `{principal_domain, tool_name, response_field}` トリプルで内部的に注釈されなければなりません（MUST）。取り込み時に注釈を落とすことが、この防御が死ぬ場所です。
2. **ツールコールのターゲットを呼び出しプリンシパルに制限する。** ターゲットプリンシパルが、決定を駆動する文字列を供給したプリンシパルと同じでないツールコールは、(a) 拒否されるか、(b) 人間の承認ステップを通るか、(c) オペレーターが事前に宣言した明示的なプリンシパルごとのポリシーで仲介されなければなりません（MUST）。デフォルトは allow ではなく refuse でなければなりません（MUST）。
3. **認証情報スコープを LLM コンテキストごとに分離する。** 単一の LLM プランニングループは、利害が衝突し得るプリンシパル（例: 同じインベントリを競う 2 つのブランド。1 つのコンテキスト内のバイヤー認証情報とガバナンスエージェントの署名鍵）のライブ認証情報を保持してはなりません（MUST NOT）。スコープ分離は、LLM に指示するのではなく、プロセス / ツール登録層で強制されます — LLM は誤用のアフォーダンスを持ってはなりません（MUST NOT）。
4. **成功だけでなく、すべてのクロスプリンシパルの*試み*をログする。** ルール 2 の下での拒否は、オペレーターが監視しなければならないシグナルです（MUST）— あるプリンシパルからの拒否率の上昇は、あなたのエージェントを標的とする注入キャンペーンの最も早く検出可能な兆候です。

この脅威は通常のプロンプト注入とは異なります: 通常の注入は*1 つの*プリンシパルの権限内でデータを流出させたり未認可のツールコールをトリガーしたりします。クロスプリンシパル混同は、攻撃者が Y の認証情報を一度も保持せずに、プリンシパル X の信頼できないテキストを使ってプリンシパル Y の権限に到達します。上記のサーバー側 Layer 2 制御は、プリンシパル Y のアカウントがバイヤーエージェントの認可セットにまだない場合にのみ試みを検出します — ある場合（エージェンシーとマルチセラーエージェントの要点そのもの）、サーバーは正当に見える呼び出しを見ます。

プロトコルはこの規律をクライアントエージェントに強制できません。そのテストは運用的です: すべての LLM 駆動 AdCP バイヤーは、どのプリンシパルが同じプランニングコンテキストに一緒に現れられるか、クロスプリンシパルのツールコールを何がゲートするかを、書面で説明できなければなりません（MUST）。

## 時間セマンティクス

AdCP は管轄区域、アドサーバー、デイパートカレンダーをまたいで動作します。実装は時間について正確でなければならず（MUST）、さもなくばバイヤーとセラーは「午後 5 時までに配信」が何を意味したかで意見が食い違います。

### タイムスタンプ形式

AdCP のリクエスト、レスポンス、Webhook ペイロードのすべてのタイムスタンプフィールドは、明示的なタイムゾーンオフセット付きの [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) でなければなりません（MUST）。

```
✅ 2026-04-19T10:00:00Z            // UTC, recommended
✅ 2026-04-19T10:00:00-04:00       // explicit offset
❌ 2026-04-19T10:00:00             // no offset — ambiguous
❌ 2026-04-19 10:00:00             // not ISO 8601
```

実装は曖昧な（「ナイーブな」）タイムスタンプを `INVALID_REQUEST` で拒否しなければなりません（MUST）。実装はワイヤー上で UTC（`Z` サフィックス）を使い、プレゼンテーション層でローカル時刻に変換すべきです（SHOULD）。

### 区間

AdCP のあらゆる時間ウィンドウ — フライト日、レポートウィンドウ、デイパートターゲティング、冪等性リプレイ TTL — は**半開区間** `[start, end)` を使います。開始タイムスタンプは含み、終了タイムスタンプは含みません。`start_time: 2026-04-01T00:00:00Z` と `end_time: 2026-05-01T00:00:00Z` のキャンペーンは 4 月中実行され、5 月の最初のティックで停止します。

### デイパートターゲティング

デイパート定義は**タイムゾーンセマンティクス**を宣言しなければなりません（MUST）— 時刻値が持つ 3 つの意味のどれか:

* **バイヤー宣言ゾーン** — デイパートと並ぶ IANA ゾーン名（例: `timezone: "America/New_York"`）。デイパートは、視聴者やパブリッシャーの場所に関わらずそのゾーンに対して評価されます。バイヤーが「ニューヨーク時間の午後 9〜11 時」をグローバルに強制したいときに使います。
* **パブリッシャーローカル** — デイパートはパブリッシャーが宣言したローカルゾーンで評価されます。バイヤーが「パブリッシャーのスケジュール上のプライムタイム」を望み、それが何を意味するかをパブリッシャーに決めさせてよいときに使います。
* **視聴者ローカル** — デイパートは各視聴者のタイムゾーンに対して評価され、配信時に視聴者の場所シグナルから解決されます。バイヤーがグローバルオーディエンス横断で「ローカル午後 8 時に配信」を望むときに使います。

宣言されたセマンティクスのないデイパートは曖昧で、`INVALID_REQUEST` で拒否しなければなりません（MUST）。セラーは宣言されたセマンティクスを守らなければなりません（MUST）。セラーが要求されたモードをサポートできない場合（例: 単一ゾーンで動作するパブリッシャーは視聴者ローカルデイパートを配信できない）、セラーは黙って変換するのではなく `INVALID_REQUEST` で拒否しなければなりません（MUST）。エージェントごとのデフォルトは非規範的で、依拠してはなりません（MUST NOT）。

## Request Safety

### 冪等性

`idempotency_key` は**すべての AdCP タスクリクエストで必須**です — 読み取りも変更系も同様。キーは `(認証済みエージェント, アカウント)` ごとにスコープされます — 同じセラー上の別エージェント、同じエージェント下の別アカウント、別セラーをまたいでは意味を持ちません。両次元でスコープすることで、1 つのエージェント（例: エージェンシー）が複数アカウントに作用するときのクロスアカウントキャッシュ衝突を防ぎます: アカウント A とアカウント B の下での同一に見える `create_media_buy` は 2 つの別個のバイであり、2 つにまたがってリプレイされる 1 つのキャッシュレスポンスにはなりません。

**強制カーブ。** セラーは 3.0 以降、`idempotency_key` を省略する**変更系**リクエストを `INVALID_REQUEST` で拒否しなければなりません（MUST、変更なし）。**読み取り**リクエストについては、ルールは 2 つのマイナーにわたって段階的に導入されます:

* **3.1.0** — セラーは `idempotency_key` を運ぶ読み取りを受け入れ、ルール 2-9 に従って処理しなければなりません（MUST、未宣言のエンベロープフィールドで拒否しない）。セラーはそれを省略する読み取りを `INVALID_REQUEST` で拒否すべきです（SHOULD）。セラーは 3.1.x メンテナンスウィンドウの間は省略を受け入れてもよい（MAY）。
* **3.2.0** — セラーは `idempotency_key` を省略する読み取りを `INVALID_REQUEST` で拒否しなければなりません（MUST）。猶予ウィンドウは 3.2 のカットで閉じます。

この段階的強制により、手書きのバイヤー統合 — curl、薄い MCP クライアント、またはフィールドを一律に含めない OpenAPI codegen で構築 — が 3.1 のカットではなくリリースウィンドウにわたって移行できます。バイヤー SDK（`@adcp/client`、`adcp-py`）は今日すでに `idempotency_key` を一律に送っているため、SDK 利用の統合者はカット日の影響を受けません。

**なぜユニバーサルか — 読み取りツールを含む。** いくつかの AdCP タスクは多相です。`get_products` が正準のケースです: `buying_mode: 'brief'` / `'wholesale'` は同期的に完了する（純粋な読み取り）ことがありますが、キュレーションが上流クエリや HITL を必要とするとき同じツールが `Submitted` エンベロープを返してもよく（MAY）、`action: 'finalize'` 付きの `buying_mode: 'refine'` はプロポーザルを `expires_at` ホールドウィンドウ付きでコミット済みに遷移させるコミットです（[refinement guide § Finalize is exclusive](/docs/media-buy/product-discovery/refinement) を参照）。バイヤーは呼び出し時に、ある呼び出しが純粋な読み取り、非同期タスク作成、コミットのどれになるかを予測できません — したがってワイヤー契約はすべての呼び出しで一律に `idempotency_key` を要求します。純粋な読み取りとして解決する呼び出しでは、キャッシュは TTL 内でバイト安定なリトライ時リプレイを提供し、これは無害でバイヤーに一律のリトライセーフな契約を与えます。非同期タスク作成またはコミットとして解決する呼び出しでは、キャッシュは変更系タスクと同じ at-most-once 保証を提供します。代替案 — バイヤーの SDK で呼び出しごとに読み取り vs 変更系を分類 — は、同じタスク名が読み取りと書き込みの両モードを持つとき実現不可能です。セラーが返す未知の `error.code` 値のデコード（猶予ウィンドウ中の `INVALID_REQUEST` でも、後のマイナーで追加されたコードでも）は [Forward-compatible decoding](/docs/building/by-layer/L3/error-handling#forward-compatible-decoding-normative) ルールに従います。

このセクションは AdCP タスクリクエストにのみ適用されます。OpenRTB 入札ストリームは独自のセマンティクス（`BidRequest.id` は冪等性キーではなくトランザクション ID）を持ち、スコープ外です。

#### 規範的なセラーの動作

1. **スキーマ検証が最初に実行される。** セラーは、冪等性キャッシュを参照する前に、リクエストをそのスキーマ（`idempotency_key` の存在と形式を含む）に対して検証しなければなりません（MUST）。不正なリクエストはキャッシュに一切触れずに `INVALID_REQUEST` を返します — さもなくばキャッシュミスがタイミングサイドチャネルになり、スキーマ検証がキー形式を受け入れたかを漏らします。検証エラーは決してキャッシュされません（ルール 2）。

2. **最初の呼び出しが正準。** **タスク成功**時（`status: completed`、または非同期オペレーションの `status: submitted`）、セラーは内側のレスポンスペイロード（プロトコルエンベロープではない）を `(authenticated_agent, account_id, idempotency_key)` でキーし、正準リクエストペイロードのハッシュと共に保存します。**キャッシュエントリは不変です** — TTL 内のリプレイは元々キャッシュされたペイロードを（`replayed: true` 付きで）返さなければならず（MUST）、そのペイロード内の状態追跡フィールドはリソースの現在の状態を反映するようリフレッシュされてはなりません（MUST NOT）。このルールは両方の成功ブランチにわたって適用されます:

   * **非同期タスク** — キャッシュされたレスポンスは `task_id` を含む `submitted` 結果です。非同期タスクがその後完了・失敗・キャンセルされても、リプレイは現在の終端状態ではなく元々キャッシュされた `submitted` レスポンスを返さなければなりません（MUST）。バイヤーは返された `task_id` を使い、最初の呼び出しと全く同じように `tasks/get` または Webhook で現在の状態を観測します。
   * **同期成功タスク** — 初回レスポンスが状態追跡フィールド（例: `create_media_buy` の `status`, `packages`, `affected_packages`。`sync_creatives` / `sync_accounts` のレコードごとの `status` 配列。`acquire_rights` / `activate_signal` のリソーススナップショット）を運ぶ場合、リプレイはリソースへの介在する変更に関わらず元々キャッシュされたペイロードを返さなければなりません（MUST）。`status: pending_creatives` で作成され、その後 `update_media_buy` で `canceled` に変更されたメディアバイは、`status: pending_creatives` としてリプレイされます — キャッシュされたバイトは作成時レスポンスの履歴スナップショットであり、現在状態の読み取りではありません。バイヤーは現在の状態についてリソースの読み取りエンドポイント（`get_media_buys`, `list_accounts`, `list_creatives` など）を参照しなければなりません（MUST）。下記「バイヤーの義務」を参照。

   これはバイト安定なキャッシュ特性を一律に保ち、冪等性層をリソースライフサイクルから分離します — セラーはタスクやリソース状態が変わってもキャッシュエントリを更新する必要がありません。代替案（「リプレイ時に状態フィールドをリフレッシュ」）は、すべてのセラーにリソース状態機械を冪等性キャッシュに通させ、あるキーの有効なキャッシュ内容の数を増やし（単一キーのリプレイが呼び出し間で決定論的でなくなる）、残りのルールが依拠する正準リプレイの不変条件を壊します。セラーは、一部の状態追跡フィールドがリプレイ時にリフレッシュされ、他がされないハイブリッドを実装してはなりません（MUST NOT）— 部分リフレッシュは両方の選択肢の最悪で、非コンフォーマントです。

3. **成功レスポンスのみがキャッシュされる。** いかなるエラー — 検証、ガバナンス拒否、トランスポート失敗、内部エラー — でもキーは**保存されません**。リトライは再実行します。これはバイヤーの意図に一致します: 5xx 後のリトライは失敗をリプレイするのではなく再試行すべきです。また、バイヤーの不正リクエストがキーに TTL 全体ロックされるのを防ぎます。

4. **リプレイはキャッシュされたレスポンスを返す。** 同じ `idempotency_key` かつ等価な正準形ペイロード（下記「ペイロード等価性」を参照）を持つ後続リクエストは、副作用を再実行せずに保存された内側レスポンスを返さなければなりません（MUST）。セラーはレスポンス時に送信プロトコルエンベロープに `replayed: true` を注入します — `replayed` は冪等性層が生成するエンベロープレベルのフィールドであり、キャッシュされた内側レスポンスの一部では**ありません**。リプレイ時の注入により、エンベロープ変更（新しい `timestamp`、ローテーションされた `governance_context` など）に関わらずキャッシュされたペイロードがリプレイ間でバイト安定に保たれます。MCP のトランスポート固有の注記: MCP ツールレスポンスは別個のエンベロープスロットを持ちません。サーバーは `replayed` をツール結果オブジェクト自体の中（例: 構造化リターンの先頭）またはレスポンスメタデータフィールドで公開してもよい（MAY）。REST と A2A レスポンスはエンベロープフィールドを直接使います。

5. **異なる正準ペイロードでのキー再利用は競合。** 同じキー、リプレイウィンドウ内で異なる正準ハッシュは `IDEMPOTENCY_CONFLICT` で拒否しなければなりません（MUST）。セラーは 2 番目のリクエストを黙って適用してはなりません（MUST NOT）。

6. **期限切れキーは明示的に拒否される。** `replay_ttl_seconds` が経過した後、セラーはキャッシュエントリを退避してもよい（MAY）。セラーが見たことのあるキーで退避後に到着するリクエストは、黙って新規として扱うのではなく `IDEMPOTENCY_EXPIRED` で拒否すべきです（SHOULD）— 黙った再実行は、まさにキーが防ぐはずのダブルブッキングのフットガンです。セラーは TTL 境界で ±60 秒のクロックスキューウィンドウ（本書の他所で JWS `exp` に適用される許容範囲と同じ）を許可すべきです（SHOULD）。名目上の期限切れの数秒後に到着するリトライが、新規として扱われるのではなく依然キャッシュからリプレイされるように。

   **耐久性は規範的。** 宣言された `replay_ttl_seconds` はベストエフォートのキャッシュヒントではなく耐久性契約です。セラーは、宣言された TTL の間、プロセス再起動、ポッド置換、リージョンフェイルオーバー、オペレーター起因のキャッシュフラッシュを生き延びるストレージで冪等性キャッシュをバックアップしなければなりません（MUST）。インメモリのみのストア（プレーンな `Map`、バッキング層なしの単一プロセス LRU）は、`replay_ttl_seconds` がプロセス寿命を超えるときは常に非コンフォーマントです — 3600 秒の下限では常に真です。宣言された TTL 未満での黙った退避の帰結は**変位リプレイウィンドウ**です: 送信者は新しい署名ノンスの下で同じ `idempotency_key` で正当にリトライし（署名済みリトライが機能すべき方法 — ノンスは送信ごとであってイベントごとではない）、署名リプレイチェックを通過し、レシーバーのインメモリ状態が落とされたためアプリ層キャッシュが空であることを見つけます。副作用が 2 回実行されます。セラーは、キャッシュ層が耐久的に守れる以上の `replay_ttl_seconds` を宣言してはならず（MUST NOT）、「見たことがない」を「宣言 TTL 下で退避された」と区別できないとき、フェイルオープン（黙った再実行）ではなくフェイルクローズ（`IDEMPOTENCY_EXPIRED`）しなければなりません（MUST）。運用上の現実が「メモリのみ、ポッド再起動で消失」のセラーは、`replay_ttl_seconds` を保証される最短ポッド寿命以下に宣言することが求められます — 実際上、これは耐久層を強制します。

7. **リプレイウィンドウは推論ではなく宣言される。** セラーは `get_adcp_capabilities` で `capabilities.idempotency.replay_ttl_seconds` を宣言しなければなりません（MUST、最小 3600 秒 / 1 時間、推奨 86400 秒 / 24 時間、最大 604800 秒 / 7 日）。クライアントは想定デフォルトにフォールバックしてはなりません（MUST NOT）— 宣言のないセラーは非準拠で、リトライ機微なオペレーションには安全でないものとして扱わなければなりません（MUST）。

8. **キャッシュ増大防御。** セラーは、リクエストレート制限とは別に、`(authenticated_agent, account)` ごとの冪等性キャッシュ挿入レート制限を適用しなければならず（MUST）、エージェントごとの挿入レートが設定上限を超えたときはキャッシュを無制限に増大させるのではなく `RATE_LIMITED`（[error taxonomy](/docs/building/by-layer/L3/error-handling#rate-limit-handling) を参照）を返さなければなりません（MUST）。安価な成功パスオペレーション（例: `log_event`）で毎秒 N 個の新しいキーを送るバイヤーは、さもなくば無制限のストレージを強制し、3600 秒の下限で `replay_ttl_seconds` に比例した増幅を伴います。自然な境界は `inserts_per_hour × replay_ttl_hours ≤ max_cache_rows_per_agent` です。

   **推奨上限（3.1+）:** 元の 60/秒持続 / 300/秒バーストの単一予算上限は、書き込み重視のローンチパターン（10 メディアバイ/分以下 × 10 パッケージ × 10 クリエイティブ、3-5 倍の余裕）に対してサイズされました。ユニバーサル冪等性の下では、読み取りトラフィックも挿入レートに寄与します — 5 アカウント横断で `get_products(brief)` + `list_creatives` + `list_accounts` を 1Hz でポーリングする単一のエージェンティックダッシュボードは、いかなる書き込み活動の前に読み取りだけで約 15 挿入/秒です。オペレーターは `(authenticated_agent, account)` ごとの**分割予算**を採用すべきです（SHOULD）:

   * **読み取り: 300 挿入/秒持続、ローリング 10 秒ウィンドウで 1,500/秒バースト。** [Polling / state re-read](#agent-retry-vs-polling-vs-re-plan) ルール下でのダッシュボードポーリングとエージェンティック状態再読み取りが支配的。読み取りトラフィックは通常、ユーザー駆動の UI 操作中はバースト的、エージェント実行中は低レートで安定。
   * **書き込み: 60 挿入/秒持続、300/秒バースト。** 元の書き込み重視サイジングから変更なし — バイヤーのダッシュボードポーリングが、`create_media_buy` / `sync_creatives` / `activate_signal` をダブル実行レースから守る書き込み容量を枯渇させられないよう、別個の予算として保持。
   * **合算上限（多層防御）:** 総挿入はエージェントごとに 350/秒持続 / 1,700/秒バーストを超えるべきではありません（SHOULD NOT）— 小さなクッション付きの 2 予算の合計。読み取り予算を飽和させる攻撃者が書き込み容量を飢えさせられないように。

   安定した低ボリュームトラフィックのオペレーターはこれらの開始値未満に締めてもよい（MAY）。この上限より大きいバーストオンボーディングやトラフィッキングパターンのオペレーターは、正当なトラフィックの黙った拒否を受け入れるのではなく引き上げなければなりません（MUST）。分割予算の形状（別個の読み取りと書き込みカウンター）は、オペレーターが大きさを締めても 3.1 以降実装しなければなりません（MUST）— 共有単一予算上限がこのルールが防ぐ失敗モードです。持続境界はローリング 60 秒ウィンドウです — 10 秒ウィンドウを空にするバーストは 60 秒ローリング境界の次の 50 秒にカウントされます。異なるウィンドウ形状（固定分バケット、EWMA）を採用するセラーは、リトライロジックを持つバイヤーが `RATE_LIMITED` がいつ発火するか予測できるよう文書化しなければなりません（MUST）。セラー間のウィンドウ形状の黙った発散は、同一のバイヤートラフィックがあるセラーを通過し、コンフォーマントな実装で別のセラーに拒否されることを意味します。3600 秒 TTL 下限で合算上限レートはエージェントごとの常駐を約 126 万エントリに制限します — 書き込みのみサイジングの元の 21.6 万から一桁上で、読み取りトラフィックの追加を反映します。エージェントごとのストレージ予算はこれを考慮すべきです。数値推奨は SHOULD レベルです。レート制限して `RATE_LIMITED` で拒否する動作自体は MUST です。セラーは上限を調整可能な設定パラメーターとして公開しなければなりません（MUST）— 300/60 の読み取り/書き込み分割数はエージェンティックバイヤーダッシュボードパターンの初回デプロイ開始点であり、凍結されたデフォルトではありません。セラーは正確な設定上限数値をケイパビリティレスポンスで公開すべきではありません（SHOULD NOT）— そうすると上限がエコシステム全体の攻撃ターゲットになります。バイヤーは、ケイパビリティイントロスペクションではなく `RATE_LIMITED` + `retry_after` レスポンスを通じて実効上限を発見します。

   上限は `(authenticated_agent, account)` ごとです — 冪等性キー自体（項目 1）と同じスコープ — なので、マルチアカウントエージェンシーはアカウントごとの予算が単一の共有クォータに畳み込まれません。`RATE_LIMITED` 拒否は [error handling taxonomy](/docs/building/by-layer/L3/error-handling#rate-limit-handling) に従い `retry_after`（秒）を設定しなければならず（MUST）、冪等性レスポンスとしてキャッシュされてはなりません（MUST NOT、ルール 3: 成功レスポンスのみキャッシュ）。セラーは `retry_after` を安価な拒否フロアとして強制すべきです（SHOULD）— `retry_after` 経過前にリトライするバイヤーは、リトライごとにフルのスキーマ検証・キャッシュチェックパイプラインに再入するのではなく、事前認証トークンバケット（例: リバースプロキシ層）にヒットすべきです（SHOULD）。この規律なしでは、誤動作するバイヤーがレートリミッター自体の負荷を増幅できます。

9. **並行リトライ — 最初の挿入が勝つ。** 同じ `(authenticated_agent, account_id, idempotency_key)` を運ぶ 2 番目のリクエストが、最初のリクエストがまだ実行中に到着してもよい（MAY）— 最も一般的には、セラーのダウンストリーム呼び出しが返る前にバイヤーのトランスポートタイムアウトが発火し、バイヤーがリトライするとき。セラーはレースを決定論的に解決しなければならず（MUST）、副作用を 2 回実行してはならず（MUST NOT）、2 番目のリクエストを黙って落としてはなりません（MUST NOT）。解決はスコープタプル上の `(unique constraint, INSERT … ON CONFLICT DO NOTHING)` パターンです: 最初に着地する行が実行を所有し、正準ペイロードハッシュを実行中の行に保存する（センチネルではない）。後続リクエストは、レスポンススロットがまだ設定されていないがペイロードハッシュは設定されている既存行を観測します。

   セラーは 2 番目のリクエストを 2 つのポリシーの 1 つで処理しなければならず（MUST）、呼び出し間で一貫して動作しなければなりません（MUST）— クライアントはセッション内の最初のレスポンスからポリシーを推論し、後続のリトライに適用します:

   * **Wait-and-replay**（高速オペレーション向け推奨、通常 5 秒未満）: セラーは最初が完了するまで 2 番目のリクエストをブロックし、その後 `replayed: true` 付きでキャッシュされたレスポンスを返します。2 番目の呼び出しの総壁時間はセラーのリクエストタイムアウト予算で制限されます。
   * **Reject-and-redirect**（長時間実行のダウンストリーム呼び出しを伴う低速オペレーション向け推奨）: セラーは即座に `IDEMPOTENCY_IN_FLIGHT` を返し、最初のリクエストの経過時間と予想完了に基づいて `error.details.retry_after`（秒、整数）を設定します。バイヤーはヒント経過後、同じ `idempotency_key` でリトライしなければなりません（MUST）— `IDEMPOTENCY_IN_FLIGHT` で新しいキーを生成するバイヤーは、安全なリトライを、まさにこのルールが防ぐダブル実行レースに変えます。

   同じキーかつ*異なる*正準ペイロードを持つ 2 番目のリクエストが実行中ウィンドウ中に来た場合、`IDEMPOTENCY_IN_FLIGHT` ではなく `IDEMPOTENCY_CONFLICT`（ルール 5）を返さなければなりません（MUST）— 正準形の不一致は行の保存済みハッシュに対して INSERT 時に計算可能なので、最初のリクエストのレスポンスを待たずに競合を検出できます。バッキングストアがハンドラー完了まで実際の正準ハッシュを永続化できないセラー（例: プレースホルダーセンチネルパターン）は、ルール 9 のコンフォーマンスを宣言する前に INSERT 時にハッシュを永続化するようストアをアップグレードしなければなりません（MUST）— 代替案（同一キー・異ペイロードレースで `IDEMPOTENCY_IN_FLIGHT` を返し、最初のリクエスト完了後にのみ競合を表面化）は、実際のクライアントバグの検出を黙って遅らせます。

   ルール 3 に従い、最初のリクエストが最終的に失敗する（検証エラー、ダウンストリームタイムアウト、内部エラー）場合、`(in_flight)` 行は解放されます — キーは「見たことがない」状態に戻り、後続のリトライは最初から再実行します。セラーは実行中の行の寿命を宣言されたタスクごとハンドラータイムアウトに制限しなければならず（MUST）、そのタイムアウトが発火したとき — ダウンストリームがまだ応答していなくても — 行を解放しなければなりません（MUST、ルール 3 に従い失敗として扱う）。この境界なしでは、ハングしたハンドラーが同じキーに対して無期限に `IDEMPOTENCY_IN_FLIGHT` を返し、バイヤーをいかなる安全なリトライパスからもロックアウトします。

   reject-and-redirect を使うセラーは、`error.details.retry_after` を `replay_ttl_seconds`（`capabilities.idempotency` で宣言）以下の値に設定しなければなりません（MUST）。セラー自身のリプレイウィンドウを過ぎて待つよう指示されたバイヤーは、レスポンスがもはやリプレイできなくなるまで待つよう言われています — 待機は無意味で、バイヤーは新しいキーを生成する（このルールが防ぐ失敗モード）か、リトライで `IDEMPOTENCY_EXPIRED` にヒットします。セラーは `capabilities.idempotency.in_flight_max_seconds` — 実行中の行の最大寿命、セラーのタスクごとハンドラータイムアウトにスコープ — も宣言すべきです（SHOULD）。バイヤーは存在する場合その宣言値を主要なリトライ予算境界として使うべきです（SHOULD）。不在の場合、桁数ヒューリスティクス（セラーの典型的なハンドラーレイテンシーから導出され、リプレイ TTL の一桁下、決して TTL 上限自体ではない値）にフォールバックします。

   セラーはスコープ境界をまたいで実行中の状態を漏らしてはなりません（MUST NOT）: 候補キーを探る攻撃者は、行が存在するか、実行中か、一度も存在しなかったかに関わらず、同じレスポンス形状とタイミングを受け取らなければなりません（MUST）。

10. **サービス境界をまたぐ — ダウンストリームリコンシリエーション。** セラーはリクエスト処理中にダウンストリームシステムを呼び出すのが一般的です — `create_media_buy` での SSP/アドサーバー呼び出し、請求オペレーションでの決済プロバイダー呼び出し、`check_governance` でのガバナンスエージェント呼び出し。これらの呼び出しは、セラーを「ダウンストリーム不明」状態に残し得る独自の失敗モードを持ちます: ダウンストリームがリクエストを受け入れた後、そのレスポンス到着前にネットワーク接続が切れた。セラープロセスが呼び出し中にクラッシュした。リージョンフェイルオーバーがレスポンス永続化前にワーカーをスワップした。ルール 3（成功レスポンスのみキャッシュ）は必要だが不十分です: 単にキャッシュせずリトライで再実行するセラーは、ダウンストリームを二重呼び出しし、そこで重複した副作用を作ります。

    **コンフォーマンスの採点。** このルールはコンプライアンスストーリーボードスイートによるプログラム的採点ではなく、レビュアー採点です。ブラックボックス観察は「セラーがクレーム行を持つ」を「セラーがテスト実行で運が良かった」と区別できません。`parallel_dispatch_runner` テストキットはルール 10 のコンフォーマンスを `reviewer_checks` の下にリストします — ルール 10 のコンフォーマンスを表明するセラーは、どのパターンがどのダウンストリームに適用されるかを記述する運用ランブックを表面化しなければならず（MUST）、レビュアーはそのランブックに対して実装を検証します。他の規範ルール（1-9）はプログラム的に採点されます。

    セラーは、二重呼び出しがビジネス上の帰結（リソース作成、決済移動、不可逆な状態変更）を持つすべてのダウンストリーム呼び出しについて、2 つのリコンシリエーションパターンの 1 つを採用しなければなりません（MUST）。読み取り専用のダウンストリーム呼び出し（キャッシュルックアップ、書き込まない適格性チェック）は免除されます — が、ダウンストリーム監査ログにも書く不正スコアリングルックアップのような境界ケースはこのルールでは書き込みとしてカウントされます（監査ログエントリが副作用）。

    * **Write-claim-before-invoke（推奨デフォルト）。** ダウンストリームを呼び出す前に、セラーは冪等性キャッシュ行と同じトランザクションで「クレーム」行を永続化します — 通常 `{idempotency_key, downstream_provider, downstream_request_id, status: 'invoked', invoked_at}` — セラー生成の `downstream_request_id`（ダウンストリーム自身の相関/冪等性識別子としてダウンストリームに渡す）を使って。リトライ時、ダウンストリームを再度呼び出す前に、セラーは `(idempotency_key, downstream_provider)` でクレーム行をルックアップしてリコンサイルしなければなりません（MUST）: `downstream_request_id` でダウンストリームをクエリして真の結果を判定し、そこからキャッシュ投入を再開します。セラーは、ローカルレコードの欠落を「ダウンストリーム呼び出しは起きなかった」と扱ってはなりません（MUST NOT）— ダウンストリーム受け入れとローカル永続化の間のクラッシュは、まさに起きてローカルレコードが欠落しているケースです。ダウンストリームが `downstream_request_id` のレコードなしを報告する場合（クレーム行は永続化されたが、セラーが呼び出し前にクラッシュ）、セラーは呼び出しを未実行として扱い、呼び出しを進めなければなりません（MUST）。クレーム行はすでに `downstream_request_id` を予約しているので、ダウンストリーム自身の冪等性が後続のリトライを重複排除します。ダウンストリームルックアップからの曖昧なレスポンス（一時的 5xx、ネットワークエラー、不正レスポンス）では、セラーはフェイルクローズしなければなりません（MUST）— 未認証の「レコードなし」シグナルで呼び出しを進めるのではなく、バイヤーに一時的エラーを返します（バイヤーがルール 9 に従い同じ `idempotency_key` でリトライするように）。
    * **Thread-buyer-key（ダウンストリームプロトコルがサポートする場合に許容）。** セラーはバイヤーの `idempotency_key` のダウンストリームプロバイダーごとの派生をダウンストリーム自身の冪等性キーとして渡します — 通常 `HMAC(K_provider, idempotency_key)`。ここで `K_provider` はプロバイダーアイデンティティでキーされたセラーの KMS 管理ルートから導出されます（ダウンストリームごとに 1 鍵、すべてのダウンストリームで共有する 1 つのセラーシークレットではない）。プロバイダーごとの導出は、単一のダウンストリームが侵害された場合のクロスプロバイダーリプレイを防ぎます。すべてのダウンストリームで共有するセラーシークレットは、すべてのプロバイダーを単一の鍵露出影響範囲に畳み込みます。ダウンストリームの at-most-once 保証が、セラーのローカル永続化が見逃したケースをカバーします。セラーは、キャッシュされたレスポンスが正しく投入されるよう成功パスで依然クレーム行を書かなければなりません（MUST）が、ダウンストリーム自体がリトライ時の真実の源になります。セラーは、異なる信頼プリンシパルが運用する任意のダウンストリームにバイヤーの生の `idempotency_key` を渡してはなりません（MUST NOT）— バイヤーのキーは TTL 内のケイパビリティトークン（下記「キーはセキュリティ機微」を参照）であり、信頼境界をまたいで転送するとケイパビリティ面が広がります。「異なる信頼プリンシパル」とは、セラーが同じセキュリティ境界の下で運用しない任意のシステムを意味します。セラーがエンドツーエンドで所有する純粋にテナント内のマイクロサービス（同じ KMS、同じ監査ログ、同じオペレーター）に生のキーを渡すことは信頼境界をまたがず、許可されます（ただしプロバイダーごとの導出が依然としてより良いデフォルト）。

    セラーは、どのパターンがどのダウンストリームに適用されるかを運用ランブックに文書化しなければなりません（MUST）。セラーは「ダウンストリームレスポンス検査でのベストエフォート重複排除」という 3 番目のパターン — ダウンストリームのレスポンスペイロードをキャッシュされた指紋と比較して呼び出しがすでに起きたか判定 — を使ってはなりません（MUST NOT）。ダウンストリームのレスポンス形状はバージョン間で変わり、指紋は同期バグの温床だからです。クレーム行 OR スレッド化されたキー。レスポンスへのパターンマッチではありません。

    セラーは、ダウンストリーム起因のエラーをバイヤーに返す際、バイヤーの `idempotency_key`（またはその可逆な派生）をエラーエンベロープに含めてはなりません（MUST NOT）。セラーのダウンストリームプロバイダーごとのキー（またはセラーが誤って生でスレッド化した場合はバイヤーのキー）に言及するダウンストリームエラーは、バイヤーに伝播する前に再キーまたは除去されなければなりません（MUST）— さもなくばダウンストリームエラーメッセージが信頼境界をまたぐキー開示面になります。

    このルールのバイヤー可視の帰結: セラーが低速ダウンストリームを呼び出し、バイヤーがウィンドウ中にリトライするとき、2 番目のリクエストでのセラーのレスポンスは、ダウンストリームの動作ではなく、ルール 9 の下でのセラーのポリシー（`IDEMPOTENCY_IN_FLIGHT` または wait-and-replay）で決まります。バイヤーはどのダウンストリームがパスにあるか知る必要はありません — セラーは関わらず一律のリトライ面を提示しなければなりません（MUST）。

#### ペイロード等価性

「等価」とは、フィールドごとのセマンティック比較ではなく、**同一の正準 JSON 形式**を意味します。セラーは正準形をハッシュしてハッシュを比較することで等価性を判定しなければなりません（MUST）。正準形は [RFC 8785 JSON Canonicalization Scheme (JCS)](https://www.rfc-editor.org/rfc/rfc8785) です — 数値シリアライズ、キー順序、エスケープはすべて JCS §3 に規範的に従います。

**ハッシュから除外されるフィールド**（閉じたリスト — セラーは拡張してはならない、MUST NOT）:

* `idempotency_key` — キー自体
* `context` — バイヤー不透明なエコーデータ（トレース ID、相関 ID）は設計上リトライで変わる
* `governance_context` — エンベロープ上。リトライでリフレッシュされた署名トークンかもしれない
* `push_notification_config.authentication.credentials` — ローテーションされた bearer トークンかもしれない。URL とスキームはハッシュに残る。クレデンシャル値のみ除外。

リクエストボディの他のすべて — `ext` を含む — は含まれ、「欠落した任意フィールド」は「明示的に null に設定されたフィールド」と等価では**ありません**（JCS は区別を保持し、ハッシュも同様）。**バイヤーはローテーションするトークンやリトライ不安定な値を `ext` 内に置いてはなりません（MUST NOT）。** `ext` は正準ペイロードの一部です。リトライ間で変わる値は、バイヤーの意図が変わっていなくても `IDEMPOTENCY_CONFLICT` をトリガーします。ローテーションする認証情報は上記の除外リストフィールドに、バイヤー側のトレースデータは `context` に属します。セラーはケイパビリティ、設定、拡張を介して除外リストを拡張してはなりません（MUST NOT）— リストは本スペックで固定され、そこでのドリフトはエコシステム全体でリトライセーフティ保証を黙って弱めます。**除外リストへの将来の追加はペイロード等価性への破壊的変更です**（`ext` に今除外される値を入れたバイヤーは、以前は別個だったリトライが互いに重複排除し始めるのを見る）ので、リストはマイグレーションノート付きのメジャーバージョンバンプでのみ成長します。追加を提案する新しい PR は、特定のバイヤーがたまたまローテーションしたというだけでなく、なぜそのフィールドがセマンティックにリトライ契約の外にあるかを示さなければなりません（MUST）。

**リファレンス実装**: `SHA-256(JCS(payload - excluded_fields))`。

* TypeScript / JavaScript: [`@truestamp/canonify`](https://www.npmjs.com/package/@truestamp/canonify) または [`canonicalize`](https://www.npmjs.com/package/canonicalize)
* Python: [`pyjcs`](https://pypi.org/project/pyjcs/) または [RFC 8785 appendix](https://www.rfc-editor.org/rfc/rfc8785) のリファレンス実装
* Go: [`gowebpki/jcs`](https://github.com/gowebpki/jcs)
* Rust: [`serde_jcs`](https://crates.io/crates/serde_jcs)

AdCP SDK ミドルウェアは JCS 正準化を出荷するので、セラーは独自実装する必要がありません。独自の正準形を作ることは「私のマシンでは動く」冪等性バグの一般的な原因です — JCS はそれを避けるよう精密に規定されています。

#### サーバー側ツールラッパーのコンフォーマンス

バイヤー SDK はエンベロープレベルのフィールド（`idempotency_key`, `context_id`, `context`, `governance_context`, `push_notification_config`）を**すべての AdCP ツール呼び出しで一律に**送ります — バイヤーはツールごとに、セラーのラッパーがどのエンベロープフィールドをたまたま宣言するかを知り得ません。サーバーは、ツールパラメーターに到着するがツールのパラメータースキーマで宣言されていないエンベロープレベルフィールドを許容しなければなりません（MUST）。具体的には:

* **`idempotency_key`** はすべての AdCP タスクリクエストで必須です（上記ルール 1 を参照 — 読み取りも変更系も）。ツールラッパーはそれを受け入れなければなりません（MUST）。冪等性層がルール 2-9 に従ってルーティングします。フィールドを `unexpected_keyword_argument`（FastMCP/Pydantic の厳格なシグネチャ）で拒否するラッパーは非コンフォーマントです。
* **`context_id`, `context`, `push_notification_config`, `governance_context`** は読み取りを含むすべてのツールで受け入れられなければなりません（MUST）。あるフィールドを消費しないツールはそれを無視しなければならず（MUST）、エンベロープフィールドが存在するという理由で呼び出しを拒否してはなりません（MUST NOT）。

これは、すべての公開 AdCP リクエストスキーマが宣言する `additionalProperties: true` デフォルトのサーバー側対応物です。スキーマ自身の `additionalProperties` 宣言と矛盾する形でサーバー側バリデーターを設定することはコンフォーマンス違反です。一般的なサーバー実装の罠:

* **厳格なシグネチャの FastMCP / Pydantic** — `def get_products(brief: str)` と宣言されたツールラッパーは、バイヤーが同じ params オブジェクト内に `idempotency_key` を送ると `unexpected_keyword_argument` を送出します。修正: `idempotency_key: str | None = None`（および他のエンベロープフィールド）を受け入れて無視する任意パラメーターとして宣言するか、`**kwargs` catch-all を使って未知のキーを破棄します。Pydantic-on-input は `Extra.allow` または `model_config = ConfigDict(extra='allow')` を使います。
* **`.strict()` 付きの Zod / valibot** はインバウンドリクエストスキーマで同じ理由で未知のキーを拒否します。入力スキーマで `.strict()` を外すか、passthrough バリアントで合成します。
* **codegen ツールが `additionalProperties: false` を注入した OpenAPI 生成サーバースタブ** — 生成された入力スキーマがスペックの `additionalProperties: true` デフォルトをミラーすることを検証します。一部のジェネレーターはモデル発行時にデフォルトを反転させます。

ワイヤーレベルの不変条件は: バイヤー SDK は同じエンベロープフィールドセットをすべてのセラーのすべての AdCP ツールに送れなければならず（MUST）、エンベロープフィールドで拒否するセラーはプロトコルが約束するクロスセラーの可搬性を壊します。このルールは 3.1+ で規範的です。エンベロープフィールドを拒否する既存のラッパーは次のメンテナンスバンプで非コンフォーマントです。

参照: このルールは、[`runner-output-contract.yaml` > `response_schema_validator_semantics`](https://github.com/adcontextprotocol/adcp/blob/main/static/compliance/source/universal/runner-output-contract.yaml) でレスポンス側バリデーター向けにすでに確立されたバリデーターごとのパターンを一般化します — 両ルールは同じ原則（「バリデーター設定はスキーマ自身の `additionalProperties` 宣言と矛盾してはならない」）をワイヤーの両端で表現します。

#### レスポンスレベルのリプレイインジケーター

プロトコルエンベロープは、冪等性キャッシュを介して解決された任意のリクエストへのレスポンスにトップレベルの `replayed` ブール値を運びます:

```json theme={null}
{
  "status": "completed",
  "replayed": true,
  "timestamp": "2026-04-18T14:35:00Z",
  "payload": {
    "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X"
  }
}
```

`replayed` はセラーの冪等性層がレスポンス時に生成し、キャッシュには保存されません。新規実行では `false`（または省略 — バイヤーは省略を `false` として扱わなければならない、MUST）。キャッシュされたリプレイでは `true`。内側の `payload` は元の成功実行で保存されたものとバイト単位で同じです。エンベロープフィールド（`timestamp`, `context_id` など）は異なることがあります — それらはキャッシュされたものではなく現在のレスポンスを記述します。

バイヤーは `replayed` を次に使います:

* **エージェントの副作用抑制** — 人間が見る前にレスポンスデータに作用するエージェント（通知、ダウンストリームツール呼び出し、メモリ書き込み）は、リトライで再発行しないよう `replayed` を確認しなければなりません（MUST）。「キャンペーン作成！」通知、LLM メモリ挿入、ダウンストリームエージェント呼び出しは、まさに黙ったリプレイが壊すものです。
* **副作用の不変条件** — exactly-once イベントセマンティクスを期待するダウンストリームシステムは、レスポンスを新しいイベントとして扱う前に `replayed` を読みます。
* **請求リコンシリエーション** — 「今月 N 個のバイを処理」は `replayed: false` のみをカウントします。
* **ロギング** — 「キャッシュを返してリトライが成功」を「リトライが新しい実行をトリガー」から区別（後者は通常リプレイウィンドウやキー管理のバグを示します）。
* **状態機械ルーティング** — キャッシュされた `payload` の状態追跡フィールド（例: リプレイされた `create_media_buy` の `status: pending_creatives`）は、現在状態の読み取りではなく履歴スナップショットです（セラールール 2 とバイヤーの義務下の「リプレイレスポンスは履歴スナップショット」を参照）。バイヤーはいかなる状態依存アクションの前にリソースの読み取りエンドポイントを介して再読み取りしなければなりません（MUST）。

#### IDEMPOTENCY\_CONFLICT レスポンス形状

標準の AdCP エラーエンベロープ。エラーボディ:

* `code: "IDEMPOTENCY_CONFLICT"` と人間可読な `message` を含めなければなりません（MUST）
* キャッシュされたレスポンス、元のペイロード、正準形の diff、それらから導出された指紋を含めてはなりません（MUST NOT）。`field` json-pointer ヒントは無害に見えますがスキーマ形状を明かします（例: `/packages/0/budget` は攻撃者に、被害者のペイロードが最初のパッケージに予算を持っていたと伝えます）。セラーは発行してはなりません（MUST NOT）。リトライをデバッグする正当なバイヤーは自身の 2 つのペイロードを diff できます — 両方を持っています。

```json theme={null}
{
  "errors": [
    {
      "code": "IDEMPOTENCY_CONFLICT",
      "message": "idempotency_key was used with a different payload within the replay window. Either resend the exact original payload (to return the cached response) or generate a fresh UUID v4 to submit this new payload.",
      "recovery": "correctable"
    }
  ],
  "context": { "correlation_id": "..." }
}
```

キャッシュされた状態を漏らすことは、キー再利用を読み取りオラクルに変えます。被害者のキーを推測または盗んだ攻撃者は、さもなくばそれを探ってペイロード構造を推論できます。エラーボディはコードのみを露出します。

#### SI send\_message の冪等性モデル

`si_send_message` は、会話ターンがセッション状態を進めるため、他の変更よりも狭いスコープを必要とします。キーは `(authenticated_agent, account_id, session_id, idempotency_key)` にスコープされます。

* **TTL 内のターン N のリトライはターン N のキャッシュされたレスポンスを返します**。ターン N+1 がその後受け入れられていても。冪等性はあなたがしたことを返し、セッションが何であるかを巻き戻しません。バイヤーのリトライは「私のメッセージは通ったか」を尋ねています — 答えは依然「はい、これが返ってきたものです」です。
* **新しい `idempotency_key` を持つ新しい `si_send_message` は新しいターン**で、現在のセッション状態に対して処理されます。バイヤーは HTTP 試行ごとではなく論理ターンごとに新しいキーを生成しなければなりません（MUST）。
* **セラーがセッション状態をターン N を超えて進め、キャッシュされたレスポンスをバイト単位で再現できない場合**（例: セッションがストレージのためにプルーニングされた）、セラーは再構築するのではなく `SESSION_NOT_FOUND` または `IDEMPOTENCY_EXPIRED` を返してもよい（MAY）。セッションタイムアウトをはるかに過ぎてリトライするバイヤーはこれを予期すべきです。

#### バイヤーの義務

バイヤーは `(seller, request)` ペアごとに一意の `idempotency_key` を生成しなければなりません（MUST）。同じキーをセラーをまたいで再利用すると、共謀するセラーが同じバイヤーからのリクエストを相関できます。各リクエストに新しい UUID v4 を使います。ネットワークエラー後のリトライでは、バイヤーは全く同じペイロードを同じキーで再送しなければなりません（MUST）— どちらかを変えると at-most-once セマンティクスが壊れます。特に、バイヤーは同じキーでのリトライ間で `push_notification_config.url` を変えてはなりません（MUST NOT）。URL は正準ハッシュの一部で、それをローテーションすると `IDEMPOTENCY_CONFLICT` をトリガーします。Webhook 設定を変えるときはキーをローテーションします。

**ネットワークリトライ vs エージェント再計画 vs ポーリング / 状態再読み取り。** 似ているが異なる処理が必要な 3 つのケース:

* **ネットワークリトライ** — ソケットタイムアウト、5xx、一時的失敗。バイヤーは*同じ意図*を持ち*同じバイト*を送った — そしてそれらを*同じキー*で再送しなければなりません（MUST）。これが idempotency\_key の存在理由です。
* **エージェント再計画** — バイヤーは、プランナーが再実行され（プロンプト再実行、ツール出力変化、ポリシー再評価）*異なるペイロード*を生成したエージェントです。意図が変わりました。エージェントは*新しいキー*を生成し、以前のリクエストを放棄されたものとして扱わなければなりません（MUST）。以前のキーを異なる正準ペイロードで再利用すると `IDEMPOTENCY_CONFLICT` を返し、これはセラーが正しくエージェントに「あなたはリトライしていない、新しいことをしている」と伝えるものです。
* **ポーリング / 状態再読み取り** — `get_products(brief)`, `list_creatives`, `list_accounts` を間隔でポーリングするダッシュボード。変更後に新しい状態をフェッチするため `get_media_buys` を読むバイヤーエージェント。任意の「時刻 T の現在状態をください」呼び出し。バイヤーは呼び出しごとに新しい `idempotency_key` を生成しなければなりません（MUST）。以前のポーリングのキーを再利用すると、キャッシュされたスナップショットを（`replay_ttl_seconds` まで）リプレイし、黙って古いデータを返します — まさにキャッシュが変更系で防ぐ失敗モードです。このルールは下記の [リプレイレスポンスは履歴スナップショット](#replay-responses-are-historical-snapshots) パターンの再読み取りステップも規定します: 「現在状態の再読み取り」呼び出しは新しいキーを運ばなければならず（MUST）、決して状態を読んでいる変更のキーを使いません。

疑わしいときは、バイヤーの意図が\*\*「以前と同じ答えをください」**（ネットワークリトライ — キーを再利用）か**「現在の答えをください」**（ポーリング / 状態再読み取り — 新しいキーを生成）か**「この新しいことをして」\*\*（エージェント再計画 — 新しいキーを生成）かを尋ねます。リクエストを構築するために LLM をループするエージェンティッククライアントは、ネットワークリトライケースのために最初の送信時にシリアライズされたバイトをキーと共に凍結・キャッシュすべきです（SHOULD）。プランナーが再実行で少し違うものを生成しても、リトライが同一のペイロードを送るように。

**ブートストラップ切り出し — `get_adcp_capabilities`。** ディスカバリー呼び出し自体はこのセクションのルール 1-9 から免除されます。`get_adcp_capabilities` は、バイヤーがセラーが `adcp.idempotency.replay_ttl_seconds` を宣言するかを学ぶ方法なので、ディスカバリー呼び出しに対するフェイルクローズルールはブートストラップをデッドロックさせます。バイヤーは `get_adcp_capabilities` で `idempotency_key` を省略してもよく（MAY）、セラーはそれなしで呼び出しを受け入れなければなりません（MUST）。`get_adcp_capabilities` で `idempotency_key` を送るバイヤー（例: フィールドを一律に含める SDK）は標準のキャッシュ動作を得ます — が、ディスカバリー呼び出しは状態を運ばず、リプレイは無害です。他のすべての AdCP タスクリクエストはルール 1-9 の対象のままです。下記のフェイルクローズ義務はケイパビリティフェッチが完了すると適用されます。

**セラーのケイパビリティ宣言が欠落している場合。** `get_adcp_capabilities` レスポンスが `adcp.idempotency.replay_ttl_seconds` を省略するセラーは非準拠です。ケイパビリティフェッチが成功した後、クライアント SDK はそのセラーに対する後続のすべての AdCP タスクリクエストでフェイルクローズしなければなりません（MUST）— エラーを発生させ、デフォルトを仮定しない — バイヤーが黙ったダブルブッキングの後ではなく即座に非準拠を学ぶように。フェイルクローズルールは、`idempotency_key` が一律に必須になった今、すべての AdCP タスクリクエスト（`get_adcp_capabilities` 自体を除く）に適用されます — 純粋な読み取りとして解決する呼び出しを含みます。バイヤーは呼び出し時に多相タスク（`get_products` brief vs refine+finalize vs 非同期 Submitted）が読み取りか変更のどちらに解決するか予測できず、TTL 宣言の欠落はセラーがどのモードでもリトライに安全でないことを意味するからです。

**セラー発行のエラーコードのデコード。** セラーは、バイヤーのピン留め語彙が認識しないかもしれないエラーコード（`IDEMPOTENCY_CONFLICT`, `IDEMPOTENCY_EXPIRED`, `IDEMPOTENCY_IN_FLIGHT`, `INVALID_REQUEST`、または後のマイナーバージョンで追加されたコード）を返してもよい（MAY）。受信者は [Forward-compatible decoding（規範的）](/docs/building/by-layer/L3/error-handling#forward-compatible-decoding-normative) に従ってこれらをデコードしなければなりません（MUST）— 復旧分類のため `error.recovery` を読み、`recovery` が不在のとき `transient` をデフォルトとし、コード値が馴染みないという理由でレスポンスを決して拒否しない。`transient` 分類エラーのリトライセマンティクスは [§ Retry Logic](/docs/building/by-layer/L3/error-handling#retry-logic)（`maxRetries` とジッター付き指数バックオフ）で制限されます — バイヤーは `transient` デフォルトで無限にループしてはなりません（MUST NOT）。

**リプレイレスポンスは履歴スナップショット。** `replayed: true` を運ぶレスポンスは元の初回呼び出しレスポンスとバイト等価です（セラールール 2）— その中の状態追跡フィールドは初回呼び出し時のリソース状態を反映し、リソースの現在状態では**ありません**。リプレイされた `create_media_buy` レスポンスから `status: pending_creatives` を読み、実際には何時間も `canceled` にあるリソースに `update_media_buy(canceled: true)` を呼ぶバイヤーは、`NOT_CANCELLABLE` エラーと状態機械バグを表面化します。現在状態を必要とするバイヤーはリソースの読み取りエンドポイント — メディアバイには `get_media_buys`、アカウントには `list_accounts`、クリエイティブには `list_creatives`、シグナルには `get_signals`、他のリソースには同等物 — を参照しなければなりません（MUST）。`replayed: true` は、いかなる状態依存の決定の前に新しい読み取りが必要という明示的なシグナルです。SDK はフラグを透過的にアンラップするのではなく呼び出し元コードに表面化すべきです（SHOULD）。エージェンティックバイヤーは、次のアクションがリソース状態に依存するいかなるプランニングステップについても `replayed: true` を停止シグナルとして扱わなければならず（MUST）、続行前に再読み取りしなければなりません（MUST）。

**再読み取りは新しい `idempotency_key` を運ばなければなりません（MUST）。** 状態を再読み取りしている変更のキーを再利用すると、`IDEMPOTENCY_CONFLICT` を返す（読み取りペイロードが変更ペイロードと異なる場合 — ほぼ常に真）か、さらに悪いことにキャッシュされた変更レスポンス自体を返します（ペイロードがたまたま一致する場合）。*以前の読み取り*のキーを再利用すると、その以前の読み取りのキャッシュされたスナップショットを返します — まさにこのルールが防ぐ古い状態の失敗モードです。状態再読み取りは上記のポーリング / 状態再読み取りケースに該当します。呼び出しごとに新しいキーを生成します。

**永続化されたキーの TTL 境界。** 一部のバイヤーは、プロセス再起動や夜間リコンサイル後のリトライも重複排除するよう、`idempotency_key` を自身のオブジェクト（例: バイヤーの DB の `campaign.pending_idempotency_key`）と共に永続化します。これは**セラーの宣言された `replay_ttl_seconds` 内でのみ**機能します。TTL を超えると、セラーはリトライを `IDEMPOTENCY_EXPIRED` で拒否する（良い）か、キャッシュが退避されていれば新しいリクエストとして扱います（黙ったダブルブッキング — このフィールドが防ぐ失敗モード）。TTL を過ぎてリトライするバイヤーは、再送前に自然キーチェック（例: `context.internal_campaign_id` で `get_media_buys` をクエリ）にフォールバックしなければなりません（MUST）。`idempotency_key` はリプレイウィンドウ内での at-most-once 実行を保証し、永遠にではありません。セラーの TTL より長いリトライホライズンを持つキューベースのリトライシステムとワークフローエンジンはこれを中心に設計されなければなりません（MUST）— 自然キー再チェックなしに数日後にリプレイするデッドレターキューにキーを入れないでください。

**キーはセキュリティ機微。** `idempotency_key` は TTL 内の秘密のケイパビリティトークンです — それを保持し元のペイロードを知る者は誰でもそれをリプレイしてキャッシュされたレスポンスを読めます。キーをセッショントークンのように扱います: 完全な形でログしない、URL に埋め込まない、エージェント間で共有しない。相関が必要なら prefix のみ（UUID の最初の 8 文字）でログします。`pending_idempotency_key` を保存時に永続化するバイヤー（例: バイヤーの DB のキャンペーン行と共に）は、bearer トークンに使うのと同じ制御でそれを暗号化しなければならず（MUST）、露出ウィンドウを最小化するため成功確認後にキーをパージすべきです（SHOULD）。

**セラーはキャッシュ層を保存時に暗号化しなければなりません（MUST）。** ユニバーサル冪等性（3.1+）の下では、キャッシュは 3.0.x で保持した書き込みレシートに加えて読み取りツールレスポンス（`get_products`, `list_accounts`, `list_creatives`, `get_signals` など）を保持します。それらの読み取りレスポンスは、セラーの基盤リソースストアと同じ機微度でアカウントスコープのデータ — ブランドドメイン、アカウント名、プロダクト配分、シグナル参照 — を運びます。セラーは、キャッシュされたデータが読まれた元のリソースストアに使うのと同じ制御で冪等性キャッシュに保存時暗号化を適用しなければならず（MUST）、キャッシュをデータ保存時制御から免除される一時的なリトライレシートストアとして扱ってはならず（MUST NOT）、設定ミスのクエリが兄弟テナントのキャッシュされた読み取りレスポンスを引き出せないよう、ストレージ層で（アプリ層だけでなく）キャッシュ読み取りを `(authenticated_agent, account_id)` でスコープしなければなりません（MUST）。

**キーは推測不能でなければなりません（MUST）。** スキーマは `^[A-Za-z0-9_.:-]{16,255}$` を強制し、バイヤーは UUID v4（約 122 ビットのエントロピー）または同等の CSPRNG 生成値を使わなければなりません（MUST）。`retry-001` や単調カウンターのような低エントロピーキーはキャッシュを列挙可能な面に変えます: 攻撃者はキー空間を歩き、それぞれをターゲットエージェントに対してテストできます。セラーは、認証済みエージェントが個別に信頼されないとき、基本的なエントロピーチェックに失敗するキー（例: すべてゼロ、繰り返し文字、短い ASCII 単語）を `INVALID_REQUEST` で拒否すべきです（SHOULD）。

**3 状態レスポンス（`success` / `IDEMPOTENCY_CONFLICT` / `IDEMPOTENCY_EXPIRED`）は冪等性キーの存在オラクルです。** 候補キーを保持する攻撃者はそれを探れます: `success` は見たことがない、`IDEMPOTENCY_CONFLICT` は異なるペイロードでライブ、`IDEMPOTENCY_EXPIRED` は以前使われた、を意味します。上記の `(agent, account)` ごとのスコープが主要な防御です — エージェント A として認証された攻撃者はエージェント B のキーを探れず、アカウント A にスコープされた呼び出し元は共有エージェント認証情報の下でもアカウント B のキーを探れません。推測不能なキーが 2 次防御です — 被害者のキーを推測できない攻撃者はオラクルを有用に探れません。セラーはスコープ境界をまたいで、または未認証の呼び出し元に `IDEMPOTENCY_EXPIRED` を表面化してはなりません（MUST NOT）。セラーは冪等性層で「キーが存在する」と「キーが存在しない」ルックアップの間の区別可能なタイミングも避けるべきです（SHOULD）。負のパスでの定数時間フロアは、エラーコードオラクルなしでも持続するサイドチャネルを閉じます。

**SI セッションスコープ。** `si_send_message` ではキーは `(authenticated_agent, account_id, session_id, idempotency_key)` にスコープされます。したがって `session_id` はオラクル面の一部です: セッション ID が推測可能なら、1 つのキーを盗んだ攻撃者は多くのセッションに対してそれを探れます。SI セラーは 122 ビット以上のエントロピーを持つ CSPRNG（UUID v4 または同等）を使ってサーバー側で `session_id` を生成しなければならず（MUST）、別のエージェントに観測可能な何か（リクエストシーケンス番号、ユーザーハンドル、タイムスタンプ）から導出してはなりません（MUST NOT）。異なる `session_id` で送られた同じ idempotency\_key は異なるスコープタプルです — 常に新しいリクエストで、決して競合ではありません。

**キャッシュスコープ安全性のための `account_id` エントロピー。** `account_id` はすべての冪等性スコープタプルの一部なので、オラクル面の一部でもあります: 盗んだ冪等性キーを持つエージェント A として認証された攻撃者は、それを候補アカウント ID に対して探って A の認可セット内のアカウントを列挙したり、A がこれまで作用したアカウントを学んだりできます。アカウント ID が短い連番またはセマンティック値（`acct_123`, `nike-us`）のとき、これは実際の列挙チャネルです。サーバー割り当てのアカウント ID を発行するセラーは、冪等性キャッシュスコープに参加する任意のアカウント ID に推測不能な値（UUID v4 / ULID、122 ビット以上のエントロピー）を使わなければなりません（MUST）。バイヤー宣言アカウントモデル（自然キー `{brand, operator}`）の下で運用するセラーは、それをキャッシュスコープコンポーネントとして使う前に自然キーをセラーローカルソルトでハッシュしなければなりません（MUST）— 自然キーは設計上公開であり、オラクル防御として直接使えません。

```javascript theme={null}
import { canonicalize } from "@truestamp/canonify"; // RFC 8785 JCS
import { createHash } from "node:crypto";

const EXCLUDED_FROM_HASH = new Set([
  "idempotency_key",
  "context",
  "governance_context",
]);

function payloadHash(request) {
  const filtered = Object.fromEntries(
    Object.entries(request).filter(([k]) => !EXCLUDED_FROM_HASH.has(k)),
  );
  // If push_notification_config.authentication.credentials rotates, exclude it too
  if (filtered.push_notification_config?.authentication) {
    const { credentials, ...auth } = filtered.push_notification_config.authentication;
    filtered.push_notification_config = {
      ...filtered.push_notification_config,
      authentication: auth,
    };
  }
  return createHash("sha256").update(canonicalize(filtered)).digest("hex");
}

async function createMediaBuy(request, envelope) {
  if (!request.idempotency_key) {
    throw new InvalidRequestError("idempotency_key is required");
  }

  const requestHash = payloadHash(request);

  const existing = await db.findByIdempotencyKey({
    agent_id: currentAgent.id,
    account_id: request.account.account_id,
    idempotency_key: request.idempotency_key,
  });

  if (existing) {
    if (existing.expires_at < new Date()) {
      throw new IdempotencyExpiredError("idempotency_key is past replay window");
    }
    if (existing.request_hash !== requestHash) {
      throw new IdempotencyConflictError("idempotency_key reused with a different payload");
    }
    // Return the stored INNER payload; replayed: true is injected by the envelope layer
    envelope.replayed = true;
    return existing.response;
  }

  return db.transaction(async (tx) => {
    const response = await processMediaBuy(tx, request);
    // Cache ONLY on success, and cache only the inner response payload
    await tx.idempotencyKeys.insert({
      agent_id: currentAgent.id,
      account_id: request.account.account_id,
      key: request.idempotency_key,
      request_hash: requestHash,
      response,
      expires_at: new Date(Date.now() + TTL_SECONDS * 1000),
    });
    envelope.replayed = false;
    return response;
  });
}
```

#### 自然キー冪等性は代替にならない

アップサート型タスク（`sync_accounts`, `sync_audiences`, `sync_catalogs`, `sync_event_sources`, `sync_governance`, `sync_plans`）はすでにリソースレベルで重複排除します — 同じ `account_id` または `audience_id` を持つ 2 つの呼び出しは 2 つではなく 1 つの行を生成します。それが**リソース冪等性**です。

`idempotency_key` はより厳格なものを保証します: **エンベロープ冪等性**。リクエスト全体 — その副作用を含む — が最大 1 回実行されます。キーなしで同じ sync エンベロープをリトライすると、リソース行が同一になっても、オンボーディング Webhook を 2 回発火したり、重複した監査ログエントリを発行したり、ピクセルエンドポイントを二重プロビジョニングしたりし得ます。キーがリトライを本当に安全にするものです。

スペックでの唯一の例外は `si_terminate_session` です: `session_id` と「terminate」動詞は完全に冪等 — すでに終了したセッションへの 2 番目の呼び出しは新しい副作用なしに同じ終端状態を返す — なので、そのスキーマは `idempotency_key` を要求しません。

### 署名付きガバナンスコンテキスト

`governance_context` は信頼境界をまたぎます — ガバナンスエージェントからバイヤー、セラー、そして戻り、最終的には元のトランザクションが閉じてからずっと後に承認を検証する必要があるかもしれない監査人や規制当局へ。AdCP 3.0 は、いかなる当事者も発行者に召喚状を出さずに真正性、バインディング、リプレイを検証できるよう、値の形式をガバナンスエージェントが署名したコンパクトな JWS に厳格化します。

**役割:**

* **ガバナンスエージェント**がトークンに署名します。署名する唯一の当事者です。
* **バイヤー**はガバナンスエージェントから受け取ったトークンをプロトコルエンベロープに添付し、セラーに転送します。バイヤーはトークンを構築・変更・再署名してはなりません（MUST NOT）。バイヤーは自身の監査記録のため `jti` と `check_id` を保持すべきです（SHOULD）。
* **セラー**はトークンを受け取ったまま永続化し、後続のすべてのガバナンス呼び出しにそのまま含めます。検証を実装するセラーは、トークンに基づいて行動する前に下記のチェックリストに従って検証しなければなりません（MUST）。検証をまだ実装していないセラーも、ダウンストリームの検証可能な当事者（監査人、規制当局）が後でそれに基づいて行動できるよう、トークンを変更せずに永続化・転送しなければなりません（MUST）。
* **監査人と規制当局**はガバナンスエージェントの公開鍵を使って独立に検証します — これが署名形式が提供するために存在するアカウンタビリティ特性です。

同じ文字列はガバナンスライフサイクルの主要な相関キーでもあります。ガバナンスエージェントは自身のトークンをデコードして内部状態（バイヤー相関 ID、ポリシー決定ログなど）をルックアップします — セラーとバイヤーはペイロードを解析する必要は決してありません。

#### スコープと依存関係

* **スコープ内（3.0）**: バイサイドガバナンス。`governance_context` トークンは、AdCP タスク（`create_media_buy`, `acquire_rights`, `activate_signal`, `creative_services`）を介してなされる支出コミットメントを認可します。独自のコンプライアンスポリシーを実行するセラー（例: CTV 政治広告ルール、パブリッシャーブランドセーフティゲート）は、それらを独自のガバナンスワークフロー上の `conditions` レスポンスで表現します。このプロファイルの下では署名付きトークンを発行しません。
* **スコープ外（3.0）**: セラーサイドのガバナンス当局。将来の RFC が `adagents.json` を介して宣言されるセラーサイドの署名付き決定をカバーするようこのプロファイルを拡張するかもしれません。
* **スコープ外（永久）**: OpenRTB 入札ストリーム。ガバナンスの証明は AdCP メディアバイ境界で終わります。署名付き証明をインプレッションごとの入札リクエストに通すことは運用上実現不可能（1 トークン、多数の受信者、ブロードキャストファンアウト）で不要（支出認可はインプレッションごとではなくメディアバイ時に発生）です。

**トランスポート署名（#2307）への依存**: このプロファイルのアンチスプーフ特性は、セラーがトークンの `iss` クレームとは独立にバイヤードメインを確立できることに依存します — 下記の [Buyer identity resolution](#buyer-identity-resolution) を参照。#2307 なしの 3.0 では、セラーはバイヤーアイデンティティを確立するため mTLS または事前プロビジョニングされたバイヤー API キーのいずれかを使わなければなりません（MUST）。リクエストの bearer トークンだけを brand.json 解決へのアイデンティティ入力として扱うことは循環的で、スプーフィングを防ぎません。3.1 は規範的に #2307 スタイルの署名付きリクエストを要求します。

#### AdCP JWS プロファイル

このプロファイルは `governance_context`（#2306）と、スタンドアロントークンとして署名される将来の任意の AdCP アーティファクトに適用されます。トランスポート層リクエスト署名（#2307）は RFC 9421 HTTP Signatures を使いますが、ここで説明する JWKS ディスカバリーを共有します。ガバナンス署名鍵を #2307 トランスポート署名鍵として使ってはなりません（MUST NOT）— JWKS エンドポイントは共有ですが、各鍵エントリは `"key_ops": ["verify"]` と `"use": "sig"` を宣言し、別個の `kid` を占めなければなりません（MUST）。検証者は、目的をまたぐ鍵再利用を防ぐため key-ops 分離を強制しなければなりません（MUST）。

**ヘッダー**

* `alg`: サーバー側ランタイムでは `EdDSA`（Ed25519）を RECOMMENDED。Ed25519 が明示的なランタイム設定を要するエッジランタイム（Cloudflare Workers、Vercel Edge、Deno Deploy）では `ES256`（ECDSA P-256）を RECOMMENDED。検証者は `none`、`HS*`、2048 ビット未満の任意の `RS*` バリアントを拒否しなければなりません（MUST）。検証者はトークンヘッダー上で許可リストを強制しなければならず（MUST）、ライブラリのデフォルトのみに頼ってはなりません（MUST NOT）。
* `kid`: REQUIRED。発行者の JWKS 内の署名鍵を識別します。
* `typ`: REQUIRED。正確に `adcp-gov+jws` でなければなりません（MUST、バイト単位一致。検証者は RFC 6838 §4.2.8 に従い `+jws` 構造化サフィックスを正規化・除去してはならない、MUST NOT）。型付きヘッダーは、ガバナンス署名鍵が別目的の汎用 JWT を検証するよう騙されるのを防ぎます。
* `crit`: `crit` リストのクレームが存在する場合 REQUIRED。RFC 7515 §4.1.11 に従い、`crit` は検証者が理解しなければならないヘッダー/クレーム名の配列です。検証者は `crit` の名前が認識されない場合トークンを拒否しなければなりません（MUST）。ガバナンスエージェントは、省略または誤解釈が認可セマンティクスを変える任意のクレーム（例: 将来の `budget_cap` クレーム）を `crit` にリストしなければなりません（MUST）。これはプロファイルが後のバージョンでクレームを追加するときの黙ったダウングレード攻撃を防ぎます。

**クレーム**

| Claim                  | Required    | Description                                                                                                                                                                                                                                                                |
| ---------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `iss`                  | Yes         | ガバナンスエージェント識別子。バイヤーの brand.json のガバナンス型エントリの `url` とバイト単位（パスコンポーネントを含む）で一致する HTTPS URL でなければならない。マルチテナント SaaS ガバナンスエージェント（例: `https://gov.vendor.com/tenant/acme`）が同じオリジンを共有する兄弟テナントにスプーフされないよう、パスレベル一致が必要。                                                                |
| `sub`                  | Yes         | トークンが認可する `plan_id`。注: ここで `sub` はユーザーや認証済みエージェントではなくリソース識別子として使われる。`sub` をユーザー ID としてログする実装はこれに注意すべき。                                                                                                                                                                     |
| `plan_hash`            | Yes         | 証明を評価されたプラン状態に監査層でバインド。セラー検証チェックリストの一部ではない — セラーは不透明なカーゴとして扱う。セマンティクス、正準化、検証パスは [Plan binding and audit](/docs/governance/campaign/specification#plan-binding-and-audit) で定義。                                                                                              |
| `aud`                  | Yes         | ターゲットセラー識別子。購入されるプロパティについてこのセラーを認可したセラーの `adagents.json` エントリの正確な URL 文字列（スキーム、ホスト、ポート、パスを含むバイト単位）でなければならない。大文字小文字を区別、パスプレフィックス一致なし。バイヤーが複数セラーを評価するインテントトークンでは、バイヤーはターゲットセラーごとに 1 トークンを要求しなければならない（プライバシートレードオフは [Intent-phase disclosure](#intent-phase-disclosure) を参照）。 |
| `iat`                  | Yes         | 発行時タイムスタンプ（エポックからの秒）。                                                                                                                                                                                                                                                      |
| `nbf`                  | No          | Not-before タイムスタンプ。存在する場合、検証者は now \< nbf（±60 秒スキュー）なら拒否しなければならない。                                                                                                                                                                                                         |
| `exp`                  | Yes         | 有効期限タイムスタンプ。インテントトークンは 15 分以内に期限切れになるべき。実行フェーズトークン（`purchase`, `modification`, `delivery`）は 30 日以内に期限切れになければならない。ガバナンスエージェントは各ライフサイクルチェックで新しいトークンを発行して長いライフサイクルをリフレッシュする。                                                                                                  |
| `jti`                  | Yes         | 一意のトークン識別子。セラーのリプレイ検出と監査人の相関に使われる。RECOMMENDED 形式: 時間順序付けのため UUID v7 または ULID。                                                                                                                                                                                              |
| `phase`                | Yes         | `intent`（セラー前）、`purchase`、`modification`、`delivery`。このトークンが認可するガバナンスチェックフェーズに一致。セラーが実行するオペレーションが必要なフェーズを決定: `create_media_buy` → `purchase`、`update_media_buy` → `modification`、デリバリーレポートコールバック → `delivery`。                                                              |
| `caller`               | Yes         | このトークンを生成したガバナンスチェックを要求した当事者の URL。インテントフェーズではオーケストレーター/バイヤー、実行フェーズでは通常セラー自身（コールバックはセラーを caller として到着）。                                                                                                                                                                     |
| `check_id`             | Yes         | この決定に対するガバナンスエージェントの `check_id`。`report_plan_outcome` と `get_plan_audit_logs` に相関。                                                                                                                                                                                         |
| `media_buy_id`         | Conditional | セラー割り当てのメディアバイ ID。`purchase`、`modification`、`delivery` フェーズトークンで存在しなければならない。`intent` フェーズトークンでは null または不在でなければならない。                                                                                                                                                       |
| `policy_decisions`     | No          | `{ policy_id, outcome }` エントリのコンパクトな配列（`confidence` を含み得る）。セラーに可視。ガバナンスエージェントはプライバシー機微なデプロイでこれを省略し（SHOULD、[Privacy considerations](#privacy-considerations) を参照）、代わりに `policy_decision_hash` を使うべき。                                                                        |
| `policy_decision_hash` | No          | 正準化された決定ログの SHA-256 ハッシュ、hex エンコード。存在する場合、セラーは不透明な完全性アンカーとして扱う。完全なログは `audit_log_pointer` を介して監査人が取得可能。ガバナンスエージェントは `policy_decisions` または `policy_decision_hash` のいずれかを含めなければならない（両方も許可）。                                                                                 |
| `audit_log_pointer`    | No          | 完全な決定証拠のため `get_plan_audit_logs` が消費可能な HTTPS URL。存在する場合、監査人はポインターを使って完全なログをフェッチできる。アクセス制御はガバナンスエージェントが管理。                                                                                                                                                                 |
| `status`               | No          | 任意の前方互換フック。存在する場合、将来の IETF JWT Status List メカニズム（draft-ietf-oauth-status-list）に準拠する JSON オブジェクトでなければならない。`status` を理解しない検証者は、それが `crit` に現れない限り、存在だけで拒否してはならない。                                                                                                            |

**未知クレームの扱い**: 検証者は、認識しない名前のクレームを無視しなければなりません（MUST）*ただし*それらのクレーム名がトークンの `crit` ヘッダーに現れる場合を除き、その場合トークンを拒否しなければなりません（MUST）。この非対称ルール — 未知は無視、未知かつクリティカルは拒否 — が、プロファイルの将来バージョンが、まだ更新していない検証者の後方互換性を壊さずにセマンティックに意味のあるクレームを追加する方法です。

**サイズ**: `policy_decision_hash` を持つ典型的なトークンは 4096 文字のエンベロープ上限に余裕を持って収まります。実装は大きな証拠ペイロードをトークンに入れてはなりません（MUST NOT）。代わりに `audit_log_pointer` を使います。

**`plan_hash` は監査層でありワイヤー層ではない**: `plan_hash` クレームは、ガバナンスエージェント、監査人、バイヤー側コンプライアンスによるオフワイヤー検証のためにトークンが運ぶ暗号学的カーゴです。このプロファイルのセラー検証契約の一部ではなく、決して `crit` にリストされません。正準化、除外フィールド、保持ルール、テストベクターは [Plan binding and audit](/docs/governance/campaign/specification#plan-binding-and-audit)（ガバナンススペック）で規定されます。セラーは `governance_context` をそのまま永続化・転送し、`plan_hash` を検査せずに下記の 15 ステップ検証チェックリスト — 真正性、認可スコープ、鮮度 — を実行します。

#### バイヤーアイデンティティ解決

brand.json クロスチェック（検証チェックリストのステップ 13）がアンチスプーフィング制御です。それはセラーが*どのバイヤーの brand.json を参照するか*を知ることを要求します — 認証済みエージェントが誰が呼んでいるかを証明し、解決チェーンがそのエージェントを、セラーが brand.json をフェッチすべきバイヤードメインにマップします。3.0 でセラーは次のいずれかを介してバイヤードメインを確立しなければなりません（MUST）:

1. **mTLS**: バイヤーがクライアント証明書を提示。証明書の Subject/SAN がバイヤーの登録済みドメインに解決。セラーが `https://{domain}/.well-known/brand.json` をフェッチ。
2. **事前プロビジョニングされたバイヤーアイデンティティ**: オンボーディング時にセラーが発行し、セラーの記録でバイヤーのドメインにマップされた API キーまたは OAuth クライアント識別子。
3. **#2307 に従う署名付きリクエスト**（3.1 規範）: `keyid` がバイヤーの adagents スタイルエージェントレジストリのバイヤー宣言公開鍵に解決する RFC 9421 HTTP Signatures。

セラーは、リクエストの未認証フィールド（トークンの `iss`、`caller`、任意のクライアント供給ヘッダーを含む）からバイヤーアイデンティティを導出してはなりません（MUST NOT）。そうすると循環的な信頼チェーンが生じます: 攻撃者は、攻撃者制御の brand.json で宣言された攻撃者制御のガバナンスエージェントが署名したトークンを提示して「私はバイヤーです」を証明します。特に、**トークンの `iss` は、検証チェックリストのステップ 13 がそれが*認証済み*バイヤーの brand.json にガバナンス型エントリとして現れることを確認するまで未信頼の入力です** — 認証メカニズム（mTLS、API キー、署名付きリクエスト）が最初にバイヤードメインを確立し、*その*ドメインからフェッチされた brand.json のみが、どのガバナンスエージェント（`iss`）がこのバイヤーのために署名してよいかを証明すると信頼されます。

brand.json 解決は 1 リダイレクト（`authoritative_location` または `house` リダイレクトバリアント）に従って停止します。セラーはリダイレクトチェーンに従ってはなりません（MUST NOT）。

#### 鍵ディスカバリー（JWKS）

セラーと監査人は JWKS（RFC 7517）を介してガバナンスエージェントの公開鍵を解決します:

1. [Buyer identity resolution](#buyer-identity-resolution) のルールを介してバイヤードメインを確立する。
2. バイヤーの brand.json をフェッチする。`type` が `governance` で `url` がトークンの `iss` とバイト単位で等しい `agents[]` エントリを見つける。一致するエントリがなければ拒否する。
3. 宣言されていればエントリの `jwks_uri` を使う。不在の場合、`{origin of iss}/.well-known/jwks.json`（origin = RFC 6454 に従う scheme+host+port）をデフォルトとする。共有オリジンから複数バイヤーを提供するマルチテナントガバナンスエージェントは、テナント鍵素材がオリジン横断でプールされないよう、明示的なテナントごとの `jwks_uri` を宣言しなければならない（MUST）。シャーディングは分離要件だけでなくサイズ要件でもある: 各 `jwks_uri` は `MAX_JWKS_BYTES` 予算（64 KiB — 下記の検証者疑似コードを参照）の下でフェッチされ、これは JWKS 固有の上限で、汎用の 5 MB SSRF ボディ上限より意図的に厳しい。数百のテナントごとの鍵をプールする単一の JWKS は 64 KiB を超えて拒否されるので、テナントごとの `jwks_uri`（それぞれ小さな鍵セットを提供）— 1 つの集約ドキュメントではない — がスケール時のコンフォーマントなパス。
4. JWKS を HTTPS でフェッチする。
5. JWKS 内で `kid` がトークンヘッダーに一致する鍵を見つける。`kid` のキャッシュミスでは、拒否する前に JWKS を 1 回再フェッチする（無制限の再フェッチを防ぐため最小 30 秒のクールダウンを尊重）。

**JWKS キャッシュ TTL** は失効リストポーリング間隔（[Revocation](#revocation) を参照）で上限が制限されなければなりません（MUST）。長いキャッシュ TTL は失効を無効にします: 侵害された `kid` が `revoked_kids` に追加されても、セラーの JWKS キャッシュが検証のために失効した鍵をまだ提供する場合、失効チェック（ステップ 14 で独立に実行）のみが不正を捕捉します。

**SSRF 保護**: `jwks_uri` と失効リスト URL は相手方が供給します。これらの URL へのすべてのアウトバウンドフェッチは [Webhook URL validation](#webhook-url-validation-ssrf) で定義された SSRF 制御に従わなければなりません（MUST）: 非 HTTPS を拒否、予約範囲（クラウドメタデータアドレスを含む）の解決 IP を拒否、接続を検証済み IP にピン留め、リダイレクトを拒否、レスポンスサイズとタイムアウトに上限、相手方への詳細なエラーメッセージを抑制。鍵ディスカバリーで SSRF 規律のない JWS プロファイルはメタデータ流出ベクトルです。

#### セラー検証チェックリスト

リクエストをガバナンス承認済みとして扱う前に、セラーはこれらのチェックを順に実行し、最初の失敗でショートサーキットしなければなりません（MUST）:

1. コンパクト JWS をパースする。不正なら拒否。
2. ヘッダー `alg` が `none` または許可リスト（EdDSA、ES256）にない場合拒否。ライブラリのデフォルトに頼ってはならない（MUST NOT）。
3. ヘッダー `typ` が正確に `adcp-gov+jws` でない場合拒否（正規化なし）。
4. ヘッダーが `crit` 配列を含み、リストされた名前が検証者に認識されない場合拒否。
5. 上記のディスカバリールールで `iss` を JWKS に解決する。JWKS がフェッチできない（SSRF 検証後）か 1 回の再フェッチ後に `kid` が存在しない場合拒否。
6. JWKS エントリの `use` が `"sig"` で `key_ops` が `"verify"` を含むことを検証。他の用途にマークされた鍵は拒否。
7. 署名を暗号学的に検証する。
8. `aud` が関連する `adagents.json` エントリで宣言されたセラー自身の正準 URL とバイト単位で等しくない場合拒否。
9. `exp` が過去、または `iat` が 60 秒より先の未来の場合拒否（±60 秒クロックスキュー許容、両境界で対称）。`nbf` が存在する場合、`now < nbf − 60 s` なら拒否。
10. `sub` がこのトークンが添付されているガバナンス呼び出しの `plan_id` と等しくない場合拒否（プランスワップを防ぐ）。
11. `phase` がオペレーションに一致しない場合拒否: `create_media_buy` には `purchase`、`update_media_buy` には `modification`、デリバリーレポートコールバックには `delivery`、`intent` はセラー前のバイヤー側評価のみ。
12. 非インテントトークンでは、`media_buy_id` がリクエストのメディアバイ ID と等しくない場合拒否。
13. クロスチェック: トークンの `iss` はバイヤーの現在の brand.json（[Buyer identity resolution](#buyer-identity-resolution) を介して確立）にガバナンス型エージェントとして現れなければならない（MUST）。セラーは妥当な TTL（1 時間推奨）で brand.json をキャッシュし、検証失敗時にリフレッシュすべき（SHOULD）。
14. 失効リスト（[Revocation](#revocation) を参照）を確認する。`jti` ∈ `revoked_jtis` またはトークンヘッダーの `kid` ∈ `revoked_kids` なら拒否。このチェックはキャッシュミス時だけでなくすべての検証で実行される。
15. `jti` がこの `(iss, aud)` タプルで以前に見られている場合拒否。ストレージガイダンスは [Replay dedup](#replay-dedup) を参照。

15 のチェックすべてが通過した後にのみ、セラーはリクエストをガバナンス承認済みとして扱います。セラーは `plan_hash` を検証しないことに注意 — そのクレームはガバナンスエージェント / 監査人層でバインドされます（[Plan-state binding](#plan-state-binding) を参照）。

#### リプレイ重複排除

ステップ 15 はリプレイを防ぐため `jti` 値の追跡を要求します。素朴な実装 — 無制限のセット — はメモリリスクであり DoS ベクトル（攻撃者がストレージを枯渇させるため一意のトークンでセラーをフラッド）でもあります。

**スケーリング推奨**:

* 実行トークンの `exp` を 30 日で上限（ガバナンスエージェントが強制。セラーはそれより長いものを拒否）。これは重複排除ウィンドウを制限します。
* 高速パスチェックとして小さな偽陽性率（約 100 万分の 1）の `(iss, aud, jti)` でキーされたブルームフィルターを使い、ブルームフィルターヒット時のみ制限されたストア（Redis `SET jti NX EX <remaining_ttl>`、TTL クリーンアップ付き Postgres 一意インデックス）で権威的ルックアップ。
* ガバナンスエージェントは、セラーが重複排除ストアを時間ウィンドウでパーティション化して期限切れパーティションを安価にドロップできるよう、`jti` 値を時間順序付け可能な形式（UUID v7 または ULID）で発行すべき（SHOULD）。

#### 失効

exp ベースの期限切れだけでは、メディアバイのライフサイクルの間生きる実行フェーズトークンをカバーしません。ガバナンスエージェントは `{origin of iss}/.well-known/governance-revocations.json` に失効リストを公開しなければならず（MUST）、同じ JWKS の鍵を使ってリスト自体に署名しなければなりません（MUST）:

```json theme={null}
{
  "payload": "<base64url of the JSON below>",
  "signatures": [
    { "protected": "<b64url header with kid, alg, typ=adcp-gov-revocation+jws>",
      "signature": "<b64url signature>" }
  ]
}
```

ペイロード（JWS フラット化 JSON シリアライズ。コンパクト形式も許容）:

```json theme={null}
{
  "version": 1,
  "issuer": "https://gov.example.com",
  "updated": "2026-04-18T14:00:00Z",
  "next_update": "2026-04-18T14:15:00Z",
  "revoked_jtis": ["01HWZX..."],
  "revoked_kids": ["gov-2026-03"]
}
```

* `revoked_jtis` は個別の決定を無効にします（例: プランが撤回された）。失効は署名鍵に関わらずその `jti` を持つ任意のトークンに適用されます。
* `revoked_kids` はその `kid` の下で署名されたすべてのトークン（失効タイムスタンプの前後）を無効にします。発行後のトークンだけではありません。
* `issuer` はこのリストが規定するトークンの `iss` オリジンと一致しなければなりません（MUST）。共有 CDN による発行者をまたぐキャッシュ置換を防ぎます。
* リストは署名されているので、侵害された CDN や DNS オリジンが、侵害された鍵の失効を解除するために古いまたは改ざんされたリストを提供できません。

**ポーリングケイデンス**:

* セラーは `next_update` で宣言されたケイデンスでリストをポーリングしなければなりません（MUST）。
* フロア: 1 分。上限: 実行フェーズトークンを受け入れる任意のセラーで 30 分。ガバナンスエージェントは、実行フェーズトラフィックがカバーする発行者について `next_update` を 30 分より先の未来に宣言してはなりません（MUST NOT）。`next_update` 値は HTTP キャッシュヘッダーではなく JSON タイムスタンプです — 標準の HTTP キャッシュはそれを尊重しません。セラーは自分でそれをパースして守らなければなりません（MUST）。DoS 耐性より高速な鍵侵害伝播を優先するセラーはフロア付近でポーリングすべき（SHOULD）。上限は、より長い失効エンドポイント停止に耐えることと引き換えに遅い `revoked_kids` 伝播を受け入れるセラーのために存在します。
* ポーリングは、15 分以下の `exp` を持つインテントフェーズトークン（上記 JWT クレーム表からのインテントトークン `exp` 上限 — ポーリング上限とは別、数値が以前は一致していたが）では任意です。
* 不要なボディ転送を避けるため HTTP 条件付きリクエスト（`If-Modified-Since` / `ETag`）を使います。

**フェッチ失敗の安全デフォルト**: セラーが `next_update + grace`（grace = 以前のポーリング間隔の 4 倍を推奨）以内に失効リストを正常にリフレッシュしていない場合、セラーはリストがリフレッシュされるまで新しい `purchase`、`modification`、`delivery` フェーズトークンを拒否しなければなりません（MUST）。これは失効エンドポイントを DoS する攻撃者が侵害された鍵の不正ウィンドウを延ばすのを防ぎます。ポーリング上限で運用するセラーは約 2.5 時間のエンドポイント停止耐性を得ます。フロアのセラーは約 5 分を得ます。リスク許容度に合わせて grace 定数ではなくポーリングケイデンスを調整します。

* ガバナンスエージェントは、現在のローテーション後に監査人が履歴トークンを検証できるよう、失効した公開鍵を監査保持期間（7 年推奨）の間発見可能に保持しなければなりません（MUST）。失効した鍵は `{origin}/.well-known/jwks-archive.json`（アクティブ JWKS とは別）で提供すべきです（SHOULD）。

#### 鍵ローテーション

* ガバナンスエージェントは、新しい `kid` を持つ新しい鍵を JWKS に追加し、新しい `kid` で新しいトークンに署名し、最も長命な未処理トークンが期限切れになるまで古い鍵を公開したままにしてローテーションします。
* セラー JWKS キャッシュは、拒否する前に missing-`kid` 失敗で無効化・再フェッチしなければなりません（MUST、無制限の再フェッチを防ぐため 30 秒クールダウン付き）。
* 緊急ローテーション（鍵侵害）は、古い `kid` を署名付き `revoked_kids` リストに追加し、即座に新しい鍵にローテーションして進みます。インテントトークンの短い exp、実行トークンの上限付き exp、失効リストポーリングが共に不正ウィンドウを制限します。

#### 検証エラータクソノミー

セラーとクライアントライブラリは、リトライ vs 拒否のセマンティクスがエコシステム全体で一貫するよう、これらのコードで検証失敗を表面化すべきです（SHOULD）。AdCP クライアントライブラリ（`@adcp/sdk` など）はこのタクソノミーにマップする型付きエラーを公開すべきです（SHOULD）。

| Failure                                                                   | Retry?            | Code                                                        | Notes                                                            |
| ------------------------------------------------------------------------- | ----------------- | ----------------------------------------------------------- | ---------------------------------------------------------------- |
| JWKS fetch timeout or 5xx                                                 | Yes, with backoff | `governance_jwks_unavailable`                               | 一時的。指数バックオフでリトライ。N 試行後に中止。                                       |
| JWKS fetch fails SSRF validation                                          | No                | `governance_jwks_untrusted`                                 | 恒久的。設定ミスの `jwks_uri` または攻撃を示す。                                   |
| `kid` not in JWKS after refetch                                           | No                | `governance_key_unknown`                                    | 拒否。ローテーションラグまたは鍵失効を示す可能性。                                        |
| Signature invalid, `typ` mismatch, `alg` not allowed, `crit` unknown      | No                | `governance_token_invalid`                                  | 拒否。改ざんまたは実装バグを示す。                                                |
| `exp` in past, `jti` replayed, `nbf` in future                            | No                | `governance_token_expired` / `_replayed` / `_not_yet_valid` | 拒否。トークンはリトライで治せない。                                               |
| `jti` ∈ `revoked_jtis` or `kid` ∈ `revoked_kids`                          | No                | `governance_token_revoked`                                  | 拒否。                                                              |
| `iss` not in buyer brand.json                                             | No                | `governance_issuer_not_authorized`                          | 拒否。スプーフィングの試みを示す可能性。                                             |
| Revocation list not refreshed within grace                                | No (block new)    | `governance_revocation_stale`                               | 失効リストがリフレッシュされるまで新しいトークンを拒否。既存の完全検証済みトークンは既存の grace 内で信頼され続けてよい。 |
| `aud` mismatch, `sub` mismatch, `phase` mismatch, `media_buy_id` mismatch | No                | `governance_token_not_applicable`                           | 拒否。トークンは有効だがこのオペレーション向けではない。                                     |

サーバーは内部検証詳細（例: どの特定クレームが不一致だったか）を相手方にエコーしてはなりません（MUST NOT）。上記の安定コードを返し、詳細はサーバー側でログします。

#### プライバシー考慮事項

**`policy_decisions` の可視性**: トークンは JWS（公開鍵を持つ誰でも読める）であり JWE（暗号化）ではありません。`policy_decisions` がガバナンスエージェントが評価したポリシー ID の完全なリストを含む場合、トークンを受け取るすべてのセラーは、バイヤーのガバナンス姿勢が考慮するポリシーを学びます — 競合インテリジェンス、場合によっては機微なオーディエンス特性についてのシグナリング（例: `minors_compliance` ポリシー ID は 18 歳未満オーディエンスのターゲティングを示唆）。ガバナンスエージェントは、バイヤーのコンプライアンス姿勢が機微なとき `policy_decisions` の代わりに `policy_decision_hash` を使うべきです（SHOULD）。完全なログはガバナンスエージェント制御のアクセスで `audit_log_pointer` を介して監査人に利用可能なままです。

<span id="intent-phase-disclosure" />**インテントフェーズのセラー開示（GA へ）**: `aud` バインディングは、競合オークションで N セラーを評価するバイヤーが、各々 1 セラーに `aud` バインドされた N 個の別個のインテントトークンを要求しなければならないことを意味します。したがってガバナンスエージェントはバイヤーが考慮したセラーの完全なリストを見ます — セラーがインテント時に GA に未知だった不透明文字列モデルに対するプライバシー退行。これは明示的なトレードオフです: クロスセラーリプレイ耐性はセラーごとのバインディングを要します。将来の `aud_hash` メカニズム（トークンがトークンスコープのソルトでセラー URL のハッシュをバインドし、各セラーが検証のため自身の URL でハッシュを計算）は、リプレイ耐性を犠牲にせずに GA に対するインテント時のセラープライバシーを回復できます。3.0 では定義されていません。フォローアップとして追跡されています。

**`caller` URL**: オーケストレーターの識別子を含みます。トークンを長期保持するセラーと監査人は、これが示唆する保持ポリシーに注意すべきです。

#### リファレンス実装

**デコードされた例トークン（インテントフェーズ）**:

ヘッダー:

```json theme={null}
{
  "alg": "EdDSA",
  "kid": "gov-2026-04",
  "typ": "adcp-gov+jws"
}
```

ペイロード:

```json theme={null}
{
  "iss": "https://gov.scope3.com",
  "sub": "plan_q1_2026_launch",
  "plan_hash": "EiCW8FkxgZ2wKqGv3Z9XuT4n2LwcJm1fK7vRaTpQ0sU",
  "aud": "https://seller.example.com/adcp",
  "iat": 1744934400,
  "exp": 1744935300,
  "jti": "01HWZXABCDEFG1234567890",
  "phase": "intent",
  "caller": "https://orchestrator.example.com",
  "check_id": "chk_001",
  "policy_decision_hash": "9b2a...f41c",
  "audit_log_pointer": "https://gov.scope3.com/plans/plan_q1_2026_launch/logs/01HWZXABCDEFG1234567890"
}
```

**セラー検証者（TypeScript、`jose` で約 30 行）**:

```ts theme={null}
import { createRemoteJWKSet, decodeProtectedHeader, decodeJwt, jwtVerify } from "jose";

class GovTokenError extends Error {
  constructor(public code: string) { super(code); }
}

const jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
function jwksFor(jwksUri: string) {
  let jwks = jwksCache.get(jwksUri);
  if (!jwks) {
    // ssrfValidatedFetch enforces the Webhook URL validation rules on the JWKS URL
    jwks = createRemoteJWKSet(new URL(jwksUri), { cacheMaxAge: 15 * 60 * 1000, cooldownDuration: 30 * 1000, [Symbol.for("fetch")]: ssrfValidatedFetch });
    jwksCache.set(jwksUri, jwks);
  }
  return jwks;
}

export async function verifyGovernanceContext(token: string, ctx: {
  sellerId: string; planId: string; mediaBuyId?: string; phase: "intent" | "purchase" | "modification" | "delivery";
  resolveBrandJsonGovernanceAgent: (iss: string) => Promise<{ jwks_uri: string } | null>;
  seenJti: (iss: string, aud: string, jti: string) => Promise<boolean>;
  isRevoked: (iss: string, jti: string, kid: string) => Promise<boolean>;
  revocationFresh: (iss: string) => Promise<boolean>;
}) {
  const header = decodeProtectedHeader(token);
  if (header.typ !== "adcp-gov+jws") throw new GovTokenError("governance_token_invalid");
  if (!["EdDSA", "ES256"].includes(header.alg ?? "")) throw new GovTokenError("governance_token_invalid");
  const { iss } = decodeJwt(token);
  const agent = await ctx.resolveBrandJsonGovernanceAgent(iss as string);
  if (!agent) throw new GovTokenError("governance_issuer_not_authorized");

  const { payload } = await jwtVerify(token, jwksFor(agent.jwks_uri), {
    issuer: iss as string, audience: ctx.sellerId, typ: "adcp-gov+jws",
    algorithms: ["EdDSA", "ES256"], clockTolerance: 60,
  }).catch(() => { throw new GovTokenError("governance_token_invalid"); });

  if (payload.sub !== ctx.planId) throw new GovTokenError("governance_token_not_applicable");
  if (payload.phase !== ctx.phase) throw new GovTokenError("governance_token_not_applicable");
  if (ctx.phase !== "intent" && payload.media_buy_id !== ctx.mediaBuyId)
    throw new GovTokenError("governance_token_not_applicable");
  if (!(await ctx.revocationFresh(iss as string))) throw new GovTokenError("governance_revocation_stale");
  if (await ctx.isRevoked(iss as string, payload.jti as string, header.kid as string))
    throw new GovTokenError("governance_token_revoked");
  if (await ctx.seenJti(iss as string, ctx.sellerId, payload.jti as string))
    throw new GovTokenError("governance_token_replayed");
  return payload;
}
```

**移行デュアルパス（3.0 中のセラー）**:

```ts theme={null}
const JWS_COMPACT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;

function handleGovernanceContext(value: string, ctx) {
  persistOpaque(value); // always persist and forward for auditor use
  if (!JWS_COMPACT.test(value)) return; // pre-3.0 opaque value, nothing to verify
  return verifyGovernanceContext(value, ctx); // throws on any failure
}
```

#### 移行（3.0 → 3.1）

* **3.0**: ガバナンスエージェントは、必須の `plan_hash` 監査層クレーム（セマンティクスは [Plan binding and audit](/docs/governance/campaign/specification#plan-binding-and-audit) を参照）を含め、このプロファイルに従ってコンパクト JWS を発行しなければなりません（MUST）。セラーは 15 ステップチェックリストを検証してもよい（MAY）。検証しないセラーはトークンを変更せずに永続化・転送しなければなりません（MUST）。JWS でない値は非推奨で、遷移中の pre-3.0 ガバナンスエージェントからのみ現れるべきです（SHOULD）。3.0 で非 JWS 値を発行するガバナンスエージェントは、セラーが検証不能なデプロイを検出できるよう、それをケイパビリティで宣言しなければなりません（MUST）。
* **3.1**: すべてのセラーは 15 ステップチェックリストに従って検証しなければなりません（MUST）。ガバナンスエージェントは JWS を発行しなければなりません（MUST）。非 JWS 値はエンドツーエンドで拒否されます。`plan_hash` は監査層のまま（ガバナンスエージェント / 監査人 / バイヤーコンプライアンス検証のみ — セラー検証ではない）。

フィールド名とスキーマ形状（単一文字列、4096 文字以下）はバージョン間で変わりません。文字列の内部形式のみが厳格化されます。これは以前のプロトコルバージョンからの相関キーセマンティクスを保持します — すでに値を不透明として扱うセラーは転送を続けるのに変更不要です。アカウンタビリティ特性を望むセラーは検証チェックリストを実装してオプトインします。

<a id="request-signing" />

### 署名付きリクエスト（トランスポート層）

[署名付きガバナンスコンテキスト](#signed-governance-context)は認可アーティファクトに署名します。リクエスト署名はリクエスト自体 — メソッド、ターゲット URI、ヘッダー、（デフォルトで）ボディバイト — に署名し、特定のエージェントがリクエストを発行したことを、リプレイと改ざん保護付きで暗号学的に確立します。有効な署名は 1 つのことだけを証明します: **リクエストは、その鍵が署名したエージェントから来た。** そのエージェントがリクエストボディで名指しされたブランドのために行動する*認可*を持つかは別の関心事で、ターゲットハウスの brand.json の `authorized_operator[]` が規定します。このセクションは認証のみを定義します。認可ルックアップは brand.json スキーマが規定し、リクエストが署名されているかに関わらず発生します。

AdCP 3.0 はこのプロファイルを、`get_adcp_capabilities` の `request_signing` を介して**オプションかつケイパビリティ宣伝**として定義します。AdCP 4.0 — 次の破壊的変更蓄積ウィンドウ — は支出コミットオペレーションでそれを要求します。基盤は 3.0 で出荷され、早期採用者が強制前に正準化とプロキシ相互運用のバグを表面化できます。[Transport migration timeline](#transport-migration-timeline) を参照。

**役割:**

* **エージェント**は、オペレーターの brand.json の `agents[]` エントリの自身の `jwks_uri` で公開した鍵でリクエストに署名します。オペレーター（brand.json をホストするドメイン）は直接購入するハウスでも認可されたサードパーティでもよい — このプロファイルは区別しません。署名者は常にエージェントです。
* **セラー**は署名を署名エージェントの公開鍵に対して検証し、エージェントアイデンティティを確立します。次にセラーは別個のブランド-オペレーター認可チェック（このプロファイルのスコープ外）を実行します。
* **エージェント側 AdCP エンドポイントを呼ぶセラー**（例: それ自体が AdCP プロトコル呼び出しであるバイヤーホストの変更コールバック）は、アウトゴーイングリクエストに対称的に署名します。受信エージェントはセラーオペレーターの brand.json の `agents[]` エントリで公開されたセラーの鍵に対して検証します。プッシュ通知 Webhook コールバック（`push_notification_config.url` や類似の非同期一方向通知）は、このプロファイルの対称 [Webhook callbacks](#webhook-callbacks) バリアントでカバーされます — セラーは `adcp_use: "request-signing"` 鍵でアウトバウンド署名し（非推奨の `"webhook-signing"` 値も受け入れられる）、バイヤーが検証します。

**依存関係:**

* JWKS ディスカバリー、SSRF ルール、alg 許可リスト、失効セマンティクス、鍵ローテーションを上記の [AdCP JWS profile](#adcp-jws-profile) と共有します。リクエスト検証は決して別の鍵目的を受け入れません: リクエスト署名 JWK は `"adcp_use": "request-signing"`、`"use": "sig"`、`"key_ops": ["verify"]`、および異なる `adcp_use` を持つ他の JWKS エントリに現れない `kid` を宣言しなければなりません（MUST）。検証者は 4 つすべてを強制します。[Agent key publication](#agent-key-publication) を参照。Webhook パスは、Webhook `tag` がドメイン分離を提供するため独自の明示的な緩和を持ちます。
* ガバナンスの [Buyer identity resolution](#buyer-identity-resolution) のアイデンティティブートストラップ依存を解決します: リクエスト署名を検証するセラーは暗号学的に確立された署名エージェントアイデンティティを持ち、署名エージェントのオペレータードメインをガバナンス検証ステップの brand.json 解決入力として使ってもよい（MAY）。

**コンフォーマンス。** 検証者の動作は、`request_signing.supported: true` を宣伝する任意のエージェントで実行される [`/compliance/latest/universal/signed-requests`](https://adcontextprotocol.org/compliance/latest/universal/signed-requests) のユニバーサルなケイパビリティゲートストーリーボードで採点されます。ストーリーボードは下記の [verifier checklist](#verifier-checklist-requests) のすべてのステップとこのプロファイルのすべての正準化エッジルールを、[`/compliance/latest/test-vectors/request-signing/`](https://adcontextprotocol.org/compliance/latest/test-vectors/request-signing/) のテストベクターに対して行使します。自身のエージェントに対して CLI グレーダーを実行するには [Auth Graders](/docs/building/verification/grading) を参照。

**汎用の RFC 9421 レスポンス署名プロファイルはない。** このプロファイルは*リクエスト*に署名します。AdCP 3.x は同期*レスポンストランスポート*に署名する汎用のペアプロファイルを定義しません。セラーは同期 AdCP レスポンス（MCP `tools/call` でも、ストリーミング `artifactUpdate` フレームを含む A2A 非ストリーミングレスポンスでも）に RFC 9421 §2.2.9 レスポンス署名を適用してはならず（MUST NOT）、バイヤーは同期返信の RFC 9421 レスポンス署名に依拠してはなりません（MUST NOT）。即時レスポンストランスポートの完全性は、リクエストを運んだ認証済みセッション内の TLS に依拠します。ボディを変更する CDN でのリクエスト側ボディ完全性を規定する標準のエッジ終端の注意事項を除きます。セッションを超えて存続する必要のあるアーティファクトの耐久的な保存時証明 — 専門分野スコープのペイロード（ブランド権利、AAO Verified コンプライアンス、セールスインテリジェンスリレー、ガバナンスレシート、`plan_receipt` のような双方向否認防止レシート）を含む — は [signed webhooks](#webhook-callbacks)（`adcp_use: "request-signing"` 鍵で署名）の役割です。この分割は意図的です — 完全な根拠と、正準アーティファクトが証明可能である必要のあるツールの request-the-webhook パターンは [Security Model: What gets signed](/docs/building/concepts/security-model#what-gets-signed--and-what-doesnt) を参照。

<a id="designated-task-response-signing" />

**指定タスクのペイロードエンベロープレスポンス署名。** 閉じたタスクのリストが、そのレスポンス*ペイロード*を `adcp_use: "response-signing"` の下で暗号学的に署名されるものとして指定します。このプリミティブは RFC 9421 §2.2.9 トランスポートレスポンス署名と、要となる 2 つの軸で異なります:

* **署名の場所:** HTTP レスポンスヘッダーではなく、レスポンスボディの中。
* **検証パス:** レスポンスボディをパースし、次に JWS をエージェントの `jwks_uri` で公開された応答エージェントの `response-signing` JWK に対して検証 — トランスポートヘッダー上の RFC 9421 ベース再構築ではない。

タスクは、そのレスポンスペイロードが正準の証明可能アーティファクトであり、かつ Webhook 発行の再構築が実現可能でない場合にのみ指定リストに認められます（デフォルトパスは [request-the-webhook パターン](/docs/building/concepts/security-model#the-request-the-webhook-pattern) を参照）。3.x のリストは次で閉じられています:

* **`verify_brand_claim`** とそのバルクバリアント **`verify_brand_claims`**（Brand Protocol）。応答するブランドエージェントは、ブランドの `adcp_use: "response-signing"` 鍵の下で JWS エンベロープとしてレスポンスペイロードに署名します。署名は方向非対称の信頼モデルの要です — [`verify_brand_claim` trust model](/docs/brand-protocol/tasks/verify_brand_claim#trust-model) と [Building a brand agent — Signing setup](/docs/brand-protocol/building-a-brand-agent#signing-setup) を参照。

このリストにないタスクは、いかなる署名プリミティブの下でもレスポンスに署名してはなりません（MUST NOT）。任意のツールに RFC 9421 §2.2.9 を適用する汎用レスポンス署名ヘルパー（どの `tag` や `adcp_use` 文字列を作っても）はこのプロファイルの外で動作し、3.x 非コンフォーマントです。スペックが 3.x で認可する唯一のレスポンス署名プリミティブは、指定タスクリストのペイロードエンベロープ JWS です。

したがって `adcp_use: "response-signing"` 値は JWK 層でペイロードエンベロープのプリミティブに予約されます。**`adcp_use: "response-signing"` で公開された鍵は、このセクションで定義されたペイロードエンベロープ JWS のみに署名しなければなりません（MUST）。そのような鍵を使って RFC 9421 §2.2.9 トランスポート署名を生成することは、署名されるタスクに関わらずプロファイル違反です。** 将来のメジャーバージョンが任意のタスクに RFC 9421 トランスポートレスポンス署名をスコープする場合、検証者が JWK だけからプリミティブを区別できるよう、別個の `adcp_use` 値（例: `"response-transport-signing"`）を使わなければなりません（MUST）— ブランドプロトコル値は両方をカバーするよう後付けできません。リスト成長と追加のプリミティブは将来のスペックバージョンに延期された規範的決定です。

指定タスクの成功レスポンスは [`response-payload-jws-envelope.json`](https://adcontextprotocol.org/schemas/v3/core/response-payload-jws-envelope.json) に一致する `signed_response` メンバーを運ばなければなりません（MUST）。エンベロープペイロードは正準の署名済みタスクボディオブジェクトで、`typ: "adcp-response-payload+jws"`、`task`、`brand_domain`、`agent_url`、`request_hash`、`iat`、`exp`、`response` を含まなければなりません（MUST）。外側のタスクボディフィールドは通常のタスクコンシューマー向けの便宜フィールドです。署名に依拠する検証者は、いずれかの未署名タスクボディフィールドが `signed_response.payload.response` と食い違う場合エンベロープを拒否しなければなりません（MUST）。プロトコル/バージョンエンベロープフィールドはこの比較から除外され、`status`、`context_id`、`task_id`、`message`、`timestamp`、`replayed`、`adcp_version`、`adcp_major_version` を含みます。

このプロファイルは RFC 7797 の非エンコードペイロードではなく通常の JWS 署名を使います。JWS 署名入力は `BASE64URL(UTF8(protected)) || "." || BASE64URL(UTF8(JCS(payload)))` で、`payload` は `signed_response.payload`、`protected` は `{ "alg": "EdDSA" | "ES256", "kid": "...", "typ": "adcp-response-payload+jws" }` にデコードされます。protected ヘッダーは `b64` を含んではなりません（MUST NOT）。レスポンス検証者は [AdCP JWS profile](#adcp-jws-profile) の共有 JWS ディスカバリーとハードニングルールを強制しなければなりません（MUST）: 許可アルゴリズム、`use: "sig"`、`"verify"` を含む `key_ops`、正確な `adcp_use: "response-signing"`、missing-`kid` 再フェッチ、失効チェック、SSRF セーフな JWKS フェッチ、正準化前の重複キー拒否。

`request_hash` は `sha256:` に JCS 正準リクエストバインディングオブジェクト `{ task, brand_domain, agent_url, caller_identity, request }` の非パディング base64url SHA-256 を加えたものです。`caller_identity` は、認証済みトランスポートまたはクレデンシャルマッピングから導出された型付き正準文字列でなければなりません（MUST）。例: `signed-agent-url:<agents[].url>`、`api-client-id:<seller-issued client id>`、`mtls-san:<lowercased SAN>`。認証済み呼び出し元アイデンティティが存在しない場合、`caller_identity` は `null` で、検証者はレスポンスを呼び出し元にバインドされない弱い証拠として扱わなければなりません（MUST）。

`brand_domain` はエコーではなくテナントバインディングフィールドです。マルチブランドエージェントは、それをサーバー側のテナント解決と、答えを生成したポリシーストアの brand.json エントリから設定しなければならず（MUST）、リクエストボディからコピーしてはなりません（MUST NOT）。`agent_url` は `response-signing` JWK がエンベロープを検証する応答 `agents[]` エントリの正準 URL です。オンライン検証者は、小さなクロックスキュー許容のみを適用した後、`exp` 以降のエンベロープを拒否しなければなりません（MUST）。監査検証者は `exp` 後に検証してもよい（MAY）が、ブランドエージェントが記載された `iat`/`exp` ウィンドウ中にそのペイロードに署名したという履歴証拠としてのみです。

レスポンス署名鍵は目的だけでなくブランドテナントでもスコープされます。共有マルチブランドフリートは、同じソフトウェアと `agent_url` が複数ブランドを提供しても、提供する各 `brand_domain` に別個のレスポンス署名鍵素材と別個の `kid` 値を公開しなければなりません（MUST）。レスポンス署名 JWK のクロスブランド再利用は、テナントバインドのリプレイ分析を無効にするためプロファイル違反です。このルールは通常のクロス目的分離より厳しく、`adcp_use: "response-signing"` 鍵にのみ適用されます。

#### トランスポートスコープ

| Class                                                                                     | 3.0                                     | 4.0          |
| ----------------------------------------------------------------------------------------- | --------------------------------------- | ------------ |
| Spend-committing (`create_media_buy`, `update_media_buy`, `acquire_*`, `activate_signal`) | Optional, capability-advertised         | Required     |
| Reversible state changes (`sync_creatives`, `update_creative_status`)                     | Optional                                | Recommended  |
| Read / discovery (`get_products`, `get_media_buy_delivery`, `list_*`)                     | Not in scope                            | Not in scope |
| TMP `provider_endpoint_url` requests                                                      | Out of scope (TMP has its own envelope) | Out of scope |

読み取り呼び出しは bearer 認証のままです。読み取りトラフィックへの署名は、比例した利益なしに検証コストを追加します。署名の目的は状態変更オペレーションの完全性です。

#### クイックスタート: 3.0 でリクエスト署名にオプトイン

4.0 のフリップ前に 3.0 で署名をパイロットしたい実装者向け:

**リクエストに署名するエージェントとして:**

0. ターゲットセラーで `get_adcp_capabilities` を呼び出す。`request_signing.supported_for` と `required_for` を読んで、セラーがあなたに署名を期待する AdCP オペレーションを確認し、`request_signing.protocol_methods_supported_for` / `protocol_methods_required_for` を読んで、セラーの検証者がカバーする JSON-RPC プロトコルメソッド（例: `tasks/cancel`）を確認する。`covers_content_digest`（`"required"` / `"forbidden"` / `"either"`）を読んで、`content-digest` をカバーしなければならない、してはならない、してもよいかを確認する。
1. Ed25519 鍵ペアを生成: `openssl genpkey -algorithm ed25519 -out signing-key.pem`。
2. 公開鍵を JWK としてエクスポート。`"kid"`、`"use": "sig"`、`"key_ops": ["verify"]`、`"adcp_use": "request-signing"`、`"alg": "EdDSA"` を追加。
3. JWK をエージェントの `jwks_uri`（brand.json の `agents[]` エントリで宣言された URL。エージェント URL のオリジンの `/.well-known/jwks.json` にデフォルト）で公開。
4. AdCP クライアントを秘密鍵とエージェント URL で設定。SDK は、セラーの `supported_for` または `required_for` ケイパビリティにリストされた任意のオペレーションと、`protocol_methods_supported_for` または `protocol_methods_required_for` にリストされた任意の JSON-RPC メソッドについて、セラーの `covers_content_digest` ポリシーを守って自動的にリクエストに署名する。SDK は、秘密鍵がプロセスメモリではなくマネージド鍵ストア（KMS / HSM / Vault）に存在できるよう、プラガブルな署名者をサポートすべき（SHOULD）— 下記の [Production key storage](#production-key-storage) を参照。
5. [`/compliance/latest/test-vectors/request-signing/`](https://adcontextprotocol.org/compliance/latest/test-vectors/request-signing/) のコンフォーマンスベクター（AdCP バージョンごとに公開。ソースは `static/compliance/source/test-vectors/request-signing/`）でエンドツーエンド検証 — クライアントが正例ベクターの `expected_signature_base` に一致する署名を生成すれば完了。

**検証者（セラー）として:**

1. `get_adcp_capabilities` で `request_signing.supported: true` を宣伝。パイロット中は `required_for: []` のまま。相手方ごとに段階的にオペレーションを追加。
2. 変更系ルートで署名検証ミドルウェアを有効化。[verifier checklist](#verifier-checklist-requests) を実装 — 14 のチェックすべて（13 の番号付きステップとサブステップ 9a）、最初の失敗でショートサーキット。
3. `required_for` を設定する前に、パイロット相手方についてシャドウモード（検証してログ。失敗で拒否しない）で開始。最初の数週間は検証失敗をオペレーションではなくモニタリングで表面化。
4. 検証者に対してコンフォーマンス負例ベクターを実行 — 各拒否はベクターの記載された `error_code` を生成しなければならない（MUST）。ベクターの `failed_step` は情報的です。正しいエラーコードで拒否する実装は、内部ステップ番号が異なってもコンフォーマントです。

**最小実行可能検証者（3.0 シャドウモード）:** チェックリストのステップ 1-9、9a、10、インメモリリプレイキャッシュ、軽量な `kid` メンバーシップチェック付き 1 分失効ポーリング（完全な grace セマンティクスは延期）。リプレイやダイジェスト失敗で拒否されるリクエストがないので、これは log-and-observe シャドウモードに許容されます。**`required_for` に任意のオペレーションを追加する前に、ステップ 11-13 を実装** — ダイジェスト再計算（ステップ 11）、成功後のリプレイ挿入（ステップ 13）、完全な失効 stale grace ウィンドウ（ステップ 9 の一部）。不完全な検証者で強制に切り替えると、シャドウログではなくライブ本番トラフィックでリプレイとボディ完全性のギャップが表面化します。ステップ 1 を飛び越さないでください — 不正な署名は常に拒否し、決してフォールバックしません。

#### 本番の鍵保管

署名者の秘密鍵がどこに存在するかは実装依存です — スペックはワイヤー上のバイトのみに関心があります — が、オペレーターは本番でプロセスメモリに秘密署名鍵を保持することを避けるべきです（SHOULD）。プロセス侵害は署名鍵を漏らし、唯一の救済は、公開鍵をキャッシュしたすべての相手方をまたぐ（それらのキャッシュ TTL 内での）ローテーションです。

推奨パターン: SDK がプラガブルな署名者インターフェース（例: `sign(payload: Uint8Array): Promise<Uint8Array>`）を公開し、オペレーターのアダプターがオペレーションをマネージド鍵ストア — AWS KMS、GCP KMS、Azure Key Vault、HashiCorp Vault Transit、または HSM — に委任します。鍵はマネージドストアを決して離れません。SDK は正準署名ベースを構築し、ストアがそれに署名し、SDK は返されたバイトから `Signature` と `Signature-Input` ヘッダーを組み立てます。ワイヤー形式はインプロセス署名と同一です。

アダプター作成者向けの 2 つの実装注記:

* ほとんどの KMS API が返す ECDSA-P256 署名は DER エンコードです。このプロファイルと RFC 9421 §3.3.1 は IEEE P1363（`r‖s`、P-256 では 64 バイト）を要求します。アダプター境界で変換します。
* KMS 鍵を単一目的として扱います。このプロファイルの `tag` パラメーターは署名者ではなく検証者を保護します — 同じ KMS 鍵を AdCP リクエスト署名と他の任意の署名プロトコルに再利用するオペレーターは、クロスプロトコルオラクルを作ります。AdCP 署名パスのみが鍵を呼び出せるよう KMS アクセスポリシーをバインドします（GCP `roles/cloudkms.signer` を特定の cryptoKey にスコープ、AWS `kms:Sign` を鍵 ARN で条件付け）。

リファレンス実装: `@adcp/sdk`（TypeScript）は sync/async パリティを持つ `SigningProvider` インターフェース、テスト用のインメモリプロバイダー、[`examples/gcp-kms-signing-provider.ts`](https://github.com/adcontextprotocol/adcp-client/blob/main/examples/gcp-kms-signing-provider.ts) の GCP KMS リファレンスアダプターを出荷します。完全なウォークスルーは [SDK signing guide](https://github.com/adcontextprotocol/adcp-client/blob/main/docs/guides/SIGNING-GUIDE.md#step-35-production-key-storage--kms--hsm--vault) を参照。

**トリップワイヤーパターン — init 時に公開鍵をアサート。** マネージド鍵ストアは黙ってローテーションできます（IAM ポリシースワップ、バージョン無効化、敵対的置換）。公開 JWKS を更新せずにローテーションが起きると、変わらない `kid` をフェッチする検証者は、明確なエラーシグナルなしにすべての署名を拒否します — オペレーターは KMS ミスマッチではなく相手方の失敗を見ます。防御: 期待される公開鍵（SPKI バイト、base64 エンコード）をコードと共にコミットし、署名者 init 時にストアが返す鍵とバイト比較（`getPublicKey()` など）します。ミスマッチは、すべての署名済み呼び出しで黙ってではなく、起動時に大きく失敗します。ローテーションはその後、意図的な二段階になります: ピン留めされた定数を更新し、新しい鍵バージョンパスを設定し、デプロイ。

**ライフサイクル: eager ではなく lazy init。** プロセスがリスナーをバインドする前に `getPublicKey`（または任意の KMS ウォームアップ呼び出し）を呼ぶことはレビューでクリーンに見えますが、危険な失敗モードがあります: KMS 認証が誤設定されていると、KMS クライアント内の gRPC / TLS リトライが無期限にブロックし、プロセスはポートを開かず、インフラのヘルスチェックがタイムアウトします — 根本的な KMS エラーではなく「サービス到達不能」アラームを表面化します。正しいライフサイクルは最初の署名時の lazy init です: リクエストが署名を必要とする最初のときにストアを呼び、成功時のみ結果をキャッシュし（エラーを決してキャッシュしない）、並行する初回呼び出しリクエストを in-flight promise で重複排除します。Fail-fast の誤設定検出は、プロセス起動時ではなく、切り替え前にデプロイターゲットの認証情報で KMS パスを行使する CI/CD プレデプロイプローブに属します。

**`adcp_use` ごとに 1 JWK — 公開形状。** 単一目的ルールは鍵素材**と** JWKS 公開に適用されます。Webhook は独自の目的を必要としないことに注意: それらは `"request-signing"` 鍵で署名されるので（[Webhook callbacks](#webhook-callbacks) のステップ 8 を参照）、リクエストと Webhook の両方を同じ鍵で署名するオペレーターは単一の `"request-signing"` エントリを公開します。Webhook に別個の鍵素材（影響範囲分離）を望むオペレーターは、**別個の `kid` を持つ 2 つ目の `"request-signing"` 鍵**を公開します — 分離は別個の `adcp_use` ではなく `kid` から来ます。`adcp_use` 値は常に**文字列**であり配列ではありません — 単一エントリに `"adcp_use": ["request-signing","webhook-signing"]` を公開することは受信者が拒否するスキーマエラーです:

```json theme={null}
{
  "keys": [
    {
      "kty": "OKP", "crv": "Ed25519",
      "x": "SRYr8eSvjkZF6dAUquI1sKuU4YGZkoGH-2jwkz4dRJg",
      "kid": "acme-signing-2026-04",
      "alg": "EdDSA", "use": "sig",
      "adcp_use": "request-signing",
      "key_ops": ["verify"]
    },
    {
      "kty": "OKP", "crv": "Ed25519",
      "x": "lHJI-IvBwCE36heDNOyBmCk5UMKRIs4b4BAWJRgao-M",
      "kid": "acme-webhook-2026-04",
      "alg": "EdDSA", "use": "sig",
      "adcp_use": "request-signing",
      "key_ops": ["verify"]
    }
  ]
}
```

この 2 つ目のエントリは [Key publication](#webhook-callbacks) セクションのオプションの Webhook 分離鍵です: 同じ `adcp_use: "request-signing"`、別個の `kid`、Webhook 鍵の侵害がリクエスト署名に及ばないよう Webhook 署名に使用。別個の `kid` 値はまた、相手方が 2 つの鍵を独立にキャッシュ・ローテーションできることを意味します。

#### AdCP RFC 9421 プロファイル

このプロファイルは、クロス実装の相互運用が扱いやすくなるよう、RFC 9421 を単一の正準形状に制約します。

**カバーされるコンポーネント（すべての署名済みリクエストで REQUIRED）:**

| Component        | Notes                                                                                                                                            |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `@method`        | 大文字。                                                                                                                                             |
| `@target-uri`    | 下記のアルゴリズムで正準化。署名者は署名ベースを計算する前に正準化を適用しなければならず（MUST）、検証者は検証前に受信したリクエストに同じ正準化を適用しなければならない（MUST）。                                                    |
| `@authority`     | 小文字の `host[:port]`、デフォルトポート（https は `443`、http は `80`）を除去。                                                                                       |
| `content-type`   | ボディのあるリクエストで必須。                                                                                                                                  |
| `content-digest` | 検証者の `request_signing.covers_content_digest` ケイパビリティが規定 — [Content-digest and proxy compatibility](#content-digest-and-proxy-compatibility) を参照。 |

**`@target-uri` 正準化**は [AdCP URL canonicalization rules](/docs/reference/url-canonicalization) に従います — RFC 3986 §6.2.2（構文ベース正規化）と §6.2.3（スキームベース正規化）、UTS-46 Nontransitional IDN 処理、IPv6 ゾーン識別子拒否を適用する 8 ステップ。署名者と検証者は同じアルゴリズムを適用します。そこで拒否された不正なオーソリティは、署名パスで `request_target_uri_malformed` にマップされます。権威的なアルゴリズム、コンフォーマンスベクター、落とし穴リストはそのページに存在します — このプロファイルの扱いを薄く保つことで、署名固有のコピーと汎用コピーの間の発散を防ぎます。

**`@authority` 正準化**は、正準化アルゴリズムのホストとポートステップ後の URL のオーソリティから `host[:port]` を生成します（小文字ホスト / IDN → ACE / IPv6 ブラケット保持。userinfo 除去。デフォルトポート除去）。IPv6 ホストは `@authority` でブラケットを保持します（`[::1]:8443`）。検証者は、存在する場合 HTTP/2+ の `:authority` 疑似ヘッダーから、そうでなければ受信した HTTP/1.1 `Host` ヘッダーから `@authority` を導出しなければなりません（MUST）— リバースプロキシルーティング状態、ロードバランサーメタデータ、フォワードプロキシが転送中に書き換えた任意の `Host` 値からではありません。**受信リクエストに `:authority` と `Host` の両方が存在する場合**（HTTP/2→HTTP/1.1 変換中間者は RFC 7540 §8.1.2.3 により両方を残すことが許可され、これは等価性を要求するがソースの除去は要求しない）、検証者は正準化後にバイト等価でなければ `request_target_uri_malformed` で拒否しなければなりません（MUST）。pick-one 動作は黙ったダウングレード面です。ソースヘッダーに関わらず、正準化された値は正準 `@target-uri` のオーソリティコンポーネントとバイト単位で一致しなければなりません（MUST）— 署名済み `@target-uri` に対するバイト一致が要となる安全ゲートです。`Host` は転送中に書き換えられ得るからです。ミスマッチは `request_target_uri_malformed` で拒否します。これはクロス vhost リプレイベクトルを閉じます: TLS 終端されたリクエストを傍受し、同じ検証者プール上の 2 つ目の vhost（同じ証明書 SAN、異なる `Host`）にリプレイする攻撃者は、署名が `@authority` をカバーしていてもオーソリティ一致チェックに失敗します。

正準化する署名者と正準化する検証者は、同じ論理リクエストに対して同一のバイトを生成しなければなりません（MUST）。あなたの 9421 ライブラリが異なるルールを適用する場合、このプロファイルに一致するよう設定するか、URL をライブラリに渡す前に正規化します。

[`canonicalization.json`](https://adcontextprotocol.org/compliance/latest/test-vectors/request-signing/canonicalization.json) コンフォーマンスセットは、固定入力と期待出力、加えて不正オーソリティ拒否ケースで、アルゴリズムのすべてのルールを行使します。SDK はこのセットをすべてのコミットで実行すべきです（SHOULD）— 署名者間の正準化発散は、そうでなくなるまで黙っており、その後診断が痛い本番相互運用バグになります。

検証者は、カバーされるコンポーネントリストがリクエストタイプに必要なコンポーネントを省略する署名を拒否しなければなりません（MUST）。署名者は調整なしに追加ヘッダーをカバーしてはなりません（MUST NOT）— 余分なコンポーネントは、それらを含めない実装をまたいで署名を黙って無効にします。

**署名パラメーター（`Signature-Input` パラメーター、すべて REQUIRED）:**

| Parameter | Notes                                                                                                                                                                                                                                                                            |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `created` | Unix 秒。60 秒より先の未来なら拒否。                                                                                                                                                                                                                                                           |
| `expires` | Unix 秒。`expires > created` かつ `expires − created ≤ 300`（5 分最大有効性）を満たさなければならない。過去なら ±60 秒スキュー許容で拒否。                                                                                                                                                                               |
| `nonce`   | Base64url エンコード、非パディング（末尾 `=` なし）。検証者は、デコードされたバイト長が 16 バイト未満、または値がパディングを含む場合拒否しなければならない（MUST）。これが「128 ビット以上のエントロピー」要件が実際に強制される方法。                                                                                                                                                |
| `keyid`   | 署名者の公開 JWKS の `kid` に一致。                                                                                                                                                                                                                                                         |
| `alg`     | `ed25519` または `ecdsa-p256-sha256` でなければならない。検証者はライブラリのデフォルトとは独立に許可リストを強制しなければならない（MUST）。                                                                                                                                                                                         |
| `tag`     | 正確に `adcp/request-signing/v1` でなければならない — バイト単位一致、プレフィックス一致なし、ケースフォールディングなし。`tag` sig-param は `Signature-Input` にちょうど 1 回現れなければならない。検証者は重複を拒否しなければならない（MUST）。tag 名前空間がプロファイルのバージョン管理方法。将来のバージョンはパラメーターセマンティクスを変えるのではなく tag をバンプし、`adcp/request-signing/v2` 検証者は `v1` 署名を拒否し逆も同様。 |

6 つのパラメーターすべてが REQUIRED です。検証者はいずれかが不在なら拒否しなければなりません（MUST、`request_signature_params_incomplete`）。

**アルゴリズム命名 — JWK vs RFC 9421。** 各アルゴリズムの 2 つの名前はソーススペックで異なります。実装はこれらを十分頻繁に混同するので表が必要です:

| Algorithm                | JWK `alg` (in JWKS) | RFC 9421 `alg` (in `Signature-Input`) |
| ------------------------ | ------------------- | ------------------------------------- |
| Ed25519                  | `EdDSA`             | `ed25519`                             |
| ECDSA P-256 with SHA-256 | `ES256`             | `ecdsa-p256-sha256`                   |

検証者が `keyid` を解決して JWK に `"alg": "EdDSA"` を見つけたとき、一致する sig-param 値は `ed25519` です。実装は、それぞれ独立に許可リストを検証することに加えて、2 つが一致すること（JWK alg がマッピング表で sig-param alg に一致）を検証すべきです。ガバナンスプロファイルからのエッジランタイム根拠が適用されます — `ES256` は `EdDSA` がランタイム設定を要するエッジ向けの代替です。

**リクエストごとに 1 署名。** 検証者はちょうど 1 つの `Signature-Input` ラベル（慣習的に `sig1`）を処理しなければならず（MUST）、リクエストに存在する追加のラベルを無視しなければなりません（MUST）。リレーされたリクエストに再署名する必要のある中間者は、上流ラベルに追加するのではなく置換しなければなりません（MUST）。完全なリレーチェーンセマンティクス（リレーが発信者の署名を保持したい場合）は [#2324](https://github.com/adcontextprotocol/adcp/issues/2324) で追跡され、3.0 のスコープ外です。

**バイナリ値エンコード（`Signature`, `Content-Digest`）。** RFC 9421 §3.1 と §2.1.3 はバイナリ値を RFC 8941 Structured Field `sf-binary` トークン（`:<base64>:`）として発行し、RFC 8941 §3.3.5 は `+`/`/` と `=` パディングを持つ標準 base64 アルファベット（RFC 4648 §4）を規定します。AdCP プロファイルはこれを上書きします: `Signature` と `Content-Digest` の sf-binary 値は**パディングなし base64url**（RFC 4648 §5）でエンコードしなければならず（MUST）、内側バイトが `[A-Za-z0-9_-]` から引かれ末尾 `=` のないトークンを生成します。

根拠: URL セーフ、パディングなし、すでに base64url 非パディングと規定された `nonce` sig-param と対称。HTTP ヘッダー値の標準 base64 の 2 つの相互運用ハザード — 一部のプロキシが書き換える `/` と一部のヘッダーパーサーが構造化フィールドパラメーター区切りとして扱う `=` — を避けます。

検証者の要件:

1. 署名者はパディングなし base64url のみを発行しなければなりません（MUST）。`+`、`/`、`=` を含む `Signature` または `Content-Digest` 値を発行する署名者は非コンフォーマントです。
2. 検証者はパディングなし base64url を受け入れなければなりません（MUST）。検証者はこの明確化より前の相手方との相互運用のため、純粋な標準 base64 トークンも寛容にデコードすべきです（SHOULD、`+`→`-` 次に `/`→`_` に変換、次に末尾 `=` を除去、次に base64url デコード）。この寛容は **AdCP 3.2** で削除予定の互換性の便宜です — それに依拠する署名者はそれまでにパディングなし base64url に移行しなければなりません（MUST）。
3. 検証者は、アルファベットを混在させる任意のトークン（同じトークン値内の `[+/=]` 内の任意の文字 AND `[-_]` 内の任意の文字）を `request_signature_header_malformed` で拒否しなければなりません（MUST）。混在アルファベットトークンは曖昧です: `A+B-` は「標準 base64 文字を変換」と「base64url デコード」ステップの順序によって異なるバイトにデコードされ得、検証者間で異なる `Content-Digest` バイトは、攻撃者があるバリデーターが受け入れ別のが拒否するダイジェストミスマッチを仕込むことを許します。
4. コンフォーマンスベクターの `expected_signature_base` フィールドはバイナリ値エンコードから独立です — 正準署名ベースバイトを含み、ヘッダーフィールドエンコードではありません。発行される `Signature` トークン自体のみがエンコードされます。

**非 AdCP 上流からの `Content-Digest` についての注記。** RFC 9530 §2 は `Content-Digest` を定義し、sf-binary を RFC 8941（標準 base64）に委ねるので、別のエコシステムからのコンフォーマントな 9530 発行者（CDN、非 AdCP フレームワーク）は、RFC 8941 デフォルトを使ってインバウンドリクエストに `Content-Digest` を設定するかもしれません。上記の AdCP 上書きは**署名済み AdCP リクエスト**に適用されます。そのようなリクエストを処理する検証者は上書きルールを使わなければなりません（MUST）。未署名トラフィックや非 AdCP 上流からの `Content-Digest` を扱う検証者はどちらのエンコードを受け入れてもよい（MAY）— これは署名プロファイルのスコープ外です。

**`required_for` / `supported_for` のオペレーション名は AdCP プロトコルオペレーション名**（`create_media_buy`, `update_media_buy`, `acquire_rights` など）です — MCP ツール名、A2A スキル名、任意のトランスポート固有の改名ではありません。検証者は AdCP プロトコルスペックが定義しないオペレーション名を受け入れてはなりません（MUST NOT）。これがクロストランスポート検証者が「`create_media_buy` に署名された」の意味に合意する方法です。

**プロトコルメソッドカバレッジ（`protocol_methods_*`）。** AdCP オペレーションは相手方が呼ぶ唯一の変更系面ではありません: A2A 0.3.0 §7.x は同じ認証済みチャネルを通るタスクライフサイクルメソッド（`tasks/cancel`, `tasks/get`, `tasks/resubscribe`）を定義し、MCP トランスポートは SDK タスクストアが接続されると同じ `tasks/*` JSON-RPC メソッドを自動登録します。セラーはこれらのメソッドの検証者カバレッジを AdCP オペレーションリストとは別の名前空間で宣言します:

| Field                                            | Contents                                         | Match semantics                                             |
| ------------------------------------------------ | ------------------------------------------------ | ----------------------------------------------------------- |
| `request_signing.protocol_methods_supported_for` | JSON-RPC method strings (e.g., `"tasks/cancel"`) | インバウンドリクエストの JSON-RPC `method` フィールドが一致するとき、検証者は署名を受け入れて検証。 |
| `request_signing.protocol_methods_warn_for`      | Same                                             | `warn_for` のシャドウモードミラー: 失敗をログ、拒否しない。                        |
| `request_signing.protocol_methods_required_for`  | Same                                             | 未署名の一致を `request_signature_required` で拒否。                   |

一致する値は JSON-RPC エンベロープの `method` フィールド（`tasks/cancel`, `tasks/get`, …）であり、MCP `tools/call` の `params.name` では**ありません**。AdCP ツール名（`/` なし）は任意の `protocol_methods_*` 配列に現れてはならず（MUST NOT）、JSON-RPC メソッド名（`/` を含む）は `supported_for` / `warn_for` / `required_for` に現れてはなりません（MUST NOT）。検証者は名前空間分割に違反するケイパビリティブロックを、2 つの間で文字列を黙って強制するのではなく、設定時エラーで拒否しなければなりません（MUST）。**検証者はクロス名前空間一致してはなりません: `protocol_methods_required_for` メンバーシップは JSON-RPC `method` が `tools/call` であるボディ（`params.name` がリストされたメソッド文字列に等しくても）で満たされてはならず（MUST NOT）、`required_for` メンバーシップは JSON-RPC `method` が `tools/call` 以外の任意のものであるボディで満たされてはなりません（MUST NOT）。** 2 つのバケットは互いに素なエンベロープフィールドに対して一致されます。

署名ベース構築は両方の名前空間で同一です: 同じ RFC 9421 カバーコンポーネント（`@target-uri`, `@method`, セラーの `covers_content_digest` ポリシーに従う `content-digest`, 存在する場合 `authorization`）が適用され、`@target-uri` と `@method` は JSON-RPC メソッド文字列ではなく実際の HTTP リクエストを反映します。`tasks/cancel` POST に署名するバイヤーは、他の変更系呼び出しと全く同じように署名します。新しいフィールドが変えるのは、どの JSON-RPC メソッドが検証のスコープ内かのセラーの宣言のみです。

**共有トランスポート上のクロス名前空間リプレイリスク。** 単一の `@target-uri` が `tools/call` エンベロープと JSON-RPC プロトコルメソッドの両方を受け入れる場合（正準の MCP レイアウト — 両方が `/mcp` に POST）、`@target-uri` と `@method` だけではボディがどの JSON-RPC メソッドを呼ぶかをバインドしません。`method` フィールドはボディに存在します。`content-digest` カバレッジなしでは、署名済み `tools/call` リクエストをキャプチャする経路上攻撃者は、署名ウィンドウ内でボディを `{"method":"tasks/cancel",...}`（または逆）にスワップでき、検証者はそれを受け入れます。`tools/call` と共有されるトランスポート上で `protocol_methods_required_for`（または任意の `protocol_methods_*`）を設定するセラーは、ボディ — そしてそれを通じて JSON-RPC メソッド — が署名にバインドされるよう `covers_content_digest: 'required'` を設定すべきです（SHOULD）。`'required'` を採用できないセラーは、`@target-uri` 自体が名前空間を分割するよう、AdCP とプロトコルメソッドトラフィックを別個の `@target-uri` にマウントしなければなりません（MUST）。

3.x でケイパビリティブロックを読むバイヤーは、`supported_for` / `required_for` からプロトコルメソッドカバレッジを仮定してはなりません（MUST NOT）: `create_media_buy` を `required_for` にリストし `protocol_methods_*` について沈黙するセラーは、`tasks/cancel` カバレッジを宣言していません。`tasks/cancel` に日和見的に署名するバイヤー SDK（セラーが沈黙するときの唯一の擁護可能なデフォルト）はスペックに違反せずにそうしてもよい（MAY）が、相互運用可能な強制は、セラーが `protocol_methods_supported_for` または `protocol_methods_required_for` を設定して初めて生じます。

#### エージェント鍵の公開

リクエスト署名鍵は、署名エージェント自身のオペレーターの brand.json の `agents[]` エントリの `jwks_uri` に存在し、アウトバウンド Webhook は `"request-signing"` 鍵（オプションで別個の `kid` の下の Webhook 専用鍵素材）で署名されます。署名するすべてのエージェント — 任意の `type` の — は同じ公開パターンを使います。パブリッシャー `adagents.json` は追加で `authorized_agents[].signing_keys[]` を通じて許可されたセラー鍵をピン留めしてもよい。存在する場合、そのピンはスコープされたセルサイド認可に権威的です。

**パブリッシャーピンの優先順位。** パブリッシャーの `adagents.json` の認可エージェントエントリが `signing_keys` ピン（[`adagents.json` §`signing_keys`](/docs/governance/property/adagents#signing_keys) を参照）を運ぶ場合、そのピンは権威的です: 検証者は、`jwks_uri` の内容に関わらず、`keyid` がピン留めセットにない任意の署名を拒否しなければなりません（MUST）。エージェントホストの JWKS は、パブリッシャーピンが存在するときは常に助言的です。これはエージェントドメイン侵害ウィンドウを閉じます — エージェントのドメインを乗っ取る攻撃者は、パブリッシャーのピンが依然受け入れを規定するため、エンドポイントとその宣伝された鍵の両方を黙ってスワップできません。パブリッシャーは、委任スコープに変更系オペレーションを含む任意のエージェントについてピン留めすることが要求されます。ローテーションとキャッシュセマンティクスは adagents.json ルールを参照。

各リクエスト署名 JWK エントリは宣言しなければなりません（MUST）:

| Member     | Value                  | Notes                                                                                                                                                                                                                                                                                                                                                                                           |
| ---------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `use`      | `"sig"`                | 標準 JWK 署名用途。                                                                                                                                                                                                                                                                                                                                                                                    |
| `key_ops`  | `["verify"]`           | 検証者可視の JWKS は verify のみを宣言。署名オペレーターは対応する秘密鍵を JWK スペックに従い `["sign"]` でローカルに保持。                                                                                                                                                                                                                                                                                                                   |
| `adcp_use` | `"request-signing"`    | AdCP 固有の目的判別子。`"governance-signing"`（JWS プロファイル）と将来の任意の AdCP 署名目的と区別。検証者はリクエスト署名を検証するとき `adcp_use` が不在または異なる任意の JWK を拒否しなければならない（MUST）。同じ `"request-signing"` 鍵（または別個の `kid` の下の 2 つ目）がアウトバウンド Webhook にも署名 — [Webhook callbacks](#webhook-callbacks) を参照。（`"webhook-signing"` は削除保留の非推奨目的 — [#5555](https://github.com/adcontextprotocol/adcp/issues/5555) を参照。後方互換性のため Webhook パスで依然受け入れられる。） |
| `kid`      | distinct               | JWKS 内で一意。`adcp_use` に関わらず他のエントリの `kid` と衝突してはならない（MUST NOT）。                                                                                                                                                                                                                                                                                                                                   |
| `alg`      | `"EdDSA"` or `"ES256"` | 署名の `alg` パラメーターに一致しなければならない（JWK `alg` は JWS 名を使い、`Signature-Input` の `alg` は RFC 9421 名を使う）。                                                                                                                                                                                                                                                                                                   |

クロス目的鍵再利用は禁止され、上記の明示的な Webhook 緩和を除き `adcp_use` を介して**ローカルに強制可能**です: `"request-signing"` 鍵は、RFC 9421 `tag` がそれらのプロファイルを分離するため、リクエストまたは Webhook のどちらにも署名できます。単一の JWK エントリは 1 つの `adcp_use` 値のみを宣言できるので、パブリッシャーはガバナンス署名鍵を有効なリクエスト署名鍵として偶然（または意図的に）提示できません。検証者はフェッチした JWK 上の `adcp_use` を確認し、他の JWKS エンドポイントをまたいでは確認しません — クロスエンドポイントルックアップは要求も許可もされません。

**オリジン分離（ガバナンスは MUST、他は SHOULD）。** `adcp_use` は帯域内判別子です — クロス目的検証を防ぎますが、公開オリジンを防御しません。共有 JWKS エンドポイントのオリジン侵害は、それが公開するすべての署名目的を同時に侵害します。ガバナンス署名鍵はシステムで最も影響範囲の大きい鍵（その侵害はマルチテナント侵害）なので、ガバナンス署名鍵はトランスポート/Webhook RFC 9421 鍵とは別のオリジンから提供されなければなりません（MUST）。正準パターンは:

* `governance-keys.{org}.example/.well-known/jwks.json` — ガバナンス署名 JWK のみ
* `keys.{org}.example/.well-known/jwks.json` — リクエスト署名鍵（Webhook 専用 `kid` を含む）、後方互換性ウィンドウ中の非推奨 Webhook 署名鍵、TMP 鍵

オペレーターはさらに進んで各署名面を別個のサブドメインから提供すべきです（SHOULD）。多層防御: ガバナンス鍵はオフラインローテーション（手動ローテーションと人間承認付き HSM/KMS）にすべきで（SHOULD）、トランスポートと Webhook 鍵は自動ローテーションを使ってもよい（MAY）。オペレーターは `get_adcp_capabilities` に `identity.key_origins` マップを公開して分離スキームを宣伝します。スキーマは `governance_signing`、`request_signing`、`webhook_signing`、`tmp_signing` オリジン URI を定義します。`webhook_signing` は Webhook 配信面を名指し、必要なライブ `adcp_use: "webhook-signing"` 目的ではありません。Webhook がリクエスト署名鍵を使うとき `request_signing` と同じオリジンを指してもよい。実装者は、相手方がオンボーディングでオリジン分離を検証できるよう、フィールドを設定すべきです（SHOULD）。**フィールドが存在する場合、検証者はオンボーディングで宣言されたガバナンス署名オリジンが宣言されたリクエスト/Webhook 署名オリジンと異なることを確認し、共存の場合ユーザーが対処可能なエラーでオンボーディングを拒否しなければなりません（MUST）。** オリジン分離の MUST はそうでなければワイヤー上で検証不能です — 宣伝を公開する要点は、相手方がそれをプログラム的に強制できるようにすることです。規範ルールに違反する宣言を受け入れることは制御を無効にします。検証者は追加で各宣言された JWKS をフェッチしてその `jwks_uri` オリジンが宣伝値に一致することを確認してもよい（MAY）。

**実装者注記:** `adcp_use` はカスタム JWK メンバーです。主要な JOSE ライブラリ（`jose`, `node-jose`, `python-jose`, `go-jose`）はパース時に未知のメンバーを保持します。厳格な JWK バリデーター（`PyJWT` の一部モード、Web Crypto API の `SubtleCrypto.importKey`）は未知のメンバーを拒否するかもしれません。JWK を `SubtleCrypto.importKey` または同等の厳格なコンシューマーに渡すとき、JWK オブジェクトから `adcp_use` を除去しますが、ステップ 8 のポリシーチェックのために保持します。フィールドは暗号ライブラリではなく AdCP 検証者ポリシー向けです。

**署名済みリクエストの JWKS ディスカバリー** — インカミング署名の `keyid` が与えられたとき:

1. 検証者は署名エージェントの URL をその brand.json の `agents[]` エントリに解決します。ディスカバリーは以前のオンボーディングから来てもよく（MAY）、レジストリキャッシュから来てもよい（MAY）が、正準のワイヤー上ブートストラップはエージェントの `get_adcp_capabilities` レスポンスの `identity.brand_json_url` フィールドです — [Discovering an agent's signing keys via `brand_json_url`](#discovering-an-agents-signing-keys-via-brand_json_url) を参照。
2. エージェントの `jwks_uri`（またはエージェントの `url` のオリジンの `/.well-known/jwks.json` にデフォルト）を [Webhook URL validation](#webhook-url-validation-ssrf) に従う SSRF 検証付きでフェッチ。JWKS キャッシュ TTL は失効リストポーリング間隔で上限が制限される。
3. `kid` がキャッシュされた JWKS に不在の場合、JWKS を**即座に**再フェッチ（ステップ 2 の最初のフェッチはキャッシュされていたかもしれない）。同じ `jwks_uri` について過去 30 秒に再フェッチがすでに実行された場合、クールダウンが適用される: 検証者は再度再フェッチしてはならず（MUST NOT）、`request_signature_key_unknown` で拒否しなければならない（MUST）。クールダウンは再フェッチ間であり、最初のフェッチ前ではない。

検証者は、特定の `agents[]` エントリに解決できない `keyid` からの署名を受け入れてはなりません（MUST NOT）— 匿名署名はアカウンタビリティを提供しません。

#### `brand_json_url` を介したエージェントの署名鍵の発見

`get_adcp_capabilities` の `identity.brand_json_url` フィールド（3.x で追加、スキーマ `static/schemas/source/protocol/get-adcp-capabilities-response.json` を参照）は、エージェント → オペレーター → 鍵チェーンのワイヤー上ブートストラップです。フィールド名は、オペレーター構造が単一ブランド、サブブランドを持つハウス、エージェンシー、純粋なオペレーターレコードのいずれかに関わらず、それが指すアーティファクト（オペレーターの `brand.json` ファイル）を反映します。エージェント URL `A` だけが与えられたとき、検証者はエージェントの署名鍵を次で解決します:

1. `A` の `get_adcp_capabilities` レスポンスを [Webhook URL validation](#webhook-url-validation-ssrf) に従う SSRF 検証付きでフェッチ（HTTPS のみ — URL `A` は呼び出し元が供給し、Webhook コールバックに使う同じアドレスファミリー + プライベート IP フィルタリングを通らなければならない）。到達不能/タイムアウトでは `request_signature_capabilities_unreachable` で拒否。
2. `identity.brand_json_url` を読む。不在でリクエストが署名されている場合、`request_signature_brand_json_url_missing` で拒否。値が非 HTTPS の場合も同じコードで拒否（スキーマは `^https://` を強制するが、検証者はチェックを再表明しなければならない。不正な値を許容する 3.x パーサーは続行してはならない、MUST NOT）。required-when ルール: `identity.brand_json_url` は、エージェントが `request_signing.supported_for`/`required_for` を非空、`webhook_signing.supported === true`、または `identity.key_origins` の下の任意のフィールドを宣言するとき存在しなければならない（MUST）。これは 3.x でストーリーボード強制。4.0 ではレスポンスが 4.x リリースを含む `supported_versions` を宣言するときスキーマ必須になる。クロスバージョン検証者（4.x サポートを宣伝しない 3.x エージェントと通信する 4.0）は不在の `identity.brand_json_url` を受け入れ続けなければならない（MUST）。
3. **オリジンバインディング。** エージェント URL `A` のホスト eTLD+1 は `brand_json_url` のホスト eTLD+1 と等しくなければならない（MUST）。eTLD+1 計算はピン留めされた日付付き [Public Suffix List](https://publicsuffix.org/list/public_suffix_list.dat) スナップショットを使わなければならない（MUST、`vercel.app`, `pages.dev`, `github.io` のようなプラットフォームがサフィックスとして扱われるよう ICANN+PRIVATE セクション両方をスコープ）。異なる PSL バージョンを実行する 2 つの検証者は互いに非コンフォーマント。eTLD+1 が不一致の場合、brand.json をフェッチして `authorized_operators[]` が `A` の eTLD+1 をリストすることを確認。どちらも成立しなければ `request_signature_brand_origin_mismatch` で拒否。これは、攻撃者が `attacker.example/mcp` にエージェントを立て、その `brand_json_url` を、たまたま正当に `attacker.example/mcp` をリストする無関係なオペレーターの brand.json（例: SaaS マルチテナントデプロイ）に向ける共有テナントスプーフィングベクトルを閉じる。
4. `brand_json_url` の brand.json を [Webhook URL validation](#webhook-url-validation-ssrf) に従う SSRF 検証付きでフェッチ。検証者はこのフェッチでリダイレクトに従ってはならない（MUST NOT、このプロファイルの他所で文書化された `authoritative_location` の単一リダイレクト切り出しはそのフィールドにスコープされ、brand.json ブートストラップに継承されてはならない）。推奨予算: 接続 5 秒、総デッドライン 10 秒、ボディ上限 256 KiB。成功フェッチのキャッシュ TTL は JWKS 失効ポーリング間隔で上限が制限されなければならない（MUST、鍵ローテーションが古い brand.json でマスクされないように）。負のレスポンス（404、ネットワーク失敗）は 60 秒以上キャッシュされてはならない（MUST NOT）— 誤設定を修正するオペレーターが完全な失効サイクルの間ロックアウトされてはならない。
5. `url` が `A` と**バイト等価**な `agents[]` エントリを見つける（このステップで正準化なし — ガバナンス JWS の `iss`-to-brand.json 一致と同じルール、[Buyer identity resolution](#buyer-identity-resolution) を参照。最も一般的な失敗モードは末尾スラッシュまたはスキーム不一致、例: `https://x.com/mcp` ≠ `https://x.com/mcp/`）。一致しなければ `request_signature_agent_not_in_brand_json` で拒否。複数一致する場合（オペレーター誤設定 — brand.json スキーマは現在 `agents[]` を URL 一意に制約しない）、`request_signature_brand_json_ambiguous` で拒否。
6. JWKS ソースを**署名面 AND 役割**（`adcp_use` だけでなく送信者 vs 受信者の位置）で解決:
   * **セルサイド Webhook 配信のみ** — すなわち、セラーがメディアバイ配信についてバイヤーへのアウトバウンド Webhook に署名: パブリッシャーの `adagents.json signing_keys` ピン（存在する場合）は上記のパブリッシャーピン優先順位ルールに従って権威的で、下記のすべてを上書きする。ピンは（エージェント、Webhook 配信面、セルサイド役割）にスコープされる — オペレーター側 Webhook 配信（例: オペレーターステータスコールバックを受け取るバイヤーホストの Webhook）を上書きせず、別個の `adcp_use: "webhook-signing"` 鍵目的を示唆しない。
   * **他のすべての（面、役割）タプル** — リクエスト署名（任意の方向）、オペレーター側 Webhook 配信、ガバナンス署名、TMP 署名: 一致した `agents[]` エントリの `jwks_uri` を使い、不在時は `A` のオリジンの `/.well-known/jwks.json` にデフォルト。
7. **`identity.key_origins` 一貫性チェック（署名時は必須）。** ケイパビリティレスポンスの `identity.key_origins` の下で宣言され、**ステップ 6 での JWKS ソースがオペレーター brand.json だった**（すなわち、パブリッシャー `adagents.json signing_keys` ピンでない）すべての面/目的について、解決された `jwks_uri` のホストはその面/目的について宣言されたオリジンと等しくなければならない（MUST）。任意の面/目的での不一致 → `{ purpose, expected_origin, actual_origin }` を運ぶ `request_signature_key_origin_mismatch` で拒否。ソースがパブリッシャーピンだった特定の（エージェント、面/役割）タプルについて**のみ**チェックをスキップ — 同じ面のオペレーター側使用は依然チェック。エージェントが対応する `identity.key_origins.{purpose}` エントリなしに署名を宣言する場合、`{ purpose, posture }` を運ぶ `request_signature_key_origin_missing` で拒否。
8. JWKS をフェッチ、`kid` を見つけ、既存の RFC 9421 プロファイル（[verifier checklist](#verifier-checklist-requests) のステップ 7 以降）に従って検証。

**トラストルート。** brand.json はオペレーター証明（「このエージェントは私のもの、これがその鍵」）。`adagents.json` はパブリッシャー証明（「このエージェントは私のインベントリを販売してよい。オプションで、これがそのピン留め `signing_keys`」）。セルサイド Webhook 署名では、パブリッシャーピンが権威的（パブリッシャー > オペレーター）。リクエスト署名とオペレーター側 Webhook 署名では、オペレーター brand.json の `jwks_uri` が権威的。エージェントは自身の鍵を決して自己証明しない — `jwks_uri` フィールドは意図的にケイパビリティレスポンスに運ばれない。オペレーターは brand.json 経由で帯域外に鍵を公開する。

**`sponsored_intelligence.brand_url` は別物。** SI エージェントはレンダリング目的（色、フォント、ロゴ、トーン）で `sponsored_intelligence` の下に `brand_url` フィールドを運んでもよい — フィールドが `brand_url` と名付けられているのは、SI コンテキストではそれが本当に「広告されているブランド」だからだ。そのフィールドはレンダリングポインターであり、トラストルートポインターではない。SI エージェントは `sponsored_intelligence.brand_url` を `identity.brand_json_url` と異なる URL に設定してもよい（MAY、例: レンダリング用のサブブランド brand.json、鍵についてはオペレーターの brand.json を依然信頼）。**検証者は鍵ディスカバリーに `identity.brand_json_url` を使わなければならず（MUST）、`identity.brand_json_url` が不在でも `sponsored_intelligence.brand_url` をトラストルートポインターとして使ってはならない（MUST NOT）。** SI レンダリングメタデータを消費する検証者は `sponsored_intelligence.brand_url` を読んでもよい（MAY）。同じ検証者は任意の署名検証フローで `identity.brand_json_url` に切り替えなければならない（MUST）。命名の区別は意図的: 「広告されているブランド」コンテキストには `brand_url`、「オペレーターマスターレコード」コンテキストには `brand_json_url`。

**このディスカバリーチェーンの拒否コード（3.x）。** 相手方ドキュメント（`brand_json_url`, `matched_entries[]`）由来の詳細フィールドは、検証者エラーを表示する管理 UI でレンダリングする前に HTML エスケープしなければならない（MUST）— 構造化形状は検証者制御でも、攻撃者が影響を与えられる文字列。

| Code                                         | When                                                                       | Detail fields                                                       | Remediation                                                                                                                                      |
| -------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `request_signature_brand_json_url_missing`   | ケイパビリティが `identity.brand_json_url` を運ばず署名済みリクエストを受信、または非 HTTPS 値を運んだ       | `agent_url`                                                         | オペレーター: `identity.brand_json_url` をオペレーター brand.json の HTTPS URL（通常 `https://{your-domain}/.well-known/brand.json`）に設定。検証者: オペレーションに表面化。リトライしない。 |
| `request_signature_capabilities_unreachable` | ケイパビリティフェッチ失敗（DNS、TCP、TLS、タイムアウト、非 2xx）                                    | `agent_url`, `http_status`, `dns_error`, `last_attempt_at`          | 検証者は 1-5 秒のジッター付きバックオフ後 1 回リトライしてよい、その後諦める。60 秒以上ネガティブキャッシュしない。一時的として表面化。                                                                        |
| `request_signature_brand_json_unreachable`   | brand.json フェッチ失敗（同条件）                                                     | `brand_json_url`, `http_status`, `dns_error`, `last_attempt_at`     | `_capabilities_unreachable` と同じリトライ/キャッシュ規律。                                                                                                     |
| `request_signature_brand_json_malformed`     | brand.json が strict-parse 失敗（重複キー、ボディ上限超過、非 JSON コンテンツ）                    | `brand_json_url`, `parse_error`                                     | オペレーター: 重複オブジェクトキーなし、256 KiB ボディ上限内の strict-JSON brand.json を提供。検証者: リトライしない。オペレーションに表面化。                                                        |
| `request_signature_brand_origin_mismatch`    | エージェント eTLD+1 ≠ `brand_json_url` eTLD+1 かつ `authorized_operators[]` が委任しない | `agent_url`, `agent_etld1`, `brand_json_url_etld1`                  | オペレーター: エージェントをブランド eTLD+1 に移すか、エージェント eTLD+1 を brand.json `authorized_operators[]` に追加。リトライ不可。                                                  |
| `request_signature_agent_not_in_brand_json`  | エージェント URL が解決された brand.json の任意の `agents[].url` とバイト等価でない                 | `agent_url`, `brand_json_url`                                       | オペレーター: エージェント URL を `agents[].url` にバイト等価で追加。一般的原因: 末尾スラッシュ、スキーム不一致、IDN/punycode 正規化。リトライ不可。                                                    |
| `request_signature_brand_json_ambiguous`     | 複数の `agents[]` エントリがエージェント URL に一致                                         | `agent_url`, `brand_json_url`, `matched_count`, `matched_entries[]` | オペレーター: URL で `agents[]` エントリを重複排除。リトライ不可。                                                                                                       |
| `request_signature_key_origin_mismatch`      | 解決された `jwks_uri` ホスト ≠ 宣言された `identity.key_origins.{purpose}`              | `purpose`, `expected_origin`, `actual_origin`                       | オペレーター: `identity.key_origins.{purpose}` を解決された `jwks_uri` のホストに合わせる。リトライ不可。                                                                     |
| `request_signature_key_origin_missing`       | 署名姿勢を宣言したが `identity.key_origins.{purpose}` が不在                            | `purpose`, `posture`                                                | オペレーター: `identity.key_origins.{purpose}` 宣言をケイパビリティに追加。リトライ不可。                                                                                   |

<Info>
  **AdCP 3.0 にピン留めしたまま `brand_json_url` を採用。** フィールドは 3.x の次のマイナーに厳密に追加的なスキーマ変更として着地します。AdCP はパッチリリース（3.0.x）で新しいフィールドを出荷しないので、正式なバックポートは検討対象外です。しかしバージョンバンプを待たずに使い始められます。ワイヤー形状は前方互換です:

  * 3.0 コンフォーマントな**セラー**は今日 `get_adcp_capabilities` レスポンスに `identity.brand_json_url` を設定してもよい（MAY）。フィールドを無視する 3.0 検証者は動き続け、3.x 検証者は自動的にそれを拾う。調整もバージョンバンプも不要。
  * 3.0 コンフォーマントな**検証者**は、フィールドを日和見的に読み（`caps.identity?.brand_json_url` 経由）、存在するとき 8 ステップチェーンを実行し、不在時は既存の帯域外エージェント → オペレーターマッピングにフォールバックしてもよい（MAY）。チェーン自体は HTTPS フェッチと JSON パースだけ — その中に 3.x SDK を要するものはない。

  これは今日署名検証を構築する [Scope3](https://github.com/scope3) のようなセラーの推奨パスです: ケイパビリティレスポンスにフィールドを出荷し、相手方にチェーンを文書化し、3.x ロールアウトを受動的に起こさせます。
</Info>

##### クイックスタート: `brand_json_url` ベースの検証者を実装

上記の [request-signing quickstart](#quickstart-opt-into-request-signing-in-30) をミラーします。エージェントごとに一度実行 — 結果の `agents[]` エントリ、`jwks_uri`、JWKS はステップ 4 の TTL ルールに従ってキャッシュされます。

1. 署名エージェントの URL `A` の**ケイパビリティをフェッチ**。これは**プロトコルレベル**呼び出し — `A` に対する生の HTTP `GET` ではなく、エージェントの宣言されたトランスポート（MCP `tools/call` または A2A スキル呼び出し）を介して `get_adcp_capabilities` を呼び出す。エージェント URL は JSON ケイパビリティドキュメントではなくプロトコルエンドポイント。[Webhook URL validation](#webhook-url-validation-ssrf) に従う SSRF セーフトランスポートを使う: HTTPS のみ、アドレスファミリー + プライベート IP フィルタリング、リダイレクトなし、予算 `{ connect: 5000, total: 10000, body: MAX_CAPABILITIES_BYTES, maxRedirects: 0 }`。
2. **`identity.brand_json_url` を読む。** 不在（かつリクエストが署名済み）または非 HTTPS なら `request_signature_brand_json_url_missing` で拒否。
3. **eTLD+1 オリジンバインディング。** ピン留め PSL スナップショットを使って `eTLD+1(A)` と `eTLD+1(brand_json_url)` を計算。ベンダー済みの日付付きスナップショットで [`tldts`](https://www.npmjs.com/package/tldts)（TS）、[`publicsuffixlist`](https://pypi.org/project/publicsuffixlist/)（Python）、または [`golang.org/x/net/publicsuffix`](https://pkg.go.dev/golang.org/x/net/publicsuffix)（Go）を使う。ランタイムで PSL をフェッチしない — ランタイムフェッチは DoS オラクルとデプロイ間で非決定論的な eTLD+1 を作る。一致すれば続行。そうでなければ `brand.json` をフェッチして `authorized_operators[]` を確認 — `eTLD+1(A)` が委任されていれば続行。そうでなければ `request_signature_brand_origin_mismatch` で拒否。このアルゴリズム全体のオリジン比較は両側を正準化しなければならない（MUST）: ホストを ASCII 小文字化し、バイト等価前に IDNA-2008 A-label 形式（Punycode）に変換。非正準比較（例: 生の `Example.COM` vs `example.com`、または U-label vs A-label）は正当なトラフィックを黙って拒否する。
4. 同じ SSRF ルール + リダイレクトなし、ボディ上限 `MAX_BRAND_JSON_BYTES`、接続 5 秒、総 10 秒で **`brand.json` をフェッチ**。重複キーを拒否する strict JSON パーサー（例: TS の [`secure-json-parse`](https://www.npmjs.com/package/secure-json-parse)、重複で raise する `object_pairs_hook` 付きの Python stdlib `json.JSONDecoder`、Go の重複キーチェックと組み合わせた [`encoding/json`](https://pkg.go.dev/encoding/json) `Decoder.DisallowUnknownFields`）でパース — 重複キーはステップ 14 がリクエスト面で閉じるパーサー差分ベクトルで、同じトラストルートドキュメントが検証者間で 2 つの異なる形状にパースされてはならない（MUST NOT）。重複キー検出で `request_signature_brand_json_malformed` で拒否。成功レスポンスを JWKS 失効ポーリング間隔まで（それより長くない）キャッシュ。失敗は最大 60 秒キャッシュ。
5. `url` が `A` とバイト等価な **`agents[]` エントリを見つける**（正準化なし）。ミスで `request_signature_agent_not_in_brand_json`、複数一致で `request_signature_brand_json_ambiguous` を拒否。
6. 一致したエントリから **`jwks_uri` を解決** — セルサイド Webhook 配信のみ、オペレーターの `jwks_uri` よりパブリッシャーの `adagents.json signing_keys` ピン（存在する場合）を優先。他のすべての（面、役割）タプルは、一致したエントリの `jwks_uri`（デフォルト: `A` のオリジンの `/.well-known/jwks.json`）を使う。
7. **一貫性チェック。** ケイパビリティ `identity.key_origins` の下で宣言されたすべての面/目的について、解決された `jwks_uri` ホストと宣言オリジンの両方に `canonicalizeOrigin()`（ASCII 小文字 + IDNA-2008 A-label）を適用し、バイト比較（パブリッシャーピン由来の特定の（エージェント、面/役割）タプルのみスキップ）。適宜 `request_signature_key_origin_mismatch` / `_missing` を拒否。
8. **[verifier checklist](#verifier-checklist-requests) のステップ 8 以降にハンドオフ** — JWKS をフェッチ（同じバイト予算 `MAX_JWKS_BYTES` と 5/10 秒の接続/総デッドライン）、`kid` を見つけ（ここでステップ 7 のプリアンブルですでに解決済み — 検証者チェックリストのステップ 7 はディスカバリープリアンブル自体）、RFC 9421 に従って検証。

疑似コード（TypeScript 風。下記の SDK ヘルパーはこれを単一呼び出しに折りたたむ）:

```ts theme={null}
const MAX_CAPABILITIES_BYTES = 65_536;
const MAX_BRAND_JSON_BYTES   = 262_144;
const MAX_JWKS_BYTES         = 65_536;
const FETCH_BUDGETS          = { connect: 5_000, total: 10_000, maxRedirects: 0 };

function canonicalizeOrigin(hostOrUrl: string): string {
  const host = hostOrUrl.includes('://') ? new URL(hostOrUrl).hostname : hostOrUrl;
  return toAsciiIdna2008(host.toLowerCase());                                 // A-label form
}

async function resolveAgent(agentUrl: string): Promise<AgentResolution> {
  const caps = await getAdcpCapabilities(agentUrl, {                          // step 1: protocol-level call
    ...FETCH_BUDGETS, body: MAX_CAPABILITIES_BYTES, ssrf: true,
  });
  const brandJsonUrl = caps.identity?.brand_json_url;
  if (!brandJsonUrl?.startsWith('https://')) throw new Err('brand_json_url_missing');  // step 2
  const agentEtld1 = etldPlusOne(new URL(agentUrl).hostname, PINNED_PSL_SNAPSHOT);     // step 3
  const brandEtld1 = etldPlusOne(new URL(brandJsonUrl).hostname, PINNED_PSL_SNAPSHOT);
  const brandJson = await safeFetch(brandJsonUrl, {                            // step 4
    ...FETCH_BUDGETS, body: MAX_BRAND_JSON_BYTES, ssrf: true, parse: 'strict-json',
  });
  if (agentEtld1 !== brandEtld1
      && !brandJson.authorized_operators?.some(o => o.domain === agentEtld1)) {
    throw new Err('brand_origin_mismatch');
  }
  const entries = brandJson.agents.filter(e => e.url === agentUrl);            // step 5 (byte-equal)
  if (entries.length === 0) throw new Err('agent_not_in_brand_json');
  if (entries.length > 1) throw new Err('brand_json_ambiguous');
  const entry = entries[0];
  const jwksUri = entry.jwks_uri ?? `${origin(agentUrl)}/.well-known/jwks.json`;  // step 6
  for (const [purpose, declared] of Object.entries(caps.identity?.key_origins ?? {})) { // step 7
    if (canonicalizeOrigin(jwksUri) !== canonicalizeOrigin(declared)) {
      throw new Err('key_origin_mismatch', { purpose });
    }
  }
  const jwks = await safeFetch(jwksUri, {                                      // step 8 setup
    ...FETCH_BUDGETS, body: MAX_JWKS_BYTES, ssrf: true, parse: 'strict-json',
  });
  return { agentUrl, brandJsonUrl, agentEntry: entry, jwksUri, jwks, /* trace, freshness */ };
}
```

公開されたら [`/compliance/latest/test-vectors/brand-discovery/`](https://adcontextprotocol.org/compliance/latest/test-vectors/brand-discovery/) の brand-discovery テストベクターに対してエンドツーエンド検証。それまで、`/compliance/latest/universal/capabilities-brand-url-discovery/` のストーリーボードがフィクスチャ brand.json + JWKS に対して検証者アルゴリズムを行使し、各エラーパスに正しい `request_signature_*` コードをアサートします。

##### リファレンス実装

8 ステップアルゴリズムは 3 つの SDK で出荷されます — ランタイムに合うものを選びます。3 つとも同じ論理レコードを返します: エージェント URL、解決された brand.json URL、一致した `agents[]` エントリ、JWKS URI、JWKS 自体、ケイパビリティレスポンスの `identity_posture` ブロック、ステップ 7 の `key_origins` チェックからの `consistency` フラグ、`freshness` タイムスタンプセット、ステップごとの `trace`。

* **TypeScript**（[`@adcp/sdk`](https://github.com/adcontextprotocol/adcp-client)）: `resolveAgent(url)` は `{ agentUrl, brandJsonUrl, agentEntry, jwksUri, jwks, identityPosture, consistency, freshness, trace }` を返す。`getAgentJwks(url)` は JWKS のみの高速パス。`createAgentJwksSet(url, opts)` は `jose` の `jwtVerify` に渡す `JWTVerifyGetKey` を返す。
* **Python**（[`adcp`](https://github.com/adcontextprotocol/adcp-client-python)）: `resolve_agent(url)` は `agent_url`, `brand_json_url`, `agent_entry`, `jwks_uri`, `jwks`, `identity_posture`, `consistency`, `freshness`, `trace` フィールドを持つ `AgentResolution` データクラスを返す。`verify_request_signature(request, *, agent_url, allowed_algs)` はディスカバリーチェーンと [verifier checklist](#verifier-checklist-requests) を 1 呼び出しで実行するワンショットヘルパー。
* **Go**（[`adcp-go`](https://github.com/adcontextprotocol/adcp-go)）: `ResolveAgent(ctx, agentURL) (*AgentResolution, error)` は `AgentURL`, `BrandJSONURL`, `AgentEntry`, `JWKSUri`, `JWKS`, `IdentityPosture`, `Consistency`, `Freshness`, `Trace` フィールドを持つ構造体を返す。`VerifyRequestSignature(ctx, req, opts) (*VerifiedIdentity, error)` は TS/Python のワンショットをミラー。

各 SDK は開発ループデバッグ用の CLI を出荷します — `npx @adcp/sdk@latest resolve <url>`、`adcp resolve <url>`（`python -m adcp resolve <url>` も）、`adcp resolve <url>`（Go バイナリ、Python と同名 — `$PATH` またはベンダーで区別）— ステップごとの `fetched_at`/`age_seconds`/`ok` 付きのトレースを表示し、`request_signature_brand_*` 失敗をトリアージするオペレーターがどのステップが拒否したかとその理由を正確に見られます。Python（`[project.scripts]` console\_scripts エントリ）と Go（バイナリ `adcp`、Go モジュールパス `github.com/adcontextprotocol/adcp-go` とは別）のツールチェーンは両方ともトップレベル `adcp` コマンドをインストールするので、単一のマッスルメモリ呼び出しがランタイムをまたいで機能します。

#### エージェントアイデンティティ

有効な署名はちょうど 1 つの事実を確立します: **リクエストは `jwks_uri` が `keyid` を含むエージェントが発行した。** 検証者は、どのオペレーターかだけでなく、どの特定のエージェントが署名したかを学びます。エージェントを含む brand.json（検証者の既存のエージェントマッピングを介して発見）が、どのオペレーターがそのエージェントを運用するかを検証者に伝えます。

**`agent_url` の導出。** 検証者のリクエストコンテキスト上の正準バイヤーエージェント識別子は、[verifier checklist](#verifier-checklist-requests) のステップ 7 で `keyid` を解決した `jwks_uri` を持つ `agents[]` エントリの `url` フィールドです。`agent_url` は JWK クレーム、JWS クレーム、署名済みエンベロープフィールドでは**ありません** — 検証者が JWKS をフェッチするためにすでに使った公開座標です。これは、検証者が完全に制御した入力（オンボーディングで確立されたエージェントマッピング、加えて今フェッチした JWKS）から導出を決定論的にし、署名者がリクエストに署名した鍵のものとは異なる `agent_url` を主張するワイヤーアフォーダンスを取り除きます。解決済み署名者オブジェクトをアダプターに表面化する SDK は、`agent_url` をこの導出からソースしなければならず（MUST）、エンベロープ上のバイヤー主張 `agent_url` フィールドを受け入れて暗号学的に確立されたものとして扱ってはなりません（MUST NOT）。（`creative.verify_agent.agent_url` や `governance.accepted_verifiers[].agent_url` のようなバイヤー主張の*検証者*参照は別の構造 — それらは公開された許可リストの下でセラーが呼び出すエージェントを名指し、インバウンドリクエストの署名者ではなく、許可されたまま。）

認可 — このオペレーターがリクエストボディで名指しされたブランドのために行動を許可されるか — は、ターゲットハウスの brand.json の `authorized_operator[]` エントリが規定する別個のプロトコルレベルチェックです。リクエストが署名されているかに関わらず発生し、このプロファイルのスコープ外です。検証者は両方のチェックを実行しなければなりません（MUST）。このセクションは最初のもののみを規定します。

検証者はリクエストボディフィールドから署名者アイデンティティを導出してはなりません（MUST NOT）。署名 → JWKS → エージェントエントリチェーンが署名済みトランスポート上の唯一の権威的アイデンティティパスです。bearer / API キー / OAuth トランスポートでは、エージェントアイデンティティはセラーのオンボーディングレコードのクレデンシャル-トゥ-エージェントマッピングから来ます — そのマッピングが唯一の正当なアイデンティティソースです。セラーはアイデンティティ解決への代替入力としてエンベロープ側の `buyer_agent_url`（または同等の自己主張呼び出し元アイデンティティフィールド）を導入してはなりません（MUST NOT）: ワイヤーアフォーダンスは、相殺チェックなしに、呼び出し元がクレデンシャルマップが主張しないアイデンティティを主張することを許します。

brand.json ディスカバリーは 1 リダイレクト（`authoritative_location`）に従って停止します。

#### 検証者チェックリスト（リクエスト）

**チェックリストを適用する前に、検証者はオペレーションが署名を要求するかを判定しなければなりません（MUST）:**

* オペレーションが検証者の `required_for` ケイパビリティにあり、AND `Signature-Input` ヘッダーが存在せず、AND 呼び出し元がこのオペレーションについて検証者が受け入れる他のクレデンシャル（bearer、API キー、mTLS）を提示しない場合、`request_signature_required` で拒否。このブランチに入る未署名リクエストは決してチェックリストに入りません。未署名だが他の方法で認証された呼び出し元を規定するルールは [Composition with fallback authenticators](#composition-with-fallback-authenticators) を参照。
* `Signature` または `Signature-Input` のどちらかが他方なしに存在する場合、`request_signature_header_malformed` で拒否。2 つのヘッダーはバインドされたペアです。一方が他方なしは不正で、「推測できる欠けた部分で署名された」ではありません。このルールは、プロキシが `Signature-Input` を除去し `Signature` を残すダウングレードベクトルを閉じます。
* `Signature-Input` ヘッダーが存在するが不正な場合、`request_signature_header_malformed` で拒否。検証者は、不正な署名が存在するとき、**`required_for` にないオペレーションでも**、bearer のみの認証にフォールバックしてはなりません（MUST NOT）— 存在するが壊れた署名は署名者の意図を示します。黙ったフォールバックはダウングレード攻撃を可能にします。

そうでなければ、検証者はこれら 15 のチェック（14 の番号付きステップとサブステップ 9a）を順に適用し、最初の失敗でショートサーキットしなければなりません（MUST）。ステップ 14 は 14a（strict-parse 要件）と 14b（ロギング規律）に分解されます — 両方ともステップ 14 が実行されるときに適用され、1 つのチェックの詳述であり、カウント上別個のチェックではありません。このチェックリストはエージェントアイデンティティのみを確立します — ブランド-オペレーター認可はターゲットハウスの brand.json が規定する別個の後続チェックです。

1. `Signature-Input` と `Signature` ヘッダーを RFC 9421 §4 に従ってパース。不正なら拒否。

2. `created`, `expires`, `nonce`, `keyid`, `alg`, `tag` のいずれかが `Signature-Input` パラメーターから不在なら拒否（`request_signature_params_incomplete`）。

3. `tag` が正確に `adcp/request-signing/v1` でないなら拒否（`request_signature_tag_invalid`）。

4. `alg` が許可リスト（`ed25519`, `ecdsa-p256-sha256`）にないなら拒否。ライブラリのデフォルトに頼ってはならない（`request_signature_alg_not_allowed`）。

5. `expires ≤ created`、`created > now + 60 s`、`expires < now − 60 s`、または `expires − created > 300 s` なら拒否（`request_signature_window_invalid`）。

6. カバーされるコンポーネントが `@method`, `@target-uri`, `@authority` のすべてを含まないなら拒否（`request_signature_components_incomplete`）。ボディが存在する場合、`content-type` がカバーされていないなら拒否。検証者の `covers_content_digest` ケイパビリティが `"required"` なら、`content-digest` がカバーされていないなら拒否。検証者の `covers_content_digest` ケイパビリティが `"forbidden"` かつ `content-digest` がカバーされて*いる*なら、`request_signature_components_unexpected` で拒否。

7. `keyid` を [Agent key publication](#agent-key-publication) を介して JWK に解決。検証者が署名エージェントのキャッシュされたエージェント → JWKS マッピングを持たない場合、このステップの前に [Discovering an agent's signing keys via `brand_json_url`](#discovering-an-agents-signing-keys-via-brand_json_url) を実行 — その 8 ステッププリアンブル（ケイパビリティ → `identity.brand_json_url` → brand.json → agents\[] → jwks\_uri）は `keyid` 解決の前提条件で、そのセクションの `request_signature_brand_*` と `request_signature_key_origin_*` コードでショートサーキット。確立されたマッピング内の `kid` ミスでは、`request_signature_key_unknown` で拒否する前に 1 回再フェッチ（再フェッチ間の 30 秒クールダウンに従う）。`keyid` が特定の `agents[]` エントリに解決できないなら拒否。

8. JWK の `use` が `"sig"`、`key_ops` が `"verify"` を含み、`adcp_use` が `"request-signing"` に等しいことを検証。任意の不一致（不在の `adcp_use` を含む。非コンフォーマントとして扱わなければならない）で拒否（`request_signature_key_purpose_invalid`）。

9. [Transport revocation](#transport-revocation) リストを確認。`keyid` ∈ `revoked_kids` なら拒否（`request_signature_key_revoked`）。検証者が grace 内に失効リストをリフレッシュしていないなら `request_signature_revocation_stale` で拒否。

   **9a. keyid ごとの上限チェック。** [keyid ごとのリプレイキャッシュ上限](#transport-replay-dedup)を確認。この `keyid` について上限に達しているなら `request_signature_rate_abuse` で拒否。暗号検証（ステップ 10）の前に実行 — ステップ 9 と同じ根拠: 上限を枯渇させる侵害されたまたは誤設定された署名者が、増幅された Ed25519/ECDSA 作業を検証者に強制してはならない（MUST NOT）。`keyid` 解決（ステップ 7）の*後*に実行し、上限状態オラクルが検証者がすでに認識をコミットした鍵についてのみ応答するように — 9a を早く実行すると、攻撃者が JWKS に公開されていない keyid を含む全 keyid 空間で検証者内部のレート制限状態を探れる。

10. [上記のプロファイル](#adcp-rfc-9421-profile)に従い `@target-uri` 正準化 AND `@authority` 導出を適用した後、カバーされるコンポーネントを使って RFC 9421 §2.5 に従い正準署名ベースを計算。**`@authority` ルールは要:** 検証者は、存在する場合 HTTP/2+ の `:authority` 疑似ヘッダーから、そうでなければ受信した HTTP/1.1 `Host` ヘッダーから `@authority` を導出しなければならない（MUST）— リバースプロキシルーティング状態、ロードバランサーメタデータ、フォワードプロキシが転送中に書き換えた任意の `Host` 値からではない。受信リクエストに `:authority` と `Host` の両方が存在する場合、正準化後にバイト等価でなければならない（RFC 7540 §8.1.2.3 等価性）。発散は `request_target_uri_malformed` で拒否。正準化された `@authority` は正準 `@target-uri` のオーソリティコンポーネントとバイト単位で一致しなければならない（MUST）。ミスマッチは `request_target_uri_malformed` で拒否。署名済み `@target-uri` に対するそのバイト一致 — ソースヘッダーの選択ではなく — が唯一の安全ゲートです。`Host` 自体が転送中に書き換えられ得るからです。このチェックリストだけから — プロファイルの正準化セクションを相互参照せずに — 構築する実装者はこのルールを適用しなければならない（MUST）。それをスキップするとクロス vhost リプレイベクトル（攻撃者が TLS 終端されたリクエストを傍受し、同じ検証者プール上の 2 つ目の vhost にリプレイ: 同じ証明書 SAN、異なる `Host`）を黙って受け入れる。正準化完了後、JWK に対して署名を検証（失敗で `request_signature_invalid`）。

11. `content-digest` がカバーされている場合、受信したボディバイトからダイジェストを再計算して比較（ミスマッチで `request_signature_digest_mismatch`）。

12. リプレイキャッシュに対してノンスを確認（[Transport replay dedup](#transport-replay-dedup) を参照）。`(keyid, nonce)` がリプレイキャッシュ TTL 内で見られている場合拒否（`request_signature_replayed`）。

13. **ステップ 1-9、9a、10-12 がすべて通過した後にのみ**、`(keyid, nonce)` を TTL = `(expires − now) + 60 s`（+60 秒はステップ 5 で適用したスキュー許容に一致）でリプレイキャッシュに挿入。この挿入はステップ 14 のボディ整形式チェックの前に発生しなければならない（MUST）。不正なボディ上に有効な署名を運ぶキャプチャされたフレームが、各リトライで暗号検証 CPU を燃やすためにリプレイできないように — ノンスは、ボディ形状に関わらず、暗号学的に有効なフレームの最初の目撃で燃やされる。

14. **ボディ整形式性。** 検証者は重複オブジェクトキーを含むボディを拒否しなければならない（MUST、`request_body_malformed`）。RFC 8259 §4 に従い、重複キーパース動作は予測不能 — 署名はワイヤー上のバイトに対して有効だが、2 つのパーサーがパースされた値について一致しないことがあり、これはパーサー差分攻撃クラス（cf. CVE-2017-12635）。このチェックは、署名検証者のペイロードのビューとダウンストリームコンシューマーのビューの間のギャップを閉じる。リクエストボディは、パーサー差分の影響範囲が Webhook のステータスフリップの影響範囲より大きい状態変更・支出コミットペイロード（`create_media_buy`, `update_media_buy_delivery` など）を運び、このチェックを少なくとも Webhook 面と同じくらい要にする。`request_body_malformed` は `request_signature_digest_mismatch` とは別: 署名は有効。ボディが曖昧な状態にパースされる。構造化 `request_body_malformed` エラーを返すのではなくクラッシュする検証者はコンフォーマントだが最適でない — 送信者は actionable なエラーコードを受け取らない。**Idempotency\_key カバレッジはこのチェックから従う**: ステップ 14 はスキーマ検証と冪等性キャッシュルックアップ（[idempotency](#idempotency) を参照）の前に実行されるので、`idempotency_key` 自体が重複する（異なるパーサーが異なるキーを見る）リクエストボディはここで拒否され、決してキャッシュに到達しない。別個の冪等性層監査は不要。

    **14a. Strict-parse 要件。** チェックは重複キーを露出するパーサーを使わなければならない（MUST）— それらを黙って破棄する last-wins/first-wins のデフォルトはこの要件を満たさない。[Webhook 検証者チェックリストのステップ 14a](#webhook-callbacks)の言語ごとの strict-parse エスケープハッチ列挙がここに同一に適用される。

    **14b. ロギング規律。** 検証者は `request_body_malformed` 拒否で完全なリクエストボディバイトをログすべきではない（SHOULD NOT）。`keyid`、ノンス、バイト長、特定の重複キー名のみをログ。[Webhook 検証者チェックリストのステップ 14b](#webhook-callbacks)のキー名サニタイズルール（最初の非印字文字で `<sanitized:N>` に切り詰め、32 バイト以下の最後の UTF-8 コードポイントに切り詰め、数を 4 で上限）がここに同一に適用される — 攻撃者制御バイトチャネルはリクエスト面で同じ形状を持つ。

14 のチェックすべてが通過した後にのみ、検証者はリクエストを暗号学的に認証されたものとして扱います。検証者はリクエストコンテキストに `verified_signer: { keyid, agent_url, verified_at }` を記録すべきです（SHOULD）。ダウンストリームコード — 後続のブランド-オペレーター認可チェックを含む — が署名済みエージェントアイデンティティでログ・監査できるように。

**暗号検証前の安価な拒否（ステップ 10 の前のステップ 9 と 9a）は意図的です。** 検証者が最初に暗号をチェックすると、失効鍵署名をリプレイする攻撃者 — または keyid ごとの上限が満杯の検証者をハンマーする署名者 — が、各拒否で Ed25519 または ECDSA 検証を強制し、安価な増幅になります。失効と keyid ごとの上限を前に移すことがその O(verify) → O(1) ギャップを閉じます。ステップ 9 の失効状態はすでに署名者のオリジンで外部公開されています。ステップ 9a の上限状態は検証者内部ですが、持続的な攻撃者によるトラフィックパターン分析で観測可能です。スペックは、上限観測が黙ったオラクルではなくインシデントシグナルとして表面化するよう、別個の `request_signature_rate_abuse` エラーコードを `SHOULD alert operators` 要件（[Transport replay dedup](#transport-replay-dedup) を参照）と意図的にペアにします — 侵害鍵イベントは、それを引き起こした攻撃者にも読めても、オペレーターにとって大きくあるべきです。

**上限の要となる不変条件。** 秘密鍵なしの外部トラフィックは上限を増やせません: リプレイキャッシュ挿入はステップ 13、暗号検証（ステップ 10）の*後*、ボディ整形式性（ステップ 14）の*前*に発生するので、ステップ 10 で失敗する任意のリクエストは決して上限エントリを消費せず、ステップ 14 で失敗する任意のリクエストはすでにノンスを燃やしています — 不正なボディ上に有効な署名を運ぶキャプチャされたフレームは、増幅された暗号検証作業を強制するためにリプレイできません。これが 9a が上限状態の*リーダー*であって*ライター*でない理由です — 正当な鍵保持者（または鍵を侵害した者、上限が検出するために存在するケース）のみがセットを増やせます。チェックリストへの将来の編集は両方の順序を保持しなければならない（MUST）: 挿入を早く移す（ステップ 10 の前）と、任意の外部当事者が偽造された構造的に有効な署名で上限をフラッドできる。挿入を遅く移す（ステップ 14 の後）と、不正ボディリプレイベクトルを再び開く。

ステップ 12 の `(keyid, nonce)` 重複排除は対照的に暗号検証の*後*に実行され、リプレイキャッシュが無効な署名で消費されないようにします。

#### フォールバック認証器との合成

`required_for` は署名要件を**呼び出し元のクレデンシャルパスに対して相対的に**規定し、絶対的ではありません。検証者は通常 1 つ以上の認証器（bearer、API キー、mTLS、9421）を受け入れ、`required_for` はその認証チェーン内の 1 つのレバーで、他を覆すオーバーライドではありません。

**下記ルールの用語:** *未認証*とは、呼び出し元が有効な署名も、このオペレーションについて検証者が受け入れる他のクレデンシャルも提示しないことを意味します。認識されない bearer トークンまたは API キー（検証者が受け入れないもの）は有効なクレデンシャルでは*ありません* — 呼び出し元は未認証で最初のルールに該当します。

規範ルール:

* `required_for` オペレーションへの**未認証**リクエストは `request_signature_required` で拒否しなければなりません（MUST）。
* `required_for` オペレーションへの**未署名だが他の方法で認証された**リクエスト（有効な bearer、API キー、mTLS アイデンティティ。`Signature-Input` なし）は、署名欠落で拒否してはなりません（MUST NOT）。フォールバッククレデンシャルは検証者がその呼び出し元に十分と宣伝したもので、`required_for` は検証者自身の認証器設定を遡及的に無効化しません。
* **署名済み**リクエストは [verifier checklist](#verifier-checklist-requests) に入り、オペレーションが `required_for` にあるかに関わらず暗号学的メリットで評価されます。
* **不正な署名**は、チェックリストプリアンブルの不正署名ルールに従い、フォールバックをとにかくブロックします。壊れた署名は署名者の意図を示し、bearer に黙ってダウングレードしてはなりません（MUST NOT）。

`warn_for` はこのルールで変わりません: 未署名リクエストについてすでに非拒否で、ロールアウト中の署名済みだが無効な署名をモニタリングシグナルとして表面化し続けます。

<Warning>
  **セラーの強制 — ケイパビリティ宣言に合う姿勢を選ぶ。**

  3 つの強制姿勢が有効です。セラーは 1 つを選び、フォールバック認証器をそれに応じて設定しなければなりません（MUST）。`required_for` を宣伝しながらリストされたオペレーションで bearer 認証を開いたままにすることはセキュリティシアターです — 検証者が bearer を有効と宣伝し、呼び出し元はそれを使う権利があります。

  * **Strict（このオペレーションで署名は無条件）。** セラーは、オペレーションで bearer/API キー/mTLS を完全に受け入れるのを止めるか、*または*、9421 オンボーディングを完了した相手方からの非署名リクエストを拒否するフォールバック認証器を呼び出し元ごとのフラグでゲートしなければなりません（MUST）。これは `required_for` がすべての未署名を拒否する姿勢。
  * **署名を優先、フォールバックを受け入れ（ロールアウト中推奨）。** オペレーションに `required_for` を宣伝するが bearer を開いたまま。合成ルールが適用: 未署名-未認証呼び出し元は拒否、未署名-bearer 認証呼び出し元は通過。バイヤーが自身のペースで 9421 にオンボードする数四半期にわたる移行に適する。
  * **助言のみ。** オペレーションを `required_for` ではなく `warn_for`（または `supported_for`）に移す。検証者は存在するとき署名を検証して失敗をログするが、署名欠落で決して拒否しない。

  *呼び出し元ごとのフラグの例（strict 姿勢）:* 9421 対応の相手方の `agents[]` エントリに `signing_onboarded: true` フラグを運ぶセラーは、`required_for` のオペレーションについて解決済みエージェントが `signing_onboarded: true` を持つ bearer クレデンシャルを拒否するよう bearer 認証器を設定します。他のエージェントはフラグが切り替わるまで bearer で認証し続けます。`required_for` への昇格は運用上安全なまま — 既存の bearer トラフィックが続く一方、オンボード済み相手方はより厳格なバーに保たれます。
</Warning>

相手方のケイパビリティ面で `required_for` を読むバイヤーは、\*\*「クレデンシャルを一切提示しない呼び出し元はこのオペレーションで拒否される。検証者が受け入れる bearer、API キー、mTLS クレデンシャルを提示する呼び出し元は署名欠落で拒否されない」\*\*を学びます。それは「すべての未署名呼び出し元が拒否される」ではありません。自身の未署名 bearer 呼び出しを `required_for` オペレーションでフェイルクローズさせたいバイヤーは、ケイパビリティブロックから動作を推論するのではなく、そのオペレーションについて bearer クレデンシャルを失効させるようセラーと交渉しなければなりません（MUST）。

**なぜこの合成で strict 解釈でないか。** strict 解釈（「`required_for` はフォールバッククレデンシャルに関わらずすべての未署名リクエストを拒否」）には 2 つの実用的問題があります。第一に、3.0 ロールアウトパターンと衝突します: セラーはオペレーションを数四半期にわたって `supported_for → warn_for → required_for` に昇格し、ほとんどが移行中に同じオペレーションでライブ bearer トラフィックを持ちます。strict 解釈は、すべての相手方をセラーの `required_for` フリップと歩調を合わせて署名に移行させるか、壊れるよう強制します。第二に、遠隔作用バグを作ります: 運用モニタリング目的で `required_for` を有効化するセラーは、警告なしにそのオペレーションのすべての bearer 認証バイヤーを不注意に 401 し、ケイパビリティを削除する以外の救済パスがありません。合成ルールは `required_for` を段階的に有効化して安全にします — その効果は検証者が実際に所有する未認証ブランチにスコープされます。

#### Content-digest とプロキシ互換性

`content-digest` をカバーすることはリクエストボディバイトを署名にバインドします。支出コミットオペレーションでは、これが要点です: ボディが金銭を指定し、ボディにコミットしない署名は重要な攻撃面を保護しません。サーバー間 AdCP デプロイ — そのほとんど — では、ボディを変更する中間者は稀で、通常特定の意図的な設定の結果です。デフォルト姿勢: **支出コミットオペレーションで `content-digest` をカバー。ボディ保持を妨げるトランスポートを、対応する制約ではなく修正すべきバグとして扱う。**

<Warning>
  **既知のボディ変更トランスポートパターン。** これらの設定はボディバインディング署名を壊し、本番での 9421 相互運用バグの単独最大の原因です:

  * POST ボディを再圧縮またはバッファ変更する CDN 設定（稀だが、特定の Cloudflare Workers、Fastly VCL、CloudFront Lambda\@Edge セットアップはバイト変更を導入し得る）。
  * JSON リクエストボディを「サニタイズ」する WAF（空白正規化、キー並べ替え、未知フィールド除去）。ほとんどの WAF は変更せずに検査するが、一部は変更する。
  * ロギング、検証、変換のためにクライアントとオリジンの間で JSON を再シリアライズするリバースプロキシまたは API ゲートウェイ。
  * チャンクエンコードフレーミングの仮定が異なる HTTP/2 → HTTP/1.1 ブリッジ。
  * **署名者側シリアライズミスマッチ。** ある JSON シリアライズ（例: デフォルトの空白セパレーター付き `json.dumps(payload)`）上で `content-digest` を計算する一方、HTTP クライアントがワイヤー上に異なるシリアライズ（例: コンパクトセパレーター）を書く署名者は、レシーバーが決して見ないバイト上のダイジェストを生成します。すべての検証者がその後 `webhook_signature_digest_mismatch` または `request_signature_digest_mismatch` で拒否します。**ボディを一度シリアライズし、それらの正確なバイトをダイジェスト入力と HTTP ボディの両方に使う** — 事前シリアライズされたオブジェクトからダイジェストを計算してクライアントが同じバイトを再現すると信頼しない。これは[レガシー HMAC スキームがコンパクトセパレーターでピン留めする](#legacy-hmac-sha256-fallback-deprecated-removed-in-40)同じ罠です。9421 は黙ってではなく大きく失敗する（ダイジェストミスマッチはハード拒否）が、署名者側の修正は同一です。

  **トランスポートを制御する場合**、ボディをバイト単位でエンドツーエンド保持し `content-digest` をカバー。**トランスポートを制御しない場合**、セキュリティ保証を劣化させるのではなくそれを修正。実際のトラフィックを送る前にテストエンドポイントに対する `POST` エコーテストでエンドツーエンド検証。
</Warning>

レガシーインフラのため本当にボディバイトを保持できない検証者は `covers_content_digest: "forbidden"` を宣伝してもよい（MAY）。これはインフラを修正できない狭いケースのオプトアウトです。`"required"` はすべての支出コミットオペレーションに推奨。`"either"` がデフォルト — 署名者がリクエストごとに選択し、検証者はカバー済みとカバーなしの両形式を受け入れます。

**`"required"` は厳格。** 検証者が `covers_content_digest: "required"` を宣伝するとき、`content-digest` をカバーしないボディを持つ署名済みリクエストは `request_signature_components_incomplete` でハード拒否です。検証者はそれを「ソフト」な署名済みだがボディ非バインドリクエストとして受け入れてはなりません（MUST NOT）。ソフトモードはありません。ある呼び出しで `content-digest` をカバーしたくない署名者は、ポリシーが `"either"` または `"forbidden"` の検証者にルーティングするか、その呼び出しに全く署名しないかしなければなりません（MUST）。

#### トランスポートリプレイ重複排除

[verifier checklist](#verifier-checklist-requests) のステップ 12 は `(keyid, nonce)` ごとの重複排除を要求します。無制限のセットはメモリと DoS リスクです。

* 各エントリの TTL = ウィンドウ検証で適用した対称クロックスキュー許容に一致する `(expires − now) + 60 s`。典型的な TTL ≤ 360 秒（5 分 + 60 秒スキュー）。
* TTL 退避付きの `(keyid, nonce)` でキーされたインメモリ LRU、期待リクエストレート × 最大署名有効性でサイズ。
* 署名者ごとに約 10K req/秒を超える場合: `EX = remaining_validity_seconds + 60` の Redis `SETNX`。
* 分散検証者（マルチリージョン）: リージョンごとのリプレイキャッシュは許容。これが可能にする唯一の攻撃はリージョンをまたぐ `(expires − now + 60 s)` 内の単一リプレイで、約 6 分に制限され、攻撃者が中間ルーティングを制御する場合のみ有効。

検証者はリクエスト bearer トークン、IP、任意の非 `(keyid, nonce)` 値をリプレイキーとして使ってはなりません（MUST NOT）— それらは正当なエージェントトラフィックを拒否する偽陽性を生みます。

**keyid ごとの上限。** 濫用的または侵害された署名者が一意のノンスで検証者メモリを枯渇させるのを防ぐため、検証者はリプレイキャッシュに keyid ごとのエントリ上限を強制しなければなりません（MUST）。推奨上限: `keyid` ごとに 1,000,000 エントリ。上限超過で、検証者はその `keyid` からの新しい署名を `request_signature_rate_abuse` で拒否しなければならず（MUST）— 黙って退避してはならず — オペレーターにアラートすべきです（SHOULD）。上限に達することは侵害された鍵または著しく誤設定された署名者を示すからです。黙った退避が危険なモードです: まさに検証者が攻撃下にあるときにリプレイウィンドウを作ります。keyid ごとの上限は総キャッシュ上限とは別: 検証者は多くの行儀の良い署名者を介して正当に総上限に達し得ますが、keyid ごとの枯渇は明白に攻撃シグナルです。上限チェックは [verifier checklist](#verifier-checklist-requests) のステップ 9a — 暗号検証の**前**に評価され、濫用的署名者が増幅された Ed25519/ECDSA 作業を検証者に強制できないように。

**単一プロセス vs 分散強制。** 単一プロセス検証者では、ステップ 9a（読み取り）とステップ 13（挿入）は 1 つの実行で逐次的で上限は正確です。Redis バックのリプレイキャッシュを共有する分散検証者では、ステップ 9a は安価な高速パス増幅ガードだが権威的ではありません: 2 つの検証者が両方 `size == cap − 1` を観測し、両方 9a を通過し、両方ステップ 10-12 を通過し、両方ステップ 13 で挿入し得ます。上限ドリフトを避けるため、ステップ 13 の挿入は上限チェックとアトミックであるべきです（SHOULD、例: over-cap センチネルを返す Lua スクリプトまたは `SETNX` パターン）— ステップ 9a は安価な増幅ガードのまま、ステップ 13 が権威的な強制ポイント。アトミック挿入が over-cap を返す検証者は、成功させるのではなく `request_signature_rate_abuse` でリクエストを拒否しなければなりません（MUST）。ステップ 13 で助言的な上限は上限ではありません。

#### トランスポート失効

オペレーターは、`agents[]` エントリの下で公開されたガバナンス、リクエスト署名、その他のエージェント署名鍵をカバーする単一の結合失効リストを brand.json オリジンで提供すべきです（SHOULD）。形式と署名セマンティクスはガバナンス失効リストに一致（上記の [Revocation](#revocation) を参照）。リクエスト署名鍵について:

* `revoked_kids` はその `kid` の下で署名されたすべてのリクエスト（失効タイムスタンプの前後）を無効にします。
* `revoked_jtis` は使われません（リクエスト署名は `jti` を持たず、ノンスの一意性は鍵ごと）。

リクエスト署名済み変更を受け入れる検証者は、`next_update` で宣言されたケイデンス（フロア 1 分、上限 30 分）で失効リストをポーリングしなければなりません（MUST）。フェッチ失敗の安全デフォルトが grace = 以前のポーリング間隔の 4 倍で適用: `next_update + grace` 内にリフレッシュしていない検証者は、リストがリフレッシュされるまで新しいリクエスト署名済み変更を `request_signature_revocation_stale` で拒否しなければなりません（MUST）。

#### トランスポートケイパビリティ宣伝

検証者は `get_adcp_capabilities` の `request_signing` ブロックを介して署名サポートと呼び出しごとの要件を宣伝します:

```json theme={null}
{
  "request_signing": {
    "supported": true,
    "covers_content_digest": "either",
    "required_for": [],
    "warn_for": ["create_media_buy"],
    "supported_for": [
      "create_media_buy",
      "update_media_buy",
      "sync_creatives",
      "activate_signal"
    ]
  }
}
```

* `supported`: true のとき、検証者は存在するとき署名を検証。false または不在のとき、署名は無視される。
* `covers_content_digest`: `"required"`, `"forbidden"`, `"either"`（デフォルト）のいずれか。`"required"`: 署名者は `content-digest` をカバーしなければならない。ボディ非署名の署名は拒否。`"forbidden"`: 署名者は `content-digest` をカバーしてはならない。ボディバインド署名は拒否。`"either"`: 署名者が選択。検証者は両方を受け入れ。
* `required_for`: **他の有効なクレデンシャルを提示しない未署名リクエスト**が `request_signature_required` で拒否される AdCP プロトコルオペレーション名（トランスポート固有でない）。3.0 ではデフォルトで空。署名者はリストされた任意のオペレーションに署名しなければならない（MUST）。bearer、API キー、mTLS フォールバックとの合成は [Composition with fallback authenticators](#composition-with-fallback-authenticators) が規定 — 特に、有効なフォールバッククレデンシャルを提示する未署名リクエストは受け入れられ、署名を無条件にしたいセラーはそのオペレーションで他のクレデンシャルタイプを拒否するようフォールバック認証器を設定しなければならない（MUST）。
* `warn_for`: 検証者が存在するとき署名を検証し、失敗をモニタリングでログするが、**拒否しない**オペレーション。`supported_for` から `required_for` へのシャドウモードブリッジとして使用。セラーが強制前に実トラフィック失敗率を見る相手方ごとのパイロットを可能にする。優先順位: `required_for > warn_for > supported_for`。署名者は `warn_for` のオペレーションに署名すべき（SHOULD）。検証者はこれらのオペレーションへの未署名または検証失敗リクエストを拒否してはならない（MUST NOT）。
* `supported_for`: 署名が存在するとき検証されるが必須でないオペレーション。署名者はこれらに署名すべき（SHOULD）。通常 `required_for` と `warn_for` のスーパーセット。

**ロールアウトパターン:**

1. 署名準備を発表: オペレーションを `supported_for` に追加。相手方は署名を開始できるが、しなくても何も変わらない。
2. シャドウモードに昇格: オペレーションを `warn_for` に移す。検証者は検証失敗をログ。トラフィックは影響なし。オペレーターは失敗率を監視してデバッグ。
3. 強制: 失敗率がオペレーターの閾値を下回ったら `required_for` に移す。そのオペレーションへの未署名または無効署名リクエストは今拒否される。

3.0 では、検証者は `required_for: []` で出荷し、選択的に設定します。`warn_for` は強制に切り替える前の推奨プレプロダクション停止です。4.0 ではプロトコルが規範的に `required_for` が検証者がサポートするすべての支出コミットオペレーションを含むことを要求し、それらのオペレーションに `covers_content_digest: "required"` が推奨されます。

#### トランスポートエラータクソノミー

401 で `WWW-Authenticate: Signature error="<code>"` に返され、SDK 検証者が型付きエラーとして表面化する安定コード。命名パターンは [governance taxonomy](#verification-error-taxonomy) に一致し、SDK エラー処理が対称になります。

| Failure                                                                                                                                                                                                                                                                         | Retry?             | Code                                      |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----------------------------------------- |
| 署名が要求される未署名リクエスト — (a) オペレーションが `required_for` にある、または (b) リクエストペイロードが `required_for` メンバーシップに関わらず署名をトリガーするフィールドを運ぶ（例: 署名対応セラーの `push_notification_config.authentication` または `accounts[].notification_configs[].authentication` — [Webhook callbacks](#webhook-callbacks) を参照） | No                 | `request_signature_required`              |
| リクエスト `@target-uri` が構文的に不正（例: 空オーソリティ、裸の IPv6、IPv6 ゾーン識別子、生の非 ASCII ホスト）、OR 正準化された `@authority` が正準 `@target-uri` のオーソリティコンポーネントとバイト一致しない（クロス vhost リプレイ）                                                                                                                      | No                 | `request_target_uri_malformed`            |
| `Signature` または `Signature-Input` ヘッダーが存在するが不正                                                                                                                                                                                                                                  | No                 | `request_signature_header_malformed`      |
| 必須 sig-param が不在（`created`, `expires`, `nonce`, `keyid`, `alg`, `tag`）                                                                                                                                                                                                          | No                 | `request_signature_params_incomplete`     |
| `tag` が `adcp/request-signing/v1` でない                                                                                                                                                                                                                                           | No                 | `request_signature_tag_invalid`           |
| `alg` が許可リストにない                                                                                                                                                                                                                                                                 | No                 | `request_signature_alg_not_allowed`       |
| 署名ウィンドウ無効（`expires ≤ created`、スキュー、期限切れ、> 5 分有効性）                                                                                                                                                                                                                               | No                 | `request_signature_window_invalid`        |
| 必須カバーコンポーネント欠落                                                                                                                                                                                                                                                                  | No                 | `request_signature_components_incomplete` |
| ケイパビリティが `"forbidden"` のときカバーコンポーネントが `content-digest` を含む                                                                                                                                                                                                                      | No                 | `request_signature_components_unexpected` |
| 1 回の再フェッチ後 `keyid` が署名者 JWKS にない                                                                                                                                                                                                                                                | No                 | `request_signature_key_unknown`           |
| JWK `key_ops` が `verify` を欠く、`use` ≠ `sig`、または `adcp_use` ≠ `request-signing`                                                                                                                                                                                                   | No                 | `request_signature_key_purpose_invalid`   |
| `keyid` ∈ `revoked_kids`                                                                                                                                                                                                                                                        | No                 | `request_signature_key_revoked`           |
| 失効リストが grace 内にリフレッシュされていない                                                                                                                                                                                                                                                     | No (block new)     | `request_signature_revocation_stale`      |
| 暗号検証失敗                                                                                                                                                                                                                                                                          | No                 | `request_signature_invalid`               |
| 再計算されたダイジェストとの `content-digest` ミスマッチ                                                                                                                                                                                                                                           | No                 | `request_signature_digest_mismatch`       |
| ボディが重複オブジェクトキーを含む（パーサー差分ベクトル）                                                                                                                                                                                                                                                   | No                 | `request_body_malformed`                  |
| ノンスがウィンドウ内ですでに見られている                                                                                                                                                                                                                                                            | No                 | `request_signature_replayed`              |
| keyid ごとのリプレイキャッシュがエントリ上限を超過                                                                                                                                                                                                                                                    | No (block new)     | `request_signature_rate_abuse`            |
| JWKS フェッチ一時的失敗                                                                                                                                                                                                                                                                  | Yes (with backoff) | `request_signature_jwks_unavailable`      |
| JWKS フェッチが SSRF 検証失敗                                                                                                                                                                                                                                                            | No                 | `request_signature_jwks_untrusted`        |

サーバーは安定コードを超えて内部検証詳細をエコーしてはなりません（MUST NOT）。詳細はサーバー側でログします。

**`WWW-Authenticate` 形式。** AdCP はリクエスト署名チャレンジの realm 値を定義しません。検証者は `realm` パラメーターなし、他のパラメーターなしで `WWW-Authenticate: Signature error="<code>"` を発行しなければなりません（MUST）。ヘッダーをパースするクライアントは他のパラメーターを許容しなければならず（MUST、RFC 7235 は実装が追加を含めることを許可）、それらに依存すべきではありません（SHOULD NOT）。

#### Webhook コールバック

プッシュ通知 Webhook（バイヤーが登録する `push_notification_config.url` への POST）、アカウントレベル Webhook（`accounts[].notification_configs[].url` への POST）、類似の非同期セラー起動コールバックは、このプロファイルの対称バリアントの下で署名されます。役割方向はリクエスト署名に対して反転します: **セラーがアウトバウンド署名**、**バイヤーが検証**。9421 Webhook 署名は Webhook を発行する任意の 3.0 セラーでベースライン必須で、[Webhook Security](#webhook-security) で説明された非推奨 HMAC フォールバック付きです。

**プログラム的宣伝付きベースライン。** 9421 Webhook 署名は Webhook を発行する任意のセラーでベースライン必須です — デフォルトは署名で、交渉されるオプションではありません。`get_adcp_capabilities` の `webhook_signing` ケイパビリティブロックは、バイヤーが非署名セラーを、トラフィック検査（このブロックが復元される前に `request_signing` との非対称性が現れた方法）で発見するのではなく*オンボーディングで*検出できるように存在します。ケイパビリティ面が変更系 Webhook 発行を他所で宣伝するセラー（例: `media_buy.reporting_delivery_methods` が `webhook` を含む、`media_buy.content_standards.supports_webhook_delivery: true`、または `wholesale_feed_webhooks.supported: true`）は、このブロックを `supported: true` で含めなければなりません（MUST）。Webhook を発行しないセラーはブロックを完全に省略してもよい（MAY）。`supported: false` は未署名 Webhook を発行する安全でない姿勢に予約され、Webhook 不在を示すために使ってはなりません（MUST NOT）。面が変更系 Webhook 発行を宣伝する一方 `webhook_signing` ブロックが `supported: false` を宣伝するか省略されるセラーと統合するバイヤーは、ユーザーが対処可能なエラーでオンボーディングを失敗させなければなりません（MUST）— 発行するが Webhook に署名しないセラーは、任意の変更系 Webhook ユースケースで統合するのに安全でありません。

```json theme={null}
{
  "webhook_signing": {
    "supported": true,
    "profile": "adcp/webhook-signing/v1",
    "algorithms": ["ed25519", "ecdsa-p256-sha256"],
    "legacy_hmac_fallback": false
  }
}
```

* `supported`: セラーがケイパビリティ面の他所で変更系 Webhook 発行を宣伝するとき `true` でなければならない（MUST）。バイヤーは `supported: false` またはブロック欠落でセラー面が Webhook 発行を宣伝するときオンボーディングを拒否。Webhook を発行しないセラーはブロック全体を省略すべき（SHOULD）。
* `profile`: このプロファイルバージョンでは正確に `adcp/webhook-signing/v1` でなければならない（MUST）。将来のプロファイルバージョンは文字列をバンプ。
* `algorithms`: `["ed25519", "ecdsa-p256-sha256"]` のサブセット — このセラーが署名するアルゴリズムセット。Webhook 署名検証者許可リストに一致。宣伝された `algorithms` 配列がこのセット外の任意の値を含む場合、バイヤーはユーザーが対処可能なエラーでオンボーディングを拒否しなければならない（MUST）。セット外アルゴリズムは誤設定または非コンフォーマントなセラーを示し、黙った受け入れは許可リストを無効にする。
* `legacy_hmac_fallback`: バイヤーが `push_notification_config.authentication.credentials` または `accounts[].notification_configs[].authentication.credentials` を設定するときセラーがレガシー HMAC-SHA256 スキームをサポートする場合に限り `true`。`false` が 3.x の推奨姿勢。

バイヤーは `push_notification_config.authentication.credentials` または `accounts[].notification_configs[].authentication.credentials` を設定してレガシー HMAC-SHA256 スキームにオプトインします。そうでなければセラーは 9421 Webhook プロファイルで署名します。セラーはレガシースキームのサポートを断ってもよい（MAY）— 上記の `legacy_hmac_fallback` フラグを参照。

**モード選択はスイッチであり両方ではない。** `push_notification_config.authentication` または `accounts[].notification_configs[].authentication` の存在は、その URL に配信されるすべての Webhook についてちょうど 1 つの署名モードを選択します: `authentication` あり → レガシー HMAC-SHA256（または Bearer）、`authentication` なし → 9421。セラーは同じ Webhook を両方の方法で署名してはなりません（MUST NOT）。バイヤーは「まず 9421 を試し、HMAC にフォールバック」検証を試みてはなりません（MUST NOT）— そのパターンはダウングレードオラクル動作を作り、バイヤーが求めていない署名を受け入れます。検証者は、レシーバーが Webhook 登録について設定された HMAC シークレットを持つかで検証パスを厳密にキーします。

**鍵公開。** 署名鍵は、署名エージェントのオペレータードメインのセラー**自身の brand.json** の `agents[]` エントリの、そのエントリの `jwks_uri` メンバーでセラーが公開します — 他の任意の AdCP エージェント鍵と同じ公開パターン。Webhook はエージェントの **`adcp_use: "request-signing"`** 鍵で署名されます。別個の Webhook 鍵目的はありません。リクエストと Webhook のドメイン分離は、鍵目的ではなく署名 `tag`（`adcp/request-signing/v1` vs `adcp/webhook-signing/v1`）が運びます。各署名 JWK は宣言しなければなりません（MUST）:

| Member     | Value                                         |
| ---------- | --------------------------------------------- |
| `use`      | `"sig"`                                       |
| `key_ops`  | `["verify"]`                                  |
| `adcp_use` | `"request-signing"`                           |
| `kid`      | JWKS 内で別個。`adcp_use` に関わらず他の `kid` と衝突してはならない |
| `alg`      | `"EdDSA"` or `"ES256"`                        |

**鍵分離は別個の `kid` を介してオプション — 別個の目的ではない。** Webhook トラフィックを別個の鍵素材で署名させたい（Webhook 鍵侵害がリクエスト署名に及ばないように、または 2 つを独立にローテーションするように）オペレーターは、**別個の `kid` を持つ 2 つ目の `adcp_use: "request-signing"` 鍵**を公開し、それで Webhook に署名します。両方の鍵は同じ `adcp_use` を運びます。検証者は `Signature-Input` の `kid` で正しいものを解決します。分離を達成するのに専用の Webhook 鍵目的は不要です。

> **非推奨:** `adcp_use: "webhook-signing"` は非推奨で将来のメジャーバージョンで削除予定（[#5555](https://github.com/adcontextprotocol/adcp/issues/5555) で追跡。正確なウィンドウは WG/RFC 決定）。検証者は後方互換性のためそれを依然受け入れなければならない（MUST、`"webhook-signing"` 鍵の下で署名された Webhook はクリーンに検証される）が、新しい署名者は `"request-signing"` 鍵のみで公開・署名すべき（SHOULD）。

Webhook を検証するバイヤーは、`adcp_use` が `"request-signing"`（または非推奨の `"webhook-signing"`）である JWK を受け入れなければならず（MUST）、他の鍵目的失敗 — 他の任意の `adcp_use` 値、不在の `adcp_use`、欠落した `verify` key\_op — を `webhook_signature_key_purpose_invalid` で拒否しなければなりません（MUST）。逆は依然禁止: リクエスト検証は `adcp_use == "request-signing"` を正確に要求し（鍵が他の目的を宣言するとリクエスト署名は拒否）、`"response-signing"` も `"governance-signing"` 鍵も Webhook 配信で決して有効ではありません。Webhook パスは鍵目的について寛容です。すでに `tag`（`adcp/webhook-signing/v1`）と必須の `content-digest` カバレッジでドメイン分離を運ぶので、鍵目的チェックはそこに混同耐性を追加しないからです。

**トラストアンカーと影響範囲。** Webhook 真正性のトラストアンカーは**署名者の brand.json オリジン** — 署名エージェントの `agents[]` エントリを宣言する brand.json をホストする HTTPS オリジンです。そのオリジンの侵害（サブパス乗っ取り、DNS ハイジャック、`/.well-known/brand.json` または `jwks_uri` の CDN キャッシュポイズニング）は、オペレーターが `revoked_kids` エントリを公開しバイヤー検証者が失効リストをリフレッシュするまで、バイヤーがその署名者から受け入れるすべての Webhook を侵害します。バイヤーは統合オンボーディングで学んだエージェントの `jwks_uri` URL をピン留めし、URL 自体の変更（安定 URL 内の `kid` ローテーションだけでなく）にアラームすべきです（SHOULD）— URL の変更は再アンカーを強制し、黙った採用ではなくオペレーターの注意を要求すべきです（SHOULD）。同じ JWKS 内の `kid` 衝突は各 `kid` がちょうど 1 つの鍵に解決するよう禁止されます。Webhook はエージェントの `request-signing` 鍵で署名されるので、デフォルトでリクエスト署名鍵侵害は Webhook に及びます。影響範囲分離を必要とするオペレーターは、Webhook 配信専用の別個の `kid` を持つ 2 つ目の `request-signing` 鍵を公開してそれで Webhook に署名します — 分離は別個の `adcp_use` ではなく別個の `kid` の下の別個の鍵素材から来ます。

**カバーされるコンポーネント**はリクエスト署名と同一: `@method`, `@target-uri`, `@authority`, `content-type`, `content-digest`。`content-digest` は Webhook コールバックで REQUIRED — ボディがイベントを運び、Webhook レシーバーはボディ保持がバイヤー自身のインフラ問題であるバイヤー制御のエンドポイントです。Webhook に `covers_content_digest: "forbidden"` オプトアウトはありません。Webhook ボディバイトを保持できないトランスポートは修正されなければなりません（MUST）。

**署名パラメーター**は 1 つの上書き付きでリクエスト署名と同一:

| Parameter                                     | Notes                                                                                                                                                                                    |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `created`, `expires`, `nonce`, `keyid`, `alg` | [リクエスト署名パラメーター](#adcp-rfc-9421-profile)と同じセマンティクス。                                                                                                                                       |
| `tag`                                         | 正確に `adcp/webhook-signing/v1` でなければならない。検証者は Webhook ルート上の `adcp/request-signing/v1` を `webhook_signature_tag_invalid` で拒否しなければならない。別個の tag は、リクエスト署名が Webhook 署名としてリプレイされること、およびその逆を防ぐ。 |

**JWKS ディスカバリー。** バイヤーはすでに使っている AdCP 統合からセラーのエージェント URL を知っています。バイヤーは解決します:

1. セラーエージェント URL `A` → `A` のオペレータードメインの `/.well-known/brand.json` を [Webhook URL validation](#webhook-url-validation-ssrf) に従う SSRF 検証付きでフェッチ。brand.json 解決は 1 リダイレクト（`authoritative_location` または `house` リダイレクトバリアント）に従って停止。
2. フェッチした brand.json で、`url` が `A` とバイト単位で一致する `agents[]` エントリを見つける。
3. そのエントリの `jwks_uri`（または `A` のオリジンの `/.well-known/jwks.json` にデフォルト）を SSRF 検証付きでフェッチ。JWKS キャッシュ TTL は失効リストポーリング間隔（フロア 1 分、上限 30 分）で上限が制限される。長時間実行のタスクフローは JWKS ローテーションをまたぐ。検証者はタスクの寿命の間単一の JWKS スナップショットをピン留めしてはならない（MUST NOT）。
4. インカミング `Signature-Input` の `keyid` をフェッチしたセットの JWK に解決。`kid` ミスでは、`webhook_signature_key_unknown` で拒否する前に 1 回再フェッチ（再フェッチ間の 30 秒クールダウンに従う）。ミス時再フェッチパスはタスク中の鍵ローテーションを扱う要のメカニズム — それをスキップするクライアントは正当なローテーション後配信を拒否する。

バイヤーは Webhook ペイロードフィールド（`task_id`, `operation_id` など）または `adagents.json` エントリから署名者アイデンティティを導出してはなりません（MUST NOT）— それらはパブリッシャー認可であり署名者アイデンティティではありません。アイデンティティは署名 → JWKS → セラー `agents[]` エントリチェーンのみを介して確立されます。

**ダウングレードと注入への耐性。** バイヤーの Webhook 署名の好みは、Webhook を登録するインバウンドリクエストの `push_notification_config.authentication` または `accounts[].notification_configs[].authentication` の存在または不在で伝えられます。3.0 では、そのインバウンドリクエストは 9421 署名ではなく頻繁に bearer 認証されるので、経路上の変更者（誤設定プロキシ、侵害された中間者）が `authentication` ブロックを黙って除去または注入できます。次のルールが影響範囲を封じ込めます:

* **セラーは**非空 `authentication` ブロックで到着するすべてのリクエストを**ログしなければなりません（MUST）。** 予期しない HMAC 選択へのオペレーションアラームは、バイヤーが 9421 を得ていると思ったときにバイヤー側を保護します。
* **リクエスト署名をサポートするセラーは**、`push_notification_config.authentication` または任意の `accounts[].notification_configs[].authentication` に `authentication` が存在するとき、インバウンドリクエストが（[request verifier checklist](#verifier-checklist-requests) に従って）9421 署名されることを**要求しなければならず（MUST）**、`request_signature_required`（`required_for` オペレーションに使うのと同じコード — [Transport error taxonomy](#transport-error-taxonomy) を参照）で拒否します。署名済みリクエストがボディに暗号学的にコミットするとき、`authentication` ブロックは署名も無効化せずに注入または除去できません。リクエスト署名を全くサポートしないセラーはこのルールを強制する方法がなく、前の項目の log-and-alarm 姿勢にフォールバックします — 3.0 移行注記であり免除ではない: [request-signing migration timeline](#transport-migration-timeline) は 4.0 で支出コミットオペレーションにリクエスト署名を必須にし、その時点で未署名のみのセラーはなくなります。
* **バイヤーは**、`authentication.credentials` で登録した後に 9421 署名済み Webhook を受け取ったとき、または `authentication` なしで登録した後に HMAC 署名済み Webhook を受け取ったとき、黙ってダウングレードするのではなく **`webhook_mode_mismatch` で拒否してアラームしなければなりません（MUST）。** 拒否が安全特性です。アラームはテレメトリ — アラームするがペイロードを受け入れるバイヤーは、すでにミスマッチした署名スキームに権限を渡しています。拒否は安定エラーコード付きの HTTP `401` として表面化し、送信者側のリトライロジックが同一にリプレイするのではなくインシデントレスポンスにルーティングできます。
* **バイヤーは**、9421 をまだ実装していないセラーと相互運用するとき、オンボーディングで **HMAC モードを帯域外で交渉すべきです（SHOULD）。** オペレーターレコードでの耐久性のある相手方ごとのモード選択は、リクエストごとのフィールドのように MITM 変更可能ではありません。

**Webhook の検証者チェックリスト。** これら 15 のチェック（14 の番号付きステップとサブステップ 9a）を順に適用し、最初の失敗でショートサーキットします。ステップ 14 は 14a（strict-parse 要件）と 14b（ロギング規律）に分解 — 両方ともステップ 14 実行時に適用され、1 つのチェックの詳述。下記のステップは [request verifier checklist](#verifier-checklist-requests) に**2 つのパラメーター置換** — `tag` 値（`adcp/request-signing/v1` の代わりに `adcp/webhook-signing/v1`）と信頼方向解決（バイヤーの代わりにセラーの brand.json `agents[]` エントリ）— を加えたものです。ステップ 14（ボディ整形式性）は 2 つのプロファイルで同一。エラーコードプレフィックスのみ異なる（`webhook_body_malformed` vs `request_body_malformed`）。実装は 2 つのプロファイル間で検証者コードを共有し、2 つのパラメーター置換で分岐し、プロファイル固有のエラーコードを設定すべきで（SHOULD）、実装をフォークすべきではありません。エラーコードは `webhook_*` プレフィックス — ほとんどが `webhook_signature_*` 中置を運び、加えてそれなしの構造コード（現在 `webhook_target_uri_malformed`, `webhook_mode_mismatch`, `webhook_body_malformed`）— なので呼び出し元側のエラー処理が 2 つのプロファイルを区別します。

1. `Signature-Input` と `Signature` ヘッダーを RFC 9421 §4 に従ってパース。不正なら拒否（`webhook_signature_header_malformed`）。`Signature` または `Signature-Input` が他方なしに存在する場合、同じコードで拒否 — 推測可能でなくバインドされたペア。

2. `created`, `expires`, `nonce`, `keyid`, `alg`, `tag` のいずれかが `Signature-Input` パラメーターから不在なら拒否（`webhook_signature_params_incomplete`）。

3. `tag` が正確に `adcp/webhook-signing/v1` でないなら拒否（`webhook_signature_tag_invalid`）。バイト単位一致、ケースフォールディングなし。

4. `alg` が許可リスト（`ed25519`, `ecdsa-p256-sha256`）にないなら拒否。ライブラリのデフォルトに頼ってはならない（`webhook_signature_alg_not_allowed`）。

5. `expires ≤ created`、`created > now + 60 s`、`expires < now − 60 s`、または `expires − created > 300 s` なら拒否（`webhook_signature_window_invalid`）。

6. カバーされるコンポーネントが `@method`, `@target-uri`, `@authority`, `content-type`, `content-digest` のすべてを含まないなら拒否（`webhook_signature_components_incomplete`）。`content-digest` は REQUIRED。ポリシーブランチはない。

7. 上記の JWKS ディスカバリーステップで `keyid` を JWK に解決。`kid` ミスでは、拒否（`webhook_signature_key_unknown`）前に 1 回再フェッチ（再フェッチ間 30 秒クールダウン）。`keyid` が署名者の brand.json の特定の `agents[]` エントリに解決できないなら拒否。

8. JWK の `use` が `"sig"`、`key_ops` が `"verify"` を含み、`adcp_use` が `"request-signing"` であることを検証 — Webhook はエージェントのリクエスト署名鍵で署名される（[Key publication](#webhook-callbacks) を参照）。非推奨の `"webhook-signing"` 値も後方互換性のため受け入れなければならない（MUST）。他の任意の結果で `webhook_signature_key_purpose_invalid` で拒否: 不在の `adcp_use`、欠落した `verify` key\_op、他の任意の `adcp_use` 値（例: `"response-signing"`, `"governance-signing"`）。ここで `"request-signing"` を受け入れるのは安全です。クロスプロトコル混同が鍵目的判別子ではなく `tag`（ステップ 3）と必須の `content-digest` カバレッジ（ステップ 6）で防がれるからです: キャプチャされたリクエスト署名は `tag=adcp/request-signing/v1` を運びステップ 3 で拒否されます。（`webhook_mode_mismatch` は HMAC-vs-9421 認証モードセレクターミスマッチに予約 — [Downgrade and injection resistance](#webhook-callbacks) を参照 — で鍵目的失敗には使われません。）

9. [Transport revocation](#transport-revocation) リスト（署名目的をまたいで再利用）を確認。`keyid ∈ revoked_kids` なら拒否（`webhook_signature_key_revoked`）。検証者が grace 内にリフレッシュしていないなら `webhook_signature_revocation_stale` で拒否。

   **9a. keyid ごとの上限チェック。** [Webhook リプレイキャッシュ上限](#webhook-replay-dedup-sizing)を確認。超過なら `webhook_signature_rate_abuse` で拒否。リクエスト署名と同じ安価な拒否の根拠で、暗号検証（ステップ 10）の前に実行。

10. [リクエスト署名プロファイル](#adcp-rfc-9421-profile)に従い `@target-uri` 正準化 AND `@authority` 導出を適用した後、カバーされるコンポーネントを使って RFC 9421 §2.5 に従い正準署名ベースを計算。**`@authority` ルールは Webhook セキュリティの要:** 検証者は、存在する場合 HTTP/2+ の `:authority` 疑似ヘッダーから、そうでなければ受信した HTTP/1.1 `Host` ヘッダーから `@authority` を導出しなければならない（MUST）— リバースプロキシルーティング状態、ロードバランサーメタデータ、フォワードプロキシが転送中に書き換えた任意の `Host` 値からではない。受信リクエストに `:authority` と `Host` の両方が存在する場合、正準化後にバイト等価でなければならない（RFC 7540 §8.1.2.3 等価性）。発散は `webhook_target_uri_malformed` で拒否。正準化された `@authority` は正準 `@target-uri` のオーソリティコンポーネントとバイト単位で一致しなければならない（MUST）。ミスマッチは `webhook_target_uri_malformed` で拒否。署名済み `@target-uri` に対するそのバイト一致 — ソースヘッダーの選択ではなく — が唯一の安全ゲート。`Host` 自体が転送中に書き換えられ得るから。このチェックリストだけから — プロファイルを相互参照せずに — 構築する実装者はこのルールを適用しなければならない（MUST）。それをスキップするとクロス vhost リプレイベクトル（攻撃者が TLS 終端された Webhook を傍受し、同じ検証者プール上の 2 つ目の vhost にリプレイ: 同じ証明書 SAN、異なる `Host`）を黙って受け入れる。正準化完了後、JWK に対して署名を検証（失敗で `webhook_signature_invalid`）。

11. 受信したボディバイトから `content-digest` を再計算して比較（ミスマッチで `webhook_signature_digest_mismatch`）。REQUIRED — ポリシーブランチなし。

12. リプレイキャッシュに対してノンスを確認。`(keyid, nonce)` がリプレイキャッシュ TTL 内で見られている場合拒否（`webhook_signature_replayed`）。

13. **ステップ 1-12 がすべて通過した後にのみ**、`(keyid, nonce)` を TTL = `(expires − now) + 60 s` でリプレイキャッシュに挿入。この挿入はステップ 14 のボディ整形式チェックの前に発生しなければならない（MUST）。不正なボディ上に有効な署名を運ぶキャプチャされたフレームが各リトライで暗号検証 CPU を燃やすためにリプレイできないように — ノンスはボディ形状に関わらず暗号学的に有効なフレームの最初の目撃で燃やされる。この順序が保持する要となる上限不変条件はステップ 14b の後に文書化。

14. **ボディ整形式性。** 検証者は重複オブジェクトキーを含むボディを拒否しなければならない（MUST、`webhook_body_malformed`）。RFC 8259 §4 に従い、重複キーパース動作は予測不能 — 署名はワイヤー上のバイトに対して有効だが、2 つのパーサーがパースされた値について一致しないことがあり、これはパーサー差分攻撃クラス（cf. CVE-2017-12635）。このチェックは、署名検証者のペイロードのビューとダウンストリームコンシューマーのビューの間のギャップを閉じる。構造化 `webhook_body_malformed` エラーを返すのではなくクラッシュする検証者はコンフォーマントだが最適でない。このチェックのコンフォーマンスフィクスチャは `static/test-vectors/webhook-hmac-sha256.json` の `duplicate-keys-conflicting-values` ベクター — 9421 プロファイルは署名検証成功後に同じボディ整形式ルールを適用しなければならない（MUST）。`webhook_body_malformed` は `webhook_signature_digest_mismatch` とは別: 署名は有効。ボディが曖昧な状態にパースされる。

    **14a. Strict-parse 要件。** チェックは重複キーを露出するパーサーを使わなければならない（MUST）— それらを黙って破棄する last-wins/first-wins のデフォルトはこの要件を満たさない。「安全」または「厳格」とマーケティングされても、重複キー入力で衝突を表面化せずに値を返すクエリライブラリもこの要件を満たさない（cf. Go の `tidwall/gjson` — バリデーターではなくクエリライブラリ）。言語ごとの strict-parse エスケープハッチ、正準の非網羅リスト:

    * **Python**: stdlib `json.loads(..., object_pairs_hook=...)` — フック内で重複を検出して raise。チェックを満たす。
    * **Node**: `JSON.parse` に strict モードなし。重複キーイベントハンドラー付きのストリーミングパーサー（`stream-json`, `jsonparse`）を使う。`secure-json-parse` はデフォルトで不十分: その保護はプロトタイプ汚染キー（`__proto__`, `constructor`）を標的とし、データキー重複ではない（依然 last-wins で畳み込む）。データキー重複を明示的に拒否するよう設定するか、下にストリーミングパーサーを重ねる。
    * **Go**: `encoding/json` に strict モードなし、重複を検出しない。オブジェクトスコープごとの明示的な `map[string]struct{}` 一意キーガード付きの `json.Decoder` トークンウォーク、OR 明示的に有効化した `decoder.DisallowDuplicateKey()` 付きの `goccy/go-json`（デフォルトではない）を使う。このチェックに `tidwall/gjson` を使ってはならない — 衝突をシグナルせずに重複キー入力で最後の値を返すクエリライブラリ。
    * **Java**: Jackson `DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY`（デフォルト無効、明示的に有効化）。
    * **Ruby**: stdlib `JSON.parse` に検出フックなし。`allow_nan: false` / 重複拒否オプションを明示的に設定した `Oj.load(..., mode: :strict)` を使う。

    **14b. ロギング規律。** 検証者は `webhook_body_malformed` 拒否で完全なリクエストボディバイトをログすべきではない（SHOULD NOT）。`keyid`、ノンス、バイト長、特定の重複キー名のみをログ。侵害された署名者鍵を保持する攻撃者は、さもなくば攻撃者が選んだバイトを大規模に防御者のログに強制でき、フレームごとにリプレイキャッシュスロットを燃やしつつ、SIEM ポイズニングや認証情報流出のフォローオン攻撃のための攻撃者制御のログトレイルを残せます。重複キー名をログするとき、検証者は各名前を順に適用される次のルールでサニタイズしなければなりません（MUST）:

    * **(a) 最初の非印字コードポイントで切り詰め**、N が切り詰めプレフィックスのバイト長である `<sanitized:N>` を発行。これは位置情報を省く（キー名内の非印字文字の配置は、さもなくばそれ自体がビット位置としてエンコード可能な攻撃者チャネル）一方、「ここで何かが間違っていた」という診断シグナルを保持。非印字セットは少なくとも次を含まなければならない（MUST）: **C0 制御**（U+0000–U+001F）、**DEL**（U+007F）、**C1 制御**（U+0080–U+009F、マルチバイト形式でのターミナル制御セマンティクス）、**bidi 制御と分離子**（U+200E, U+200F, U+202A–U+202E, U+2066–U+2069 — ターミナルと SIEM UI での逆レンダリング）、**行と段落の分離子**（U+2028, U+2029 — 多くのログビューアで改行としてレンダリングされ行注入を可能にする）、**ゼロ幅文字**（U+200B–U+200D — 不可視難読化）、**バイトオーダーマーク**（U+FEFF — パーサー破損）。実装はセットをより広い Unicode 非印字分類に拡張してもよい（MAY）が、狭めてはならない（MUST NOT）— ASCII のみのチェックは、このルールが閉じるログ注入チャネルをまさに再び開く bidi オーバーライドと行分離子攻撃を見逃す。
    * **(b) 最後の完全な UTF-8 コードポイント境界で最大 32 バイトに切り詰め**。現実的な AdCP フィールド名はおよそ 24 文字（`signed_authorized_agents`）が上限なので、32 は寛大な上限でありつつ攻撃者制御バイト面を制限。切り詰めは 32 バイト以下の最後の完全な UTF-8 コードポイント境界で発生しなければならない（MUST）。マルチバイトシーケンスがコードポイント中間で分割されず、無効な UTF-8 がログに落ちないように（同じ入力を異なる無効 UTF-8 末尾に切り詰める異なる検証者もログ集約を壊す）。
    * **(c) 拒否ごとにログされる重複キー名の数を 4 で上限**、超過なら `<...N more>` を発行。4 vs 8 vs 16 の衝突キーを知る診断価値はほぼゼロ。

    これらの制約なしでは、キー名チャネルは攻撃者制御バイトサイドチャネルのまま — 完全ボディロギングより小さいが非ゼロで、ログ注入ベクトルとしてよく前例がある。上流入力の拒否をログする署名者（[重複オブジェクトキー署名者側ルール](#legacy-hmac-sha256-fallback-deprecated-removed-in-40)を参照）は、署名者側エラー出力で表面化する任意のキー名に同じ (a)/(b)/(c) サニタイズルールを適用しなければならない（MUST）。ワイヤー方向が逆でもチャネル形状は同一。

**Webhook キャッシュの要となる不変条件。** 署名者の秘密鍵なしの外部トラフィックはこのキャッシュを増やせません: ステップ 13 で認められる各エントリはすでにステップ 10 の暗号検証を通過しているので、キャッシュ増大を駆動する当事者は正当な鍵保持者か、鍵を侵害した者 — keyid ごとの上限（ステップ 9a）と新規 keyid 認可プレッシャーアラーム（[Webhook replay dedup sizing](#webhook-replay-dedup-sizing) を参照）が検出するよう設計されたケース — です。不変条件は[類似のリクエスト署名ルール](#verifier-checklist-requests)（そこのステップ 13 直後の「上限の要となる不変条件」段落を参照）をミラーします。Webhook チェックリストへの将来の編集はこの順序を保持しなければならない（MUST）: ステップ 13 の挿入をステップ 10 の署名検証の前に移すと、任意の外部当事者が偽造された構造的に有効な署名でキャッシュをフラッドできる。

Webhook パスに後続のブランド-オペレーター認可ステップはありません — 署名がセラーのアイデンティティを確立し、そのアイデンティティが Webhook を受け入れるのに十分です。`idempotency_key` のアプリケーション層重複排除は、重複した副作用から保護するため署名検証（ステップ 13）の後に実行されます。

**Webhook ごとに 1 署名。** 検証者はちょうど 1 つの `Signature-Input` ラベルを処理し、追加のラベルを無視しなければなりません（MUST）。

##### Webhook リプレイ重複排除のサイジング

Webhook のリプレイ重複排除は [Transport replay dedup](#transport-replay-dedup) の `(keyid, nonce)` キー形状と TTL セマンティクスを再利用しますが、バイヤー側キャッシュはバイヤーが統合するすべてのセラーからの署名を見ます — リクエスト側ケースとは根本的に異なるファンイン。

* **keyid ごとのエントリ上限**: 推奨 100,000 エントリ（リクエスト側 1,000,000 上限の 10 分の 1）。6 分ウィンドウで 100K の一意 Webhook を発行するセラーは単一署名者から 275/秒持続 — 通常オペレーションに十分な余裕でありつつ、誤設定または鍵侵害の強いシグナル。
* **集約キャッシュ上限**: すべての署名者にわたって推奨 `min(aggregate_memory_budget, 10,000,000)` エントリ。集約上限超過で、検証者は新しい署名を `webhook_signature_rate_abuse` で拒否しなければならず（MUST）、オペレーターにアラートすべき（SHOULD）— 黙った退避はまさに検証者が攻撃下にあるときにリプレイウィンドウを作る。
* **セラーごとの予算**: オペレーターは、すべてのセラーを各 100K で等重み付けするのではなく、統合の重要度でセラーごとに予算すべき（SHOULD）。支出コミットセラーの Webhook ファンインはディスカバリーのみのセラーのそれとは異なる。
* **新規 keyid 認可プレッシャー**（MUST 追跡、SHOULD アラート）。検証者は単位時間あたりに以前に見たことのない `keyid` から認められるキャッシュエントリのレート（例: 最初のエントリを挿入する別個の `keyid` の 5 分ローリングカウント）を追跡しなければならない（MUST）。新規 keyid 認可レートの急なスパイクは**分散侵害攻撃**のシグネチャです: N 個の侵害された署名者鍵を保持する攻撃者は、各鍵が keyid ごとの上限（ステップ 9a）内に十分収まりつつ、集合的に集約キャッシュを飽和させ、TTL ウィンドウごとに各鍵 N エントリを駆動できます。各鍵のトラフィックは個別には低ボリュームの正当な署名者に見えます。集約形状がシグナルです。

  検証者は、新規 keyid 認可が 4 つの閾値の**いずれか**（最初にトリガーするもの）を超えたときアラートすべきで（SHOULD）、各々が別個の攻撃者パターンを閉じます:

  * **(a)** 現在の認可レートを短期地平の移動平均ベースラインと比較する**短ウィンドウ比率閾値** — 安定ベースラインに対する急なスパイクを捕捉。
  * **(b)** 中期地平パーセンタイルベースラインに対する**中ウィンドウ比率閾値** — その地平でトラフィックがベースライン末尾に支配される数週間のランプアップ攻撃を捕捉。
  * **(c)** 長期地平パーセンタイルベースラインに対する**長ウィンドウ比率閾値** — 中期地平アンカーを自らとともにドリフトさせる数ヶ月のランプアップ攻撃を捕捉。
  * **(d)** 絶対フロアと文書化されたウィンドウにわたる一意 keyid カウントの一部を組み合わせた**比例上限** — 比率ベースラインがゼロ近くのスパーストラフィック検証者を捕捉し、AND 任意のサイズのオペレーターに自動スケール（小さな検証者は低い比例フロアを得、エンタープライズ検証者は比例的に大きいものを得る）。

  **4 つのカテゴリは規範的。具体的な閾値はそうではない。** オペレーターは任意の公開された例値を出発点として扱い、自身のトラフィックをベースライン化し、それに応じて調整しなければなりません（MUST）— 公開された規範閾値数は攻撃者に検出姿勢へのオラクルを渡します。具体的な開始値、ベースライン化方法論、攻撃シナリオウォークスルーは非規範的な [Webhook Verifier Tuning Guide](/docs/building/by-layer/L1/webhook-verifier-tuning) で公開されています。実装はガイドの開始値を初回デプロイデフォルトとして出荷してもよい（MAY）が、各閾値を調整可能な設定パラメーター（例: 環境変数、設定ファイル）として公開しなければなりません（MUST）— ハードコードされた開始値は事実上オペレーター可視のデフォルトになり攻撃者オラクルを再導入します。実装は、任意の閾値が検証者の最初の認可より 30 日を超えて出荷開始値のままであるとき `threshold_tuning_overdue` イベントをログまたはアラームすべきです（SHOULD）。これはオペレーター調整義務に、オペレーターの勤勉さだけに頼るのではなくテスト可能・監査可能なフックを与えます。

  アラームペイロードは、オペレーターのトリアージが正しい脅威形状に応答できるよう、どの節（a、b、c、d）がトリップしたかを名指さなければなりません（MUST）。ここでのアラームは、集約上限がトリガーする*前*にスローバーン分散侵害パターンを捕捉します — 集約上限で `webhook_signature_rate_abuse` が発火すると、キャッシュはすでに満杯で、すべての正当な署名者が拒否されています。アラームは自動失効ではなくインシデントレスポンスにルーティングすべきです（SHOULD）: 「攻撃」と「新しいセラーのバッチをオンボーディング」の区別シグナルはオペレーターコンテキストで、マシン導出可能ではなく、アラームでの自動失効は DoS ベクトルを作ります（正当な新規署名者オンボーディングを駆動する任意の当事者がアラームをトリップして大量失効を引き起こせる）。

**クロスエンドポイントスコープ（MUST）。** 複数の Webhook エンドポイント（統合ごと、環境ごと、テナントごと、または水平スケールされたフリートのポッドごと）を公開するバイヤーは、次のいずれかをしなければなりません（MUST）:

1. ある署名者が到達できるすべてのエンドポイントにわたって**単一の論理リプレイキャッシュを共有**（Redis / 共有重複排除サービス — プロセスごとインメモリではない）。エンドポイント A が挿入した `(keyid, nonce)` がステップ 12 実行前にエンドポイント B に見えるように。または
2. **正準宛先 URL をリプレイキーに含める**、重複排除を `(keyid, canonical destination URL, nonce)` にスコープ。正準形は [リクエスト署名プロファイル](#adcp-rfc-9421-profile)に従う正規化後の `@target-uri`（スキーム小文字、ホスト IDNA 正規化、デフォルトポート省略、フラグメント除去）。

オプション 1 がより強い — ±360 秒ウィンドウ内でクロスエンドポイントリプレイをきっぱり拒否。オプション 2 はより弱い — 同じ `(keyid, nonce)` が各別個のエンドポイント URL でリプレイ可能だが、署名済み `@target-uri` が署名でカバーされるので、エンドポイント B の検証者はエンドポイント A 向けに署名された `@target-uri` を持つ任意のペイロードを `webhook_signature_digest_mismatch`（正準署名ベースが失敗）または `webhook_signature_invalid` で拒否。オプション 2 は署名者の正準 `@target-uri` がエンドポイントごとのときのみ許容。複数エンドポイントに同じペイロードを署名する署名者はオプション 2 を無効にし、オプション 1 を使わなければならない（MUST）。

共有層なしのポッドごとまたはリージョンごとの*インメモリ*リプレイキャッシュは、複数エンドポイントを実行するバイヤーには非コンフォーマント: ±360 秒と攻撃者が別のポッドにルーティングする能力のみに制限されるクロスエンドポイントリプレイウィンドウを残します。オペレーターは Webhook フリートを共有重複排除層でフロントするか、上記のエンドポイントごと URL スコープを文書化・強制するかしなければなりません（MUST）。

[Transport replay dedup](#transport-replay-dedup) の他のすべてのルールがそのまま適用されます: 単一プロセス検証者のインメモリ LRU、高ボリュームでの Redis `SETNX`、分散デプロイのステップ 13 でのアトミック挿入-上限チェック。

##### Webhook の失効とローテーション

署名者はリクエスト署名に使うのと同じ結合失効リストを介して失効を公開しなければなりません（MUST）— [Transport revocation](#transport-revocation) を参照。オペレーターオリジンごとの単一リストがガバナンス署名、リクエスト署名、Webhook 署名鍵をカバーします。

**HMAC→9421 移行。** HMAC から 9421 に移行するバイヤーは、セラーが切り替えを確認したら HMAC 検証者を無効化しなければなりません（MUST）。両検証者を同時に実行することは、HMAC パスを元の 5 分リプレイウィンドウ + バイヤーが切るのを忘れた時間の分、悪用可能なままにします。「念のため」の運用姿勢は非推奨パスを意図された非推奨を過ぎてライブに保ちます。セラーは以前に 9421 に移行された相手方からの `authentication` ブロックを拒否し、拒否をログすべきです（SHOULD）。切り替えウィンドウ中、バイヤーは両検証者を実行してもよい（MAY）が、どちらのスキームの下でも同じ論理イベントが同じ `(sender identity, idempotency_key)` タプルにマップされるよう単一の重複排除キースペースを維持すべきです（SHOULD）— 混在モード配信下の重複排除スコープは [Reliability](/docs/building/by-layer/L3/webhooks#reliability) セクションを参照。

##### Webhook エラータクソノミー

コードは [request-signing error taxonomy](#transport-error-taxonomy) と並行し、SDK エラー処理が 2 つのプロファイルを区別するよう `webhook_` プレフィックス付き。バイヤーはこれらのいずれでもセラーに `401` を返してもよい（MAY）。セラーのリトライループは同じ署名バイトでリプレイするので、この表のすべてのコードは送信者にリトライ不可 — 署名失敗、オーソリティミスマッチ、モードミスマッチはすべてリトライで同一の出力を生む — HTTP セマンティクスがリトライを許可しても。

| Failure                                                                                 | Code                                      |
| --------------------------------------------------------------------------------------- | ----------------------------------------- |
| `Signature` または `Signature-Input` ヘッダーが不正、または一方が他方なし                                    | `webhook_signature_header_malformed`      |
| 必須 sig-param 不在                                                                         | `webhook_signature_params_incomplete`     |
| `tag` が `adcp/webhook-signing/v1` でない                                                   | `webhook_signature_tag_invalid`           |
| `alg` が許可リストにない                                                                         | `webhook_signature_alg_not_allowed`       |
| 署名ウィンドウ無効                                                                               | `webhook_signature_window_invalid`        |
| 必須カバーコンポーネント欠落（`content-digest` を含む）                                                    | `webhook_signature_components_incomplete` |
| 1 回の再フェッチ後 `keyid` がセラー JWKS にない                                                        | `webhook_signature_key_unknown`           |
| JWK `adcp_use` ∉ {`webhook-signing`, `request-signing`}、不在、または `key_ops` が `verify` を欠く | `webhook_signature_key_purpose_invalid`   |
| `keyid` ∈ `revoked_kids`                                                                | `webhook_signature_key_revoked`           |
| 失効リストが grace 内にリフレッシュされていない                                                             | `webhook_signature_revocation_stale`      |
| 暗号検証失敗                                                                                  | `webhook_signature_invalid`               |
| `content-digest` ミスマッチ                                                                  | `webhook_signature_digest_mismatch`       |
| ボディが重複オブジェクトキーを含む（パーサー差分攻撃クラス）                                                          | `webhook_body_malformed`                  |
| `@authority` が署名済み `@target-uri` オーソリティコンポーネントと一致しない（クロス vhost リプレイ）                    | `webhook_target_uri_malformed`            |
| ノンスがウィンドウ内ですでに見られている                                                                    | `webhook_signature_replayed`              |
| keyid ごとのリプレイキャッシュが上限超過                                                                 | `webhook_signature_rate_abuse`            |
| 登録された認証モードが受信 Webhook の署名モードと一致しない                                                      | `webhook_mode_mismatch`                   |

**検証失敗のリトライセマンティクス。** 少なくとも 1 回の配信は送信者に任意の非 2xx レスポンスでリトライするよう伝えますが、検証失敗は一時的エラーではありません — 署名バイトとリクエストコンテキストは各リトライで同一に到着するので、各リトライは同一に失敗します。送信者は `WWW-Authenticate: Signature error="webhook_*"`（上記タクソノミーで定義された任意のコード、`webhook_signature_*`, `webhook_target_uri_malformed`, `webhook_mode_mismatch` を含む）を運ぶ `401` レスポンスを、その特定の配信試行の終端失敗として扱わなければなりません（MUST）: 現在のイベントのリトライを停止し、オペレーターの注意のためエラーコードで失敗をログし、後続イベントの通常のリトライキューを続ける。送信者は、オペレーター定義の閾値を超える持続的な `webhook_*` エラーレートを、発行し続けるのではなくインシデントレスポンスにルーティングすべきです（SHOULD）— 持続的な署名、オーソリティ、モード失敗は鍵ローテーション調整問題、誤設定検証者、または侵害を示し、すべて人間のアクションが必要。レシーバーはこれらの失敗を黙って破棄してはならず（MUST NOT）、オペレーターログでの表面化がセキュリティ姿勢の一部。

**将来の追加に関する編集者注記。** 上記のワイルドカード `webhook_*` 終端失敗分類は eager sweep です: タクソノミーに追加される任意の新コードは、個別レビューなしに配信ごと終端セマンティクスを継承します。リトライ可能であるべき新しい `webhook_*` コード（例: 将来の一時的インフラシグナル）を追加する編集者は、追加の時点で例外を切り出すようこの段落を更新しなければなりません（MUST）— まだ定義されていないコードについてパターンマッチが安全なままであることに頼らない。

##### Webhook 移行タイムライン

| Phase  | Behavior                                                                                                                                                                                                                        |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 3.0 GA | 9421 Webhook 署名は Webhook を発行する任意のセラーのベースライン。バイヤーが `push_notification_config.authentication.credentials` または `accounts[].notification_configs[].authentication.credentials` を設定するときレガシー HMAC-SHA256 フォールバック利用可能。セラーはサポートを断ってもよい。 |
| 3.x    | HMAC フォールバックは非推奨。セラーは選択時に警告をログすべき（SHOULD）。SDK は依然 `authentication` を設定するバイヤーに非推奨通知を表面化すべき（SHOULD）。                                                                                                                              |
| 4.0    | `push_notification_config` と `accounts[].notification_configs[]` の `authentication` がスキーマから削除。9421 Webhook 署名が唯一のサポートパス。                                                                                                        |

#### TMP クロスリファレンス

**TMP 鍵は別個の `adcp_use` 値を宣言しなければならない（MUST）**（または完全に省略）。検証者がステップ 8 を介してリクエスト署名でそれらを拒否するように。TMP 鍵をリクエスト署名と Webhook 署名鍵と同じ `jwks_uri` で公開することは許可され推奨されます — 1 つの公開パターン、5 つの署名システム、各々 `kid` スコープ:

* ガバナンス JWS — `adcp_use: "governance-signing"`
* リクエスト署名（RFC 9421）— `adcp_use: "request-signing"`（Webhook にも署名。[Webhook callbacks](#webhook-callbacks) を参照）
* Webhook 署名（RFC 9421）— `request-signing` 鍵を使用。レガシー `adcp_use: "webhook-signing"` 値は**非推奨**（依然受け入れ、削除保留 — 非推奨注記のフォローアップイシューを参照）
* 指定タスクレスポンスペイロード JWS — `adcp_use: "response-signing"`（上記の [Designated-task payload-envelope response signing](#designated-task-response-signing) を参照）
* TMP エンベロープ — TMP 独自の将来の `adcp_use` 値

すべての検証者が自身のプロファイルで正確な `adcp_use` 一致を強制するので、クロス目的再利用は自動的に防がれます。

Trusted Match Protocol はマッチ時リクエストに独自の Ed25519 エンベロープで署名します。TMP のリクエストごと予算（約 5% でサンプル検証）は、すべての呼び出しでの完全な RFC 9421 検証には厳しすぎます。**TMP 署名はこのセクションのスコープ外**です。このプロファイルは TMP 鍵が同じ JWKS でリクエスト署名鍵と並んで公開される方法のみを制約します。

#### トランスポート移行タイムライン

AdCP 4.0 は次の破壊的変更蓄積ウィンドウです。支出コミットオペレーションの必須リクエスト署名はそのフロア要件の 1 つ — AdCP 4.0 支出トラフィックの最小セキュリティバー — であり、唯一の目玉機能ではありません。他の v4.0 変更は[ロードマップ](/docs/reference/roadmap#v40-planned)に蓄積されます。

| Phase  | Status                                   | Behavior                                                                                                                                                     |
| ------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 3.0 GA | Optional, capability-advertised          | 検証者は検証してもよい。デフォルトで `required_for: []`。署名者は署名してもよい。リファレンスベクター出荷。リファレンス SDK パイロット開始。                                                                           |
| 3.x    | Reference SDKs ship; pilots surface bugs | コンフォーマンステストベクターがクロス SDK 相互運用を駆動。早期採用者が名指しの相手方で段階的に `required_for` を有効化。                                                                                      |
| 4.0    | Required for spend-committing operations | `required_for` は `create_media_buy`, `acquire_*`, 検証者がサポートする任意の支出コミットオペレーションを含まなければならない。署名者は署名しなければならない。それらのオペレーションに `covers_content_digest: "required"` 推奨。 |

3.x で署名を出荷する実装は、実トラフィックに対してエンドツーエンドパスを検証するため、4.0 の前に検証者側 `required_for` を選択的に（相手方ごとパイロット、その後より広いロールアウト）有効化すべきです（SHOULD）— これがエコシステム全体の破壊なしに 4.0 移行を実現可能にするものです。

#### リクエスト検証者リファレンス（TypeScript）

説明目的のみ。`verify9421` と `parseSignatureInput` コールバックはプロトコル固有の正準化と署名検証をカプセル化します。実装は [`/compliance/latest/test-vectors/request-signing/`](https://adcontextprotocol.org/compliance/latest/test-vectors/request-signing/) の AdCP コンフォーマンステストベクターに対して検証された特定の RFC 9421 ライブラリをピン留めすべきです。

```ts theme={null}
import { createRemoteJWKSet } from "jose";

class RequestSignatureError extends Error {
  constructor(public code: string) { super(code); }
}

const ALLOWED_ALGS = new Set(["ed25519", "ecdsa-p256-sha256"]);
const REQUIRED_TAG = "adcp/request-signing/v1";
const REQUIRED_COMPONENTS = new Set(["@method", "@target-uri", "@authority"]);
const REQUIRED_PARAMS = ["created", "expires", "nonce", "keyid", "alg", "tag"] as const;

export async function verifyAdcpRequestSignature(req: Request, ctx: {
  operationName: string;
  requiredFor: Set<string>;
  contentDigestPolicy: "required" | "forbidden" | "either";
  resolveJwk: (keyid: string) => Promise<{ jwk: unknown; agentUrl: string }>; // throws _key_unknown after refetch
  isKeyRevoked: (keyid: string) => Promise<boolean>;
  isRevocationStale: () => Promise<boolean>;
  isKeyidAtCapacity: (keyid: string) => Promise<boolean>;
  isReplayed: (keyid: string, nonce: string) => Promise<boolean>;
  recordNonce: (keyid: string, nonce: string, ttlSeconds: number) => Promise<void>;
  verify9421: (req: Request, jwk: unknown, covered: string[]) => Promise<void>; // throws on signature or digest failure
  parseSignatureInput: (header: string) => {
    keyid?: string; alg?: string; created?: number; expires?: number;
    nonce?: string; tag?: string; components: string[];
  };
}) {
  const sigInput = req.headers.get("signature-input");

  // Pre-check: required_for / downgrade protection.
  if (!sigInput) {
    if (ctx.requiredFor.has(ctx.operationName)) throw new RequestSignatureError("request_signature_required");
    return; // operation doesn't require a signature; verify nothing.
  }

  let parsed;
  try { parsed = ctx.parseSignatureInput(sigInput); }
  catch { throw new RequestSignatureError("request_signature_header_malformed"); }

  // 2: presence
  for (const p of REQUIRED_PARAMS) {
    if ((parsed as any)[p] == null) throw new RequestSignatureError("request_signature_params_incomplete");
  }
  // 3: tag
  if (parsed.tag !== REQUIRED_TAG) throw new RequestSignatureError("request_signature_tag_invalid");
  // 4: alg
  if (!ALLOWED_ALGS.has(parsed.alg!)) throw new RequestSignatureError("request_signature_alg_not_allowed");
  // 5: window (including expires > created)
  const now = Math.floor(Date.now() / 1000);
  if (parsed.expires! <= parsed.created! ||
      parsed.created! > now + 60 ||
      parsed.expires! < now - 60 ||
      parsed.expires! - parsed.created! > 300) {
    throw new RequestSignatureError("request_signature_window_invalid");
  }
  // 6: components
  for (const c of REQUIRED_COMPONENTS) {
    if (!parsed.components.includes(c)) throw new RequestSignatureError("request_signature_components_incomplete");
  }
  const coversCd = parsed.components.includes("content-digest");
  if (ctx.contentDigestPolicy === "required" && !coversCd) {
    throw new RequestSignatureError("request_signature_components_incomplete");
  }
  if (ctx.contentDigestPolicy === "forbidden" && coversCd) {
    throw new RequestSignatureError("request_signature_components_unexpected");
  }
  // 7: JWK resolution
  const { jwk } = await ctx.resolveJwk(parsed.keyid!); // throws _key_unknown
  // 8: key purpose
  const j = jwk as any;
  if (j.use !== "sig" || !Array.isArray(j.key_ops) || !j.key_ops.includes("verify") || j.example_use !== "request-signing") {
    throw new RequestSignatureError("request_signature_key_purpose_invalid");
  }
  // 9: revocation (BEFORE crypto verify)
  if (await ctx.isRevocationStale()) throw new RequestSignatureError("request_signature_revocation_stale");
  if (await ctx.isKeyRevoked(parsed.keyid!)) throw new RequestSignatureError("request_signature_key_revoked");
  // 9a: per-keyid cap (BEFORE crypto verify) — prevents amplified crypto work by abusive/misconfigured signer.
  if (await ctx.isKeyidAtCapacity(parsed.keyid!)) {
    throw new RequestSignatureError("request_signature_rate_abuse");
  }
  // 10 + 11: crypto verify, content-digest recompute — both inside verify9421.
  try { await ctx.verify9421(req, jwk, parsed.components); }
  catch (e: any) {
    if (e?.code === "digest_mismatch") throw new RequestSignatureError("request_signature_digest_mismatch");
    throw new RequestSignatureError("request_signature_invalid");
  }
  // 12: replay check
  if (await ctx.isReplayed(parsed.keyid!, parsed.nonce!)) {
    throw new RequestSignatureError("request_signature_replayed");
  }
  // 13: replay insert (only after all checks pass)
  await ctx.recordNonce(parsed.keyid!, parsed.nonce!, (parsed.expires! - now) + 60);
}
```

### 予算検証

コミット前に予算を検証します:

```javascript theme={null}
async function validateBudget(request, account) {
  const { budget } = request;

  // Check positive amount
  if (budget.amount <= 0) {
    throw new ValidationError('Budget must be positive');
  }

  // Check against account limits
  const limits = await getAccountLimits(account.account_id);
  if (budget.amount > limits.daily_spend_limit) {
    throw new BudgetError('Exceeds daily spend limit');
  }

  // Check available balance
  const balance = await getAvailableBalance(account.account_id);
  if (budget.amount > balance) {
    throw new BudgetError('Insufficient balance');
  }
}
```

## トランスポートセキュリティ

AdCP のアプリケーション層セキュリティプリミティブ（9421 署名、JWS ガバナンス、冪等性）は、トランスポートが攻撃者を助けないことを前提とします。誤設定された TLS スタックはその前提を壊します — アクティブな経路上の敵対者に耐えるよう設計されたプロトコルを、すべての中間者を信頼するものに格下げします。

このセクションはすべての AdCP エンドポイント — インバウンド（セラーとバイヤーの API 面）とアウトバウンド（JWKS フェッチ、brand.json フェッチ、失効リストフェッチ、Webhook 配信）— で規範的です。オペレーターが午前 3 時に暗号スイートについて第一原理から推論しなくてよいよう、意図的に規定的です。

### TLS バージョンポリシー

* **TLS 1.3 がすべての AdCP エンドポイントで RECOMMENDED。**
* **TLS 1.2 が最小。** エンドポイントはハンドシェイクで TLS 1.1 以下を拒否しなければなりません（MUST）。
* **クライアント側検証者**（例: 相手方の JWKS、brand.json、失効リストをフェッチする AdCP サーバー）は TLS 1.2 未満をネゴシエートすることを拒否しなければなりません（MUST）。「互換性」のため依然 TLS 1.0 をデフォルトとするライブラリは明示的に設定されなければなりません（MUST）。
* SSL 2.0、SSL 3.0、TLS 1.0、TLS 1.1 は有効化してはなりません（MUST NOT）— どのエンドポイントでも、どのレガシーパートナーでも、別のポートでも。

### 暗号スイートとアルゴリズム

* TLS 1.3: IETF 定義スイート（`TLS_AES_128_GCM_SHA256`, `TLS_AES_256_GCM_SHA384`, `TLS_CHACHA20_POLY1305_SHA256`）を使います。3 つとも AEAD。他の TLS 1.3 スイートは存在しません。それらを恣意的に無効化しないでください — 「速度」を理由に ChaCha20 を無効化するオペレーターは、1 つのクライアントの癖でモバイルクライアントを壊す寸前です。
* TLS 1.2: **AEAD のみ**の ECDHE スイートに制限。許可セットは `ECDHE-ECDSA-AES128-GCM-SHA256`, `ECDHE-ECDSA-AES256-GCM-SHA384`, `ECDHE-ECDSA-CHACHA20-POLY1305`, `ECDHE-RSA-AES128-GCM-SHA256`, `ECDHE-RSA-AES256-GCM-SHA384`, `ECDHE-RSA-CHACHA20-POLY1305`。
* CBC-MAC、RC4、3DES、DES、NULL、EXPORT、匿名 DH、静的 RSA 鍵交換スイートは TLS 1.2 で無効化されなければなりません（MUST）— その存在はハンドシェイクの上に構築されたすべてのセキュリティ特性を黙って格下げします。
* サーバー証明書は ECDSA（P-256 または P-384）または RSA ≥ 2048 ビットを使わなければなりません（MUST）。RSA \< 2048 は使ってはなりません（MUST NOT）。
* エンドポイントはサーバー側暗号順序（OpenSSL `SSL_OP_CIPHER_SERVER_PREFERENCE`、nginx `ssl_prefer_server_ciphers on`）を優先しなければなりません（MUST）。強いスイートが相互に利用可能なとき、弱いクライアントが弱いスイートを強制できないように。

### 証明書検証（アウトバウンドフェッチ）

AdCP が行うすべてのアウトバウンド HTTPS リクエスト — JWKS、brand.json、失効リスト、Webhook コールバック、アグリゲータープロキシ — は完全な PKIX 検証を実行しなければなりません（MUST）。具体的なチェック:

* **トラストチェーン**はオペレーターが意図的に含めたパブリックルートで終端しなければなりません（MUST）。本番コードパスのどこにも `--insecure`、`verify=False`、`rejectUnauthorized: false` なし。これは単独で最も一般的な本番侵害です — エンジニアがステージングの証明書問題を回避するため検証を切り、そのフラグが出荷される。
* **SAN 一致**が権威的なアイデンティティチェックです。証明書は URL ホストに一致する Subject Alternative Name エントリを持たなければなりません（MUST）。CN のみのフォールバックは受け入れてはなりません（MUST NOT）。主要な HTTP クライアントはレガシーの理由で依然それをサポートしますが、AdCP 検証者は SAN を要求しなければなりません（MUST）。
* **有効期限**は現在のクロックに対してチェックされなければなりません（MUST）。TLS 証明書が先週期限切れになったドメインから JWKS をフェッチすることは、互換性問題ではなくガバナンスのレッドフラグです。
* **ホスト名検証**はライブラリ設定で有効化されなければなりません（MUST）。いくつかの人気の HTTP クライアントライブラリはホスト名検証をデフォルトでオンにして出荷しますが、驚くほど多くがそれを無効にするフラグを持ちます。AdCP 実装はホスト名検証がオンであることを仮定するのではなくアサートしなければなりません（MUST）。
* **OCSP ステープリング**は提供されたとき受け入れるべきです（SHOULD）。オペレーター制御の証明書での OCSP must-staple は RECOMMENDED。Must-staple は欠落したステープルをハード失敗に変え、OCSP でのソフト失敗の抜け穴を閉じます。
* **Certificate Transparency（CT）** SCT は規制された支出を提供するエンドポイントでチェックされるべきです（SHOULD）。ブラウザはすでに CT を強制します。規制カテゴリのワークフローでガバナンス JWKS をフェッチする AdCP SDK もそうすべきで（SHOULD）、隠された誤発行証明書が検出可能に。
* **ピン留め**はプロトコル層で必須ではなく、正当なオペレーター証明書ローテーションと衝突するため相手方が供給する URL（brand.json、JWKS）では避けるべきです（SHOULD）。パブリック CA チェーンへのピン留め（中間ピン）は許容。特定のリーフ証明書へのピン留めは非推奨。

### インバウンドサーバー側ヘッダー

```javascript theme={null}
app.use((req, res, next) => {
  // HSTS: 1 year, include subdomains, preload-eligible. MUST be on every HTTPS response.
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');

  // No framing of AdCP API responses — even though they're JSON, frame isolation
  // protects any error or debug HTML that could leak through.
  res.setHeader('X-Frame-Options', 'DENY');

  // MIME sniffing off: responses declare their type, clients MUST respect it.
  res.setHeader('X-Content-Type-Options', 'nosniff');

  // Prevent referrers leaking to external URLs supplied by counterparties.
  res.setHeader('Referrer-Policy', 'no-referrer');

  // AdCP endpoints serve no browser-facing HTML — block script-source loading outright.
  // If your operator reuses the same origin for a dashboard, adjust this per-path.
  res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'");

  next();
});
```

**HSTS max-age は AdCP エンドポイントを提供する任意のドメインで ≥ 31536000（1 年）でなければなりません（MUST）。** オペレーターに文書化された理由がない限り `includeSubDomains` を設定しなければなりません（MUST）。支出コミット AdCP エンドポイントを提供するドメインは HSTS プリロードリストに提出すべきです（SHOULD）。

### クライアント / アウトバウンド TLS ハードニング

アウトバウンドフェッチのコードパス（ガバナンス JWKS、brand.json、失効リスト、Webhook 配信、アグリゲータープロキシ）は次をしなければなりません（MUST）:

* ホストごとの固定上限と全体の固定上限を持つ接続プールを使う。無制限のプールはリソース枯渇面。
* TLS ハンドシェイク時間をデフォルトで 10 秒、総リクエスト時間を 30 秒で上限 — 相手方が供給する URL はさもなくばタールピット DoS ベクトル。
* 接続を [SSRF 制御](#webhook-url-validation-ssrf)を通過した IP アドレスにピン留め — SSRF チェックと実際の接続の間の DNS 再解決が TOCTOU バイパスが着地する方法。
* セキュリティ機微なフェッチでリダイレクトを拒否。JWKS、brand.json、失効リスト、Webhook コールバックのフェッチはリダイレクトに従ってはならず（MUST NOT）、[brand.json 解決ルール](#buyer-identity-resolution)はすでに「1 リダイレクト（`authoritative_location` または `house` バリアント）、チェーンなし」と述べ、初回の `/.well-known/adagents.json` フェッチは同一登録可能ドメインリダイレクトのみに従う（apex↔www、HTTPS 保持、3 ホップ以下、最初に要求されたドメインに固定）— 他のすべての場所ではゼロ。`adagents.json` の `authoritative_location` 参照は「他のすべての場所」: ゼロリダイレクト。
* 信頼境界をまたぐセッション再開を無効化。攻撃者制御の相手方との TLS セッションを後の検証済み相手方（DNS リバインド経由の同じ IP）に再開することはよく知られた混同のクラス。ライブラリのデフォルトは通常問題ないが、オペレーターは監査しなければなりません（MUST）。

### TLS 再ネゴシエーションとダウングレード

* TLS 1.2 の**セキュア再ネゴシエーション**（RFC 5746）は、再ネゴシエーションがサポートされる場合有効化されなければなりません（MUST）。非セキュア再ネゴシエーション許容スタックは MUST-disable。
* **TLS 圧縮**（CRIME）はオフでなければなりません（MUST）。
* **Heartbeat 拡張**は TLS 1.2 エンドポイントでオフでなければなりません（MUST、Heartbleed 系統）。
* TLS 1.3 の **0-RTT / early-data** は、変更系 AdCP オペレーションを受け入れる任意のエンドポイントで有効化してはなりません（MUST NOT）。0-RTT は設計上リプレイ可能です。冪等性と署名ノンス重複排除は、リクエストがアプリケーションロジックに到達した後は無料の救済ではありません。読み取り専用ディスカバリーエンドポイント（`get_adcp_capabilities`, `list_creative_formats`）は 0-RTT を使ってもよい（MAY）。他のすべては使ってはなりません（MUST NOT）。

### mTLS トランスポート

[mTLS](/docs/building/by-layer/L2/authentication#mtls) が認証メカニズムのとき:

* クライアント証明書 SAN / Subject は、`adagents.json` または `brand.json` で宣言されたバイヤーの登録済みドメインに一致しなければなりません（MUST）。任意のヘッダーフィールド（`X-Forwarded-Client-Cert`, `X-Client-DN` など）に頼ることは[明示的に禁止](#buyer-identity-resolution)されています — ヘッダーフィールドは誤設定プロキシをまたいで注入され得ます。
* 終端エッジ（ロードバランサー、メッシュサイドカー）は、検証済み証明書アイデンティティを、サーバーが認証できるクラスタ内チャネルで AdCP サーバーに転送しなければなりません（MUST）。未認証のサイドカーヘッダーはバイパス — mTLS をエンドツーエンドでデプロイするか、クラスタ内チャネルをピン留めします。
* クライアント証明書はオペレーターが運用する CRL または OCSP レスポンダーに対してチェックされなければなりません（MUST）。「私たちが発行した」は「まだ有効」と同じではありません。

### プライベートネットワークとメタデータ保護

このセクションのトランスポート制御は、相手方が供給する URL の [SSRF 制御](#webhook-url-validation-ssrf)を代替しません。相手方 URL へのすべてのアウトバウンドフェッチは SSRF ルールを適用しなければなりません（MUST）— 非 HTTPS を拒否、予約範囲（クラウドメタデータアドレスを含む）の IP を拒否、リダイレクトを拒否、サイズと時間に上限。URL が `169.254.169.254` を指すなら TLS は無用です。

### このセクションが置き換えないもの

トランスポートセキュリティは天井ではなくフロアです。完璧な TLS スタックでも次を置き換えません:

* **アプリケーション層のボディ完全性**（[リクエスト署名](#request-signing)と [Webhook コールバック](#webhook-callbacks)）— TLS はワイヤーを保護し、侵害された中間者後のペイロードは保護しません。
* **ガバナンス証明**（[署名付きガバナンスコンテキスト](#signed-governance-context)）— TLS は、バイヤーのガバナンスエージェントがこの支出を認可したかをセラーに伝えません。
* **冪等性**（[Request Safety](#request-safety)）— TLS は、送信者がネットワークタイムアウト後にリトライするのを防ぎません。

「私たちは現代的な TLS 設定を持つ」を「私たちの AdCP デプロイは安全」と混同するオペレーターは、まさにボディバインド署名プロファイルが防御するために存在するオペレーターです。

## 入力検証

### リクエスト検証

すべてのユーザー提供入力を検証します:

```javascript theme={null}
const INPUT_LIMITS = {
  targeting_brief_max_length: 5000,
  creative_upload_max_size: 100 * 1024 * 1024, // 100MB
  max_formats_per_request: 50,
  max_products_per_query: 100
};

function validateRequest(request) {
  // Check string lengths
  if (request.brief?.length > INPUT_LIMITS.targeting_brief_max_length) {
    throw new ValidationError('Brief exceeds maximum length');
  }

  // Validate IDs are proper UUIDs
  if (request.product_id && !isValidUUID(request.product_id)) {
    throw new ValidationError('Invalid product_id format');
  }

  // Reject unexpected fields
  const allowedFields = ['brief', 'product_id', 'budget', 'context_id'];
  for (const field of Object.keys(request)) {
    if (!allowedFields.includes(field)) {
      throw new ValidationError(`Unexpected field: ${field}`);
    }
  }
}
```

### SQL インジェクション防止

常にパラメーター化クエリを使います:

```javascript theme={null}
// GOOD: Parameterized query (request-supplied account_id after auth precheck)
const result = await db.query(
  'SELECT * FROM media_buys WHERE id = $1 AND account_id = $2',
  [mediaBuyId, request.account.account_id]
);

// BAD: String concatenation (NEVER do this)
// const result = await db.query(
//   `SELECT * FROM media_buys WHERE id = '${mediaBuyId}'`
// );
```

## 監査ログ

### 必須ログイベント

すべてのセキュリティ関連イベントをログします:

```javascript theme={null}
const LOG_EVENTS = {
  AUTH_SUCCESS: 'auth_success',
  AUTH_FAILURE: 'auth_failure',
  BUDGET_COMMIT: 'budget_commit',
  BUDGET_MODIFY: 'budget_modify',
  ACCESS_DENIED: 'access_denied',
  WEBHOOK_VERIFIED: 'webhook_verified',
  WEBHOOK_REJECTED: 'webhook_rejected'
};

function logSecurityEvent(eventType, details) {
  console.log(JSON.stringify({
    event: eventType,
    timestamp: new Date().toISOString(),
    agent_id: details.agentId,
    account_id: details.accountId,
    ip_address: details.ipAddress,
    resource: details.resource,
    outcome: details.outcome,
    // NEVER log: credentials, PII, targeting briefs
  }));
}
```

### ログ保持

* セキュリティログ: 最低 90 日（365 日推奨）
* 金融ログ: 7 年（コンプライアンス要件）
* アクセスログ: 最低 30 日

## セキュリティチェックリスト

### パブリッシャー（AdCP サーバー）向け

* [ ] 強力な認証を実装（OAuth 2.0、API キー、または mTLS）
* [ ] すべてのデータベースクエリでエージェントとアカウントの分離を強制
* [ ] 金融オペレーションに冪等性を実装
* [ ] 厳格なスキーマ検証ですべての入力を検証
* [ ] すべての通信に TLS 1.3+ を使用
* [ ] Webhook 署名を暗号学的に検証
* [ ] すべてのセキュリティイベントを改ざん不可能にログ

### バイヤーエージェント（AdCP クライアント）向け

* [ ] 認証情報をセキュアな鍵管理システムに保管
* [ ] 認証情報を 90 日ごとにローテーション
* [ ] すべての AdCP 通信に HTTPS を使用
* [ ] パブリッシャーからのレスポンスを検証
* [ ] 異常な支出パターンのアラートを実装

### オーケストレーター（マルチエージェント、マルチアカウント）向け

* [ ] 各エージェントの認証情報を別々に保管（暗号化）
* [ ] すべてのクエリでエージェントとアカウントのフィルタリングを強制
* [ ] データベースで行レベルセキュリティを使用
* [ ] すべてのオペレーションをエージェントとアカウントのアイデンティティ付きでログ
* [ ] エージェントごとのレート制限を実装

## 次のステップ

* **Security Model**: このリファレンスが実装する脅威モデルと 5 層防御の物語は [Security Model](/docs/building/concepts/security-model) を参照
* **Webhooks**: Webhook セキュリティパターンは [Webhooks](/docs/building/by-layer/L3/webhooks) を参照
* **Error Handling**: 認証エラーは [Error Handling](/docs/building/by-layer/L3/error-handling) を参照
* **Orchestrator Design**: マルチテナントセキュリティは [Orchestrator Design](/docs/building/operating/orchestrator-design) を参照
