なぜを探していますか? このページは規範的な実装リファレンス — コンフォーマントなエージェントが従うルールです。脅威モデル、層状防御の物語、ブランド IT と CISO 向けのチェックリストは、Security Model を参照してください。
概要
AdCP は次のような高リスク環境で動作します:- 金銭取引: 実際の広告費が動く
- 複数主体の信頼: 認証済みエージェント、パブリッシャー、オーケストレーター間の連携が必要
- 機微なデータ: 1P シグナル、未公開クリエイティブ、競合ターゲティング戦略を含む
- 非同期オペレーション: 複数のシステムとプロトコルにまたがる
リスク分類
高リスクオペレーション(金融)
これらのオペレーションは実際の広告予算をコミットします:
要件:
- 短命な認証情報 — 漏洩したトークンの影響範囲に見合ったサイズにする。支出をコミットできるトークンには 1 時間以内が妥当なデフォルト。相当な閾値を超える支出をコミットできる、または組織境界をまたぐトークンには 15 分以内が適切。最小の数字をデフォルトにするのではなく、選択したウィンドウを文書化して正当化する。
- トランザクション整合性のためのリクエスト署名
- 大規模予算向けの多要素認証または承認ワークフロー
- 改ざん不可能なログによる完全な監査証跡
中リスクオペレーション(データアクセス)
これらのオペレーションは機微なビジネスデータにアクセスします:低リスクオペレーション(ディスカバリー)
これらのオペレーションは公開アクセス可能です:Webhook セキュリティ
AdCP 3.0 は Webhook 署名を AdCP RFC 9421 プロファイルに統一します — セラーはオペレーターの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 プロファイルに従って署名しなければなりません(MUST)。
レガシー HMAC-SHA256 フォールバック(非推奨、4.0 で削除)
9421 プロファイルをまだ採用していないレシーバーと相互運用する必要のあるバイヤーは、push_notification_config.authentication.credentials または accounts[].notification_configs[].authentication.credentials を設定してオプトインしてもよい(MAY)。バイヤーのリクエストに authentication が存在する場合、セラーは Push Notifications で定義されたセマンティクスを使って 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)— トークン間に空白なし。言語レベルのシリアライザー JavaScriptJSON.stringify、Goencoding/jsonjson.Marshal、RubyJSON.generate、Java JacksonwriteValueAsStringはデフォルトでコンパクト出力を生成します。それらをラップする HTTP クライアント(axios、json.Marshalされたボディを持つ Gonet/http、JSON.generateを持つ RubyNet::HTTP、Jackson を持つ Java OkHttp)はそのデフォルトを継承します。Python ではhttpxはコンパクトセパレーターでシリアライズしますが、stdlibjson.dumpsはデフォルトで", "/": "になり、separatorskwarg なしでペイロードをjson.dumpsに渡す HTTP クライアント(requests(json=...)、aiohttp)は空白入りのボディを発行します — それらのパスの署名者はseparators=(",", ":")を明示的に渡さなければなりません(MUST)。この列挙は網羅的ではありません。署名者はこのリストに頼るのではなく、HTTP クライアントの実際のワイヤー上のシリアライズを検証しなければなりません(MUST、例: プロキシやフックでリクエストボディをキャプチャ)。署名は、署名者がシリアライズしたオブジェクトではなく、レシーバーが見るバイトをカバーします。 - 正準化されない側面: キー順序、ユニコードエスケープポリシー、数値表現はこのスキームで正準化されません。特に数値については言語デフォルトが発散し(
JSON.stringify(1.0)→1、Pythonjson.dumps(1.0)→1.0、Gojson.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を参照してください。検証者側のコンフォーマンスフィクスチャは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で定義された同じキー名サニタイズルール(最初の非印字文字で<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()、FastAPIRequest.body()、aiohttpRequest.read()、json.Unmarshal前の Goio.ReadAll(r.Body))。生キャプチャフックは同じルート上のいかなる JSON パースミドルウェアの前に実行しなければなりません(MUST)。検証者が実行される前にリクエストボディを消費するグローバルにマウントされたexpress.json()または FastAPIBaseModelボディバインディングは、署名されたバイトではなく再文字列化されたペイロード上で検証者を動作させます — これは一般的なデプロイミスです。検証者はパースされたペイロードを再シリアライズして署名されたバイトを再構築すべきではありません(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)。
X-ADCP-SignatureまたはX-ADCP-Timestampヘッダーが欠落していれば拒否- タイムスタンプが非数値なら拒否
- タイムスタンプが 5 分ウィンドウ外なら拒否
- 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 で定義されています。
相手方が制御する URL へのアウトバウンドフェッチの前に、フェッチャーは次を行わなければなりません(MUST):
- 本番で非 HTTPS URL を拒否する。
- ホスト名を解決し、解決された 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 アドレス。
- IPv4: RFC 1918(
- 接続を検証済み IP にピン留めする。 DNS ベースのフィルタリングだけでは DNS リバインディングに脆弱です: 攻撃者は検証時にパブリック IP を、接続時にプライベート IP を提供します。フェッチャーは接続をピン留めしなければなりません(MUST)。推奨: (a) 検証済み IP を TCP connect 呼び出しに直接渡し、
Host:ヘッダーを URL から設定する。フォールバック(HTTP クライアントが事前解決 IP を受け付けられない場合のみ): (b) いかなるリクエストボディを送る前に、ソケットのハンドシェイク後ピアアドレスを予約範囲リストに対して検証する。注: (b) は最初のボディバイトが出荷される前に発火するピアアドレスフックをクライアントライブラリが公開することに依存します。多くの一般的なライブラリはそうしないため、(b) を選ぶ実装はテストでフックを検証しなければなりません(MUST)。ピン留めなしの DNS 再解決は不十分です。 - 相手方制御の URL をフェッチする際、リダイレクトの追従を拒否する(30x レスポンスは、オリジンが最初のチェックをバイパスした予約アドレスにリダイレクトすることを許す)。2 つの制限された例外があり、各ホップでステップ 1-3 を再検証しチェーンに上限を設ける: brand.json 解決(1 リダイレクト、チェーンなし)と、初回の
/.well-known/adagents.jsonフェッチ(標準の apex→www ホスティングが解決するよう、同一登録可能ドメインリダイレクトのみ追従 —apex ↔ www、HTTPS 保持、3 ホップ以下、最初に要求されたドメインに固定 — managed networks を参照)。adagents.jsonのauthoritative_location参照は例外を取りません: その 2 番目のホップでのリダイレクトは拒否しなければなりません(MUST)。 - レスポンスサイズとタイムアウトに上限を設ける。 推奨: 5 MB ボディ上限、10 秒接続、10 秒読み取り。唯一の例外は、マネージドネットワーク間接パターンでの参照解決された権威的ファイル — ポインターファイルの
authoritative_locationがネットワークオリジンにリダイレクトした後の 2 番目のホップのみ — で、パブリッシャーネットワーク横断でファンアウトするため推奨 20 MB 上限を使います。ポインターファイル自体は 5 MB のままです。managed networks security を参照。 - 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" を超えて制約されません。ハードモードのポートフィルタリングはオペレーター側のポリシー選択であり、プロトコル側の要件ではありません。
機能固有のセキュリティセクションは、これらのルールを独自のライフサイクルとコンテンツ処理要件で拡張します:
- オフラインレポートバケット — IAM 層のプレフィックススコープ、アカウントステータス変更時の認証情報失効。
- コレクションリスト —
auth_tokenスコープと失効、配信 ID 検証、Webhook 署名の規範ルール。 - マネージドネットワークの
authoritative_location— バリデーターのフェッチセマンティクス、変更検出、関係終了。 - TMP プロバイダー登録 — 動的登録認証、ルーター-プロバイダー間認証、
/healthの情報漏洩ルール。
認証のベストプラクティス
認証情報の保管
トークンの有効期限
高リスクオペレーションには短命なトークンを使います:エージェントとアカウントの分離
あらゆる状態 — メディアバイ、クリエイティブ、冪等性キャッシュエントリ、セッション ID、ガバナンストークン — は、それを所有するアカウントにスコープされます。クロスアカウント読み取りは、存在を漏らすのではなく汎用の「not found」を返さなければなりません(MUST)。認証済みエージェントは、セラーが誰が呼んでいるかを知る手段です。リクエストのaccount はその呼び出しが作用している請求関係です。分離には両方のチェックが必要です。
セールスエージェントは次を行わなければなりません(MUST):
- 作成時にバインド — 各オブジェクト(メディアバイ、クリエイティブ、セッションなど)を、それを作成したリクエストで使われたアカウントに恒久的に関連付ける。
- アクセス時に検証 — 後続の各読み取りまたは変更で、認証済みエージェントがオブジェクトのバインドされたアカウントへのアクセス権を持つことを検証する。
- フェイルクローズ — 検証が失敗した場合、汎用エラーを返す(ステータス 403 または 404 が許容されるが、ボディは「未認可」を「not found」と区別したりアカウントを名指ししたりしてはならない)。決してリソースクエリにフォールスルーしない。
二段階パターン
スキーマがaccount を要求するすべてのアカウントスコープリクエストは、明示的な AccountRef(アカウント ID 名前空間では account_id、バイヤー宣言アカウントでは {brand, operator} 自然キー)を運びます。セラーは、欠落した必須 account を認証情報が示すデフォルトで黙って置き換えてはなりません(MUST NOT)。account が任意のタスクでは、省略セマンティクスはタスクローカルで、そのタスクが文書化しなければなりません。正しい分離は、順に実行される 2 つのチェックです:
- 認可プリチェック — リクエストの
accountは認証済みエージェントの認可セット内になければなりません(MUST)。403 または汎用の「not found」でフェイルクローズ(決して「あなたはそのアカウントに認可されていません」ではない — それは存在の漏洩です)。 - リソースクエリ — リクエストの
account_idを主キー制約としてフィルタリング。認可セット全体ではなく、このリクエストが作用している特定のアカウントのみで。
get_media_buy(X) は、両方がエージェントの認可セット内にあれば、アカウント B が所有するバイに対して成功してしまいます。リクエストが供給する account_id が、ルックアップを呼び出し元の表明された意図に結び付けるものです。
行レベルセキュリティ
最も一般的な分離の失敗は、結合またはネストされた関係を介した IDOR です: クエリが主テーブルをaccount_id でスコープするが、同じプリンシパルでフィルタリングされなかった関連テーブル(ラインアイテム、クリエイティブ、配信行)から結合または返す。1 つのハンドラーのバグが壁を突き破れないよう、ハンドラーコードだけでなくデータ層でプリンシパルごとに防御します:
get_media_buys)では、RLS は認証時に設定されるセッション変数を介してエージェントの認可セットにスコープします:
クライアント側の分離: クロスプリンシパルのツールコール混同
上記のルールはサーバー側の強制です。正当だが侵害されたエージェントが呼び出し元であっても、セラーのデータを保護します。クライアント側の相棒は、プリンシパル X が供給したテキストにプリンシパル Y の権限を使うツールコールを駆動させないというバイヤーエージェントの義務です。 LLM 駆動のバイヤーエージェントは通常、複数のプリンシパルの認証情報を同時に保持します: 複数のセラー(セラーごとに 1 つの認証情報セット)と、エージェンシーエージェント内では複数のブランドアカウント。エージェントが処理する任意の信頼できない文字列 — セラーが返すプロダクト説明、ブリーフから継承されたキャンペーン名、エラーエンベロープの拒否理由、Webhook イベントボディ — は、それらのプリンシパルの1 つから供給されたテキストです。エージェントのプランニングループが単一の LLM コンテキストからそれらすべてにわたってツールを呼べる場合、セラー X のテキストに注入されたプロンプトが、エージェントにセラー Y のエンドポイントでcreate_media_buy を呼ばせたり、ブランド A の予算をブランド B のインベントリに使わせたりできます。これはツールコール粒度での混乱した代理人問題です: 攻撃者はサンドボックスを脱出する必要がありません — エージェント自身の正当な権限が損害を与えます。
LLM 駆動の AdCP エージェントを運用するオペレーターは、少なくとも次の制御を適用しなければなりません(MUST):
- テキストにその起源プリンシパルをタグ付けする。 LLM コンテキストがネットワークから取り込むすべての文字列(ツール結果、Webhook ボディ、レジストリドキュメント、クリエイティブメタデータ)は、それを生成した
{principal_domain, tool_name, response_field}トリプルで内部的に注釈されなければなりません(MUST)。取り込み時に注釈を落とすことが、この防御が死ぬ場所です。 - ツールコールのターゲットを呼び出しプリンシパルに制限する。 ターゲットプリンシパルが、決定を駆動する文字列を供給したプリンシパルと同じでないツールコールは、(a) 拒否されるか、(b) 人間の承認ステップを通るか、(c) オペレーターが事前に宣言した明示的なプリンシパルごとのポリシーで仲介されなければなりません(MUST)。デフォルトは allow ではなく refuse でなければなりません(MUST)。
- 認証情報スコープを LLM コンテキストごとに分離する。 単一の LLM プランニングループは、利害が衝突し得るプリンシパル(例: 同じインベントリを競う 2 つのブランド。1 つのコンテキスト内のバイヤー認証情報とガバナンスエージェントの署名鍵)のライブ認証情報を保持してはなりません(MUST NOT)。スコープ分離は、LLM に指示するのではなく、プロセス / ツール登録層で強制されます — LLM は誤用のアフォーダンスを持ってはなりません(MUST NOT)。
- 成功だけでなく、すべてのクロスプリンシパルの試みをログする。 ルール 2 の下での拒否は、オペレーターが監視しなければならないシグナルです(MUST)— あるプリンシパルからの拒否率の上昇は、あなたのエージェントを標的とする注入キャンペーンの最も早く検出可能な兆候です。
時間セマンティクス
AdCP は管轄区域、アドサーバー、デイパートカレンダーをまたいで動作します。実装は時間について正確でなければならず(MUST)、さもなくばバイヤーとセラーは「午後 5 時までに配信」が何を意味したかで意見が食い違います。タイムスタンプ形式
AdCP のリクエスト、レスポンス、Webhook ペイロードのすべてのタイムスタンプフィールドは、明示的なタイムゾーンオフセット付きの ISO 8601 でなければなりません(MUST)。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 のカットで閉じます。
@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 を参照)。バイヤーは呼び出し時に、ある呼び出しが純粋な読み取り、非同期タスク作成、コミットのどれになるかを予測できません — したがってワイヤー契約はすべての呼び出しで一律に idempotency_key を要求します。純粋な読み取りとして解決する呼び出しでは、キャッシュは TTL 内でバイト安定なリトライ時リプレイを提供し、これは無害でバイヤーに一律のリトライセーフな契約を与えます。非同期タスク作成またはコミットとして解決する呼び出しでは、キャッシュは変更系タスクと同じ at-most-once 保証を提供します。代替案 — バイヤーの SDK で呼び出しごとに読み取り vs 変更系を分類 — は、同じタスク名が読み取りと書き込みの両モードを持つとき実現不可能です。セラーが返す未知の error.code 値のデコード(猶予ウィンドウ中の INVALID_REQUEST でも、後のマイナーで追加されたコードでも)は Forward-compatible decoding ルールに従います。
このセクションは AdCP タスクリクエストにのみ適用されます。OpenRTB 入札ストリームは独自のセマンティクス(BidRequest.id は冪等性キーではなくトランザクション ID)を持ち、スコープ外です。
規範的なセラーの動作
-
スキーマ検証が最初に実行される。 セラーは、冪等性キャッシュを参照する前に、リクエストをそのスキーマ(
idempotency_keyの存在と形式を含む)に対して検証しなければなりません(MUST)。不正なリクエストはキャッシュに一切触れずにINVALID_REQUESTを返します — さもなくばキャッシュミスがタイミングサイドチャネルになり、スキーマ検証がキー形式を受け入れたかを漏らします。検証エラーは決してキャッシュされません(ルール 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)。下記「バイヤーの義務」を参照。
- 非同期タスク — キャッシュされたレスポンスは
- 成功レスポンスのみがキャッシュされる。 いかなるエラー — 検証、ガバナンス拒否、トランスポート失敗、内部エラー — でもキーは保存されません。リトライは再実行します。これはバイヤーの意図に一致します: 5xx 後のリトライは失敗をリプレイするのではなく再試行すべきです。また、バイヤーの不正リクエストがキーに TTL 全体ロックされるのを防ぎます。
-
リプレイはキャッシュされたレスポンスを返す。 同じ
idempotency_keyかつ等価な正準形ペイロード(下記「ペイロード等価性」を参照)を持つ後続リクエストは、副作用を再実行せずに保存された内側レスポンスを返さなければなりません(MUST)。セラーはレスポンス時に送信プロトコルエンベロープにreplayed: trueを注入します —replayedは冪等性層が生成するエンベロープレベルのフィールドであり、キャッシュされた内側レスポンスの一部ではありません。リプレイ時の注入により、エンベロープ変更(新しいtimestamp、ローテーションされたgovernance_contextなど)に関わらずキャッシュされたペイロードがリプレイ間でバイト安定に保たれます。MCP のトランスポート固有の注記: MCP ツールレスポンスは別個のエンベロープスロットを持ちません。サーバーはreplayedをツール結果オブジェクト自体の中(例: 構造化リターンの先頭)またはレスポンスメタデータフィールドで公開してもよい(MAY)。REST と A2A レスポンスはエンベロープフィールドを直接使います。 -
異なる正準ペイロードでのキー再利用は競合。 同じキー、リプレイウィンドウ内で異なる正準ハッシュは
IDEMPOTENCY_CONFLICTで拒否しなければなりません(MUST)。セラーは 2 番目のリクエストを黙って適用してはなりません(MUST NOT)。 -
期限切れキーは明示的に拒否される。
replay_ttl_secondsが経過した後、セラーはキャッシュエントリを退避してもよい(MAY)。セラーが見たことのあるキーで退避後に到着するリクエストは、黙って新規として扱うのではなくIDEMPOTENCY_EXPIREDで拒否すべきです(SHOULD)— 黙った再実行は、まさにキーが防ぐはずのダブルブッキングのフットガンです。セラーは TTL 境界で ±60 秒のクロックスキューウィンドウ(本書の他所で JWSexpに適用される許容範囲と同じ)を許可すべきです(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を保証される最短ポッド寿命以下に宣言することが求められます — 実際上、これは耐久層を強制します。 -
リプレイウィンドウは推論ではなく宣言される。 セラーは
get_adcp_capabilitiesでcapabilities.idempotency.replay_ttl_secondsを宣言しなければなりません(MUST、最小 3600 秒 / 1 時間、推奨 86400 秒 / 24 時間、最大 604800 秒 / 7 日)。クライアントは想定デフォルトにフォールバックしてはなりません(MUST NOT)— 宣言のないセラーは非準拠で、リトライ機微なオペレーションには安全でないものとして扱わなければなりません(MUST)。 -
キャッシュ増大防御。 セラーは、リクエストレート制限とは別に、
(authenticated_agent, account)ごとの冪等性キャッシュ挿入レート制限を適用しなければならず(MUST)、エージェントごとの挿入レートが設定上限を超えたときはキャッシュを無制限に増大させるのではなくRATE_LIMITED(error taxonomy を参照)を返さなければなりません(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 ルール下でのダッシュボードポーリングとエージェンティック状態再読み取りが支配的。読み取りトラフィックは通常、ユーザー駆動の UI 操作中はバースト的、エージェント実行中は低レートで安定。
- 書き込み: 60 挿入/秒持続、300/秒バースト。 元の書き込み重視サイジングから変更なし — バイヤーのダッシュボードポーリングが、
create_media_buy/sync_creatives/activate_signalをダブル実行レースから守る書き込み容量を枯渇させられないよう、別個の予算として保持。 - 合算上限(多層防御): 総挿入はエージェントごとに 350/秒持続 / 1,700/秒バーストを超えるべきではありません(SHOULD NOT)— 小さなクッション付きの 2 予算の合計。読み取り予算を飽和させる攻撃者が書き込み容量を飢えさせられないように。
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 に従いretry_after(秒)を設定しなければならず(MUST)、冪等性レスポンスとしてキャッシュされてはなりません(MUST NOT、ルール 3: 成功レスポンスのみキャッシュ)。セラーはretry_afterを安価な拒否フロアとして強制すべきです(SHOULD)—retry_after経過前にリトライするバイヤーは、リトライごとにフルのスキーマ検証・キャッシュチェックパイプラインに再入するのではなく、事前認証トークンバケット(例: リバースプロキシ層)にヒットすべきです(SHOULD)。この規律なしでは、誤動作するバイヤーがレートリミッター自体の負荷を増幅できます。 -
並行リトライ — 最初の挿入が勝つ。 同じ
(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で新しいキーを生成するバイヤーは、安全なリトライを、まさにこのルールが防ぐダブル実行レースに変えます。
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)。 - Wait-and-replay(高速オペレーション向け推奨、通常 5 秒未満): セラーは最初が完了するまで 2 番目のリクエストをブロックし、その後
-
サービス境界をまたぐ — ダウンストリームリコンシリエーション。 セラーはリクエスト処理中にダウンストリームシステムを呼び出すのが一般的です —
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、同じ監査ログ、同じオペレーター)に生のキーを渡すことは信頼境界をまたがず、許可されます(ただしプロバイダーごとの導出が依然としてより良いデフォルト)。
idempotency_key(またはその可逆な派生)をエラーエンベロープに含めてはなりません(MUST NOT)。セラーのダウンストリームプロバイダーごとのキー(またはセラーが誤って生でスレッド化した場合はバイヤーのキー)に言及するダウンストリームエラーは、バイヤーに伝播する前に再キーまたは除去されなければなりません(MUST)— さもなくばダウンストリームエラーメッセージが信頼境界をまたぐキー開示面になります。 このルールのバイヤー可視の帰結: セラーが低速ダウンストリームを呼び出し、バイヤーがウィンドウ中にリトライするとき、2 番目のリクエストでのセラーのレスポンスは、ダウンストリームの動作ではなく、ルール 9 の下でのセラーのポリシー(IDEMPOTENCY_IN_FLIGHTまたは wait-and-replay)で決まります。バイヤーはどのダウンストリームがパスにあるか知る必要はありません — セラーは関わらず一律のリトライ面を提示しなければなりません(MUST)。 - Write-claim-before-invoke(推奨デフォルト)。 ダウンストリームを呼び出す前に、セラーは冪等性キャッシュ行と同じトランザクションで「クレーム」行を永続化します — 通常
ペイロード等価性
「等価」とは、フィールドごとのセマンティック比較ではなく、同一の正準 JSON 形式を意味します。セラーは正準形をハッシュしてハッシュを比較することで等価性を判定しなければなりません(MUST)。正準形は RFC 8785 JSON Canonicalization Scheme (JCS) です — 数値シリアライズ、キー順序、エスケープはすべて 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またはcanonicalize - Python:
pyjcsまたは RFC 8785 appendix のリファレンス実装 - Go:
gowebpki/jcs - Rust:
serde_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)。
additionalProperties: true デフォルトのサーバー側対応物です。スキーマ自身の additionalProperties 宣言と矛盾する形でサーバー側バリデーターを設定することはコンフォーマンス違反です。一般的なサーバー実装の罠:
- 厳格なシグネチャの FastMCP / Pydantic —
def get_products(brief: str)と宣言されたツールラッパーは、バイヤーが同じ params オブジェクト内にidempotency_keyを送るとunexpected_keyword_argumentを送出します。修正:idempotency_key: str | None = None(および他のエンベロープフィールド)を受け入れて無視する任意パラメーターとして宣言するか、**kwargscatch-all を使って未知のキーを破棄します。Pydantic-on-input はExtra.allowまたはmodel_config = ConfigDict(extra='allow')を使います。 .strict()付きの Zod / valibot はインバウンドリクエストスキーマで同じ理由で未知のキーを拒否します。入力スキーマで.strict()を外すか、passthrough バリアントで合成します。- codegen ツールが
additionalProperties: falseを注入した OpenAPI 生成サーバースタブ — 生成された入力スキーマがスペックのadditionalProperties: trueデフォルトをミラーすることを検証します。一部のジェネレーターはモデル発行時にデフォルトを反転させます。
runner-output-contract.yaml > response_schema_validator_semantics でレスポンス側バリデーター向けにすでに確立されたバリデーターごとのパターンを一般化します — 両ルールは同じ原則(「バリデーター設定はスキーマ自身の additionalProperties 宣言と矛盾してはならない」)をワイヤーの両端で表現します。
レスポンスレベルのリプレイインジケーター
プロトコルエンベロープは、冪等性キャッシュを介して解決された任意のリクエストへのレスポンスにトップレベルのreplayed ブール値を運びます:
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)。
fieldjson-pointer ヒントは無害に見えますがスキーマ形状を明かします(例:/packages/0/budgetは攻撃者に、被害者のペイロードが最初のパッケージに予算を持っていたと伝えます)。セラーは発行してはなりません(MUST NOT)。リトライをデバッグする正当なバイヤーは自身の 2 つのペイロードを diff できます — 両方を持っています。
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まで)リプレイし、黙って古いデータを返します — まさにキャッシュが変更系で防ぐ失敗モードです。このルールは下記の リプレイレスポンスは履歴スナップショット パターンの再読み取りステップも規定します: 「現在状態の再読み取り」呼び出しは新しいキーを運ばなければならず(MUST)、決して状態を読んでいる変更のキーを使いません。
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(規範的) に従ってこれらをデコードしなければなりません(MUST)— 復旧分類のため error.recovery を読み、recovery が不在のとき transient をデフォルトとし、コード値が馴染みないという理由でレスポンスを決して拒否しない。transient 分類エラーのリトライセマンティクスは § 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)— 自然キーは設計上公開であり、オラクル防御として直接使えません。
自然キー冪等性は代替にならない
アップサート型タスク(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)。
- 監査人と規制当局はガバナンスエージェントの公開鍵を使って独立に検証します — これが署名形式が提供するために存在するアカウンタビリティ特性です。
スコープと依存関係
- スコープ内(3.0): バイサイドガバナンス。
governance_contextトークンは、AdCP タスク(create_media_buy,acquire_rights,activate_signal,creative_services)を介してなされる支出コミットメントを認可します。独自のコンプライアンスポリシーを実行するセラー(例: CTV 政治広告ルール、パブリッシャーブランドセーフティゲート)は、それらを独自のガバナンスワークフロー上のconditionsレスポンスで表現します。このプロファイルの下では署名付きトークンを発行しません。 - スコープ外(3.0): セラーサイドのガバナンス当局。将来の RFC が
adagents.jsonを介して宣言されるセラーサイドの署名付き決定をカバーするようこのプロファイルを拡張するかもしれません。 - スコープ外(永久): OpenRTB 入札ストリーム。ガバナンスの証明は AdCP メディアバイ境界で終わります。署名付き証明をインプレッションごとの入札リクエストに通すことは運用上実現不可能(1 トークン、多数の受信者、ブロードキャストファンアウト)で不要(支出認可はインプレッションごとではなくメディアバイ時に発生)です。
iss クレームとは独立にバイヤードメインを確立できることに依存します — 下記の 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)。これはプロファイルが後のバージョンでクレームを追加するときの黙ったダウングレード攻撃を防ぎます。
未知クレームの扱い: 検証者は、認識しない名前のクレームを無視しなければなりません(MUST)ただしそれらのクレーム名がトークンの
crit ヘッダーに現れる場合を除き、その場合トークンを拒否しなければなりません(MUST)。この非対称ルール — 未知は無視、未知かつクリティカルは拒否 — が、プロファイルの将来バージョンが、まだ更新していない検証者の後方互換性を壊さずにセマンティックに意味のあるクレームを追加する方法です。
サイズ: policy_decision_hash を持つ典型的なトークンは 4096 文字のエンベロープ上限に余裕を持って収まります。実装は大きな証拠ペイロードをトークンに入れてはなりません(MUST NOT)。代わりに audit_log_pointer を使います。
plan_hash は監査層でありワイヤー層ではない: plan_hash クレームは、ガバナンスエージェント、監査人、バイヤー側コンプライアンスによるオフワイヤー検証のためにトークンが運ぶ暗号学的カーゴです。このプロファイルのセラー検証契約の一部ではなく、決して crit にリストされません。正準化、除外フィールド、保持ルール、テストベクターは Plan binding and audit(ガバナンススペック)で規定されます。セラーは governance_context をそのまま永続化・転送し、plan_hash を検査せずに下記の 15 ステップ検証チェックリスト — 真正性、認可スコープ、鮮度 — を実行します。
バイヤーアイデンティティ解決
brand.json クロスチェック(検証チェックリストのステップ 13)がアンチスプーフィング制御です。それはセラーがどのバイヤーの brand.json を参照するかを知ることを要求します — 認証済みエージェントが誰が呼んでいるかを証明し、解決チェーンがそのエージェントを、セラーが brand.json をフェッチすべきバイヤードメインにマップします。3.0 でセラーは次のいずれかを介してバイヤードメインを確立しなければなりません(MUST):- mTLS: バイヤーがクライアント証明書を提示。証明書の Subject/SAN がバイヤーの登録済みドメインに解決。セラーが
https://{domain}/.well-known/brand.jsonをフェッチ。 - 事前プロビジョニングされたバイヤーアイデンティティ: オンボーディング時にセラーが発行し、セラーの記録でバイヤーのドメインにマップされた API キーまたは OAuth クライアント識別子。
- #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)を介してガバナンスエージェントの公開鍵を解決します:- Buyer identity resolution のルールを介してバイヤードメインを確立する。
- バイヤーの brand.json をフェッチする。
typeがgovernanceでurlがトークンのissとバイト単位で等しいagents[]エントリを見つける。一致するエントリがなければ拒否する。 - 宣言されていればエントリの
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 つの集約ドキュメントではない — がスケール時のコンフォーマントなパス。 - JWKS を HTTPS でフェッチする。
- JWKS 内で
kidがトークンヘッダーに一致する鍵を見つける。kidのキャッシュミスでは、拒否する前に JWKS を 1 回再フェッチする(無制限の再フェッチを防ぐため最小 30 秒のクールダウンを尊重)。
kid が revoked_kids に追加されても、セラーの JWKS キャッシュが検証のために失効した鍵をまだ提供する場合、失効チェック(ステップ 14 で独立に実行)のみが不正を捕捉します。
SSRF 保護: jwks_uri と失効リスト URL は相手方が供給します。これらの URL へのすべてのアウトバウンドフェッチは Webhook URL validation で定義された SSRF 制御に従わなければなりません(MUST): 非 HTTPS を拒否、予約範囲(クラウドメタデータアドレスを含む)の解決 IP を拒否、接続を検証済み IP にピン留め、リダイレクトを拒否、レスポンスサイズとタイムアウトに上限、相手方への詳細なエラーメッセージを抑制。鍵ディスカバリーで SSRF 規律のない JWS プロファイルはメタデータ流出ベクトルです。
セラー検証チェックリスト
リクエストをガバナンス承認済みとして扱う前に、セラーはこれらのチェックを順に実行し、最初の失敗でショートサーキットしなければなりません(MUST):- コンパクト JWS をパースする。不正なら拒否。
- ヘッダー
algがnoneまたは許可リスト(EdDSA、ES256)にない場合拒否。ライブラリのデフォルトに頼ってはならない(MUST NOT)。 - ヘッダー
typが正確にadcp-gov+jwsでない場合拒否(正規化なし)。 - ヘッダーが
crit配列を含み、リストされた名前が検証者に認識されない場合拒否。 - 上記のディスカバリールールで
issを JWKS に解決する。JWKS がフェッチできない(SSRF 検証後)か 1 回の再フェッチ後にkidが存在しない場合拒否。 - JWKS エントリの
useが"sig"でkey_opsが"verify"を含むことを検証。他の用途にマークされた鍵は拒否。 - 署名を暗号学的に検証する。
audが関連するadagents.jsonエントリで宣言されたセラー自身の正準 URL とバイト単位で等しくない場合拒否。expが過去、またはiatが 60 秒より先の未来の場合拒否(±60 秒クロックスキュー許容、両境界で対称)。nbfが存在する場合、now < nbf − 60 sなら拒否。subがこのトークンが添付されているガバナンス呼び出しのplan_idと等しくない場合拒否(プランスワップを防ぐ)。phaseがオペレーションに一致しない場合拒否:create_media_buyにはpurchase、update_media_buyにはmodification、デリバリーレポートコールバックにはdelivery、intentはセラー前のバイヤー側評価のみ。- 非インテントトークンでは、
media_buy_idがリクエストのメディアバイ ID と等しくない場合拒否。 - クロスチェック: トークンの
issはバイヤーの現在の brand.json(Buyer identity resolution を介して確立)にガバナンス型エージェントとして現れなければならない(MUST)。セラーは妥当な TTL(1 時間推奨)で brand.json をキャッシュし、検証失敗時にリフレッシュすべき(SHOULD)。 - 失効リスト(Revocation を参照)を確認する。
jti∈revoked_jtisまたはトークンヘッダーのkid∈revoked_kidsなら拒否。このチェックはキャッシュミス時だけでなくすべての検証で実行される。 jtiがこの(iss, aud)タプルで以前に見られている場合拒否。ストレージガイダンスは Replay dedup を参照。
plan_hash を検証しないことに注意 — そのクレームはガバナンスエージェント / 監査人層でバインドされます(Plan-state binding を参照)。
リプレイ重複排除
ステップ 15 はリプレイを防ぐためjti 値の追跡を要求します。素朴な実装 — 無制限のセット — はメモリリスクであり DoS ベクトル(攻撃者がストレージを枯渇させるため一意のトークンでセラーをフラッド)でもあります。
スケーリング推奨:
- 実行トークンの
expを 30 日で上限(ガバナンスエージェントが強制。セラーはそれより長いものを拒否)。これは重複排除ウィンドウを制限します。 - 高速パスチェックとして小さな偽陽性率(約 100 万分の 1)の
(iss, aud, jti)でキーされたブルームフィルターを使い、ブルームフィルターヒット時のみ制限されたストア(RedisSET jti NX EX <remaining_ttl>、TTL クリーンアップ付き Postgres 一意インデックス)で権威的ルックアップ。 - ガバナンスエージェントは、セラーが重複排除ストアを時間ウィンドウでパーティション化して期限切れパーティションを安価にドロップできるよう、
jti値を時間順序付け可能な形式(UUID v7 または ULID)で発行すべき(SHOULD)。
失効
exp ベースの期限切れだけでは、メディアバイのライフサイクルの間生きる実行フェーズトークンをカバーしません。ガバナンスエージェントは{origin of iss}/.well-known/governance-revocations.json に失効リストを公開しなければならず(MUST)、同じ JWKS の鍵を使ってリスト自体に署名しなければなりません(MUST):
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)。
サーバーは内部検証詳細(例: どの特定クレームが不一致だったか)を相手方にエコーしてはなりません(MUST NOT)。上記の安定コードを返し、詳細はサーバー側でログします。
プライバシー考慮事項
policy_decisions の可視性: トークンは JWS(公開鍵を持つ誰でも読める)であり JWE(暗号化)ではありません。policy_decisions がガバナンスエージェントが評価したポリシー ID の完全なリストを含む場合、トークンを受け取るすべてのセラーは、バイヤーのガバナンス姿勢が考慮するポリシーを学びます — 競合インテリジェンス、場合によっては機微なオーディエンス特性についてのシグナリング(例: minors_compliance ポリシー ID は 18 歳未満オーディエンスのターゲティングを示唆)。ガバナンスエージェントは、バイヤーのコンプライアンス姿勢が機微なとき policy_decisions の代わりに policy_decision_hash を使うべきです(SHOULD)。完全なログはガバナンスエージェント制御のアクセスで audit_log_pointer を介して監査人に利用可能なままです。
インテントフェーズのセラー開示(GA へ): aud バインディングは、競合オークションで N セラーを評価するバイヤーが、各々 1 セラーに aud バインドされた N 個の別個のインテントトークンを要求しなければならないことを意味します。したがってガバナンスエージェントはバイヤーが考慮したセラーの完全なリストを見ます — セラーがインテント時に GA に未知だった不透明文字列モデルに対するプライバシー退行。これは明示的なトレードオフです: クロスセラーリプレイ耐性はセラーごとのバインディングを要します。将来の aud_hash メカニズム(トークンがトークンスコープのソルトでセラー URL のハッシュをバインドし、各セラーが検証のため自身の URL でハッシュを計算)は、リプレイ耐性を犠牲にせずに GA に対するインテント時のセラープライバシーを回復できます。3.0 では定義されていません。フォローアップとして追跡されています。
caller URL: オーケストレーターの識別子を含みます。トークンを長期保持するセラーと監査人は、これが示唆する保持ポリシーに注意すべきです。
リファレンス実装
デコードされた例トークン(インテントフェーズ): ヘッダー:jose で約 30 行):
移行(3.0 → 3.1)
- 3.0: ガバナンスエージェントは、必須の
plan_hash監査層クレーム(セマンティクスは 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は監査層のまま(ガバナンスエージェント / 監査人 / バイヤーコンプライアンス検証のみ — セラー検証ではない)。
署名付きリクエスト(トランスポート層)
署名付きガバナンスコンテキストは認可アーティファクトに署名します。リクエスト署名はリクエスト自体 — メソッド、ターゲット URI、ヘッダー、(デフォルトで)ボディバイト — に署名し、特定のエージェントがリクエストを発行したことを、リプレイと改ざん保護付きで暗号学的に確立します。有効な署名は 1 つのことだけを証明します: リクエストは、その鍵が署名したエージェントから来た。 そのエージェントがリクエストボディで名指しされたブランドのために行動する認可を持つかは別の関心事で、ターゲットハウスの brand.json のauthorized_operator[] が規定します。このセクションは認証のみを定義します。認可ルックアップは brand.json スキーマが規定し、リクエストが署名されているかに関わらず発生します。
AdCP 3.0 はこのプロファイルを、get_adcp_capabilities の request_signing を介してオプションかつケイパビリティ宣伝として定義します。AdCP 4.0 — 次の破壊的変更蓄積ウィンドウ — は支出コミットオペレーションでそれを要求します。基盤は 3.0 で出荷され、早期採用者が強制前に正準化とプロキシ相互運用のバグを表面化できます。Transport migration timeline を参照。
役割:
- エージェントは、オペレーターの brand.json の
agents[]エントリの自身のjwks_uriで公開した鍵でリクエストに署名します。オペレーター(brand.json をホストするドメイン)は直接購入するハウスでも認可されたサードパーティでもよい — このプロファイルは区別しません。署名者は常にエージェントです。 - セラーは署名を署名エージェントの公開鍵に対して検証し、エージェントアイデンティティを確立します。次にセラーは別個のブランド-オペレーター認可チェック(このプロファイルのスコープ外)を実行します。
- エージェント側 AdCP エンドポイントを呼ぶセラー(例: それ自体が AdCP プロトコル呼び出しであるバイヤーホストの変更コールバック)は、アウトゴーイングリクエストに対称的に署名します。受信エージェントはセラーオペレーターの brand.json の
agents[]エントリで公開されたセラーの鍵に対して検証します。プッシュ通知 Webhook コールバック(push_notification_config.urlや類似の非同期一方向通知)は、このプロファイルの対称 Webhook callbacks バリアントでカバーされます — セラーはadcp_use: "request-signing"鍵でアウトバウンド署名し(非推奨の"webhook-signing"値も受け入れられる)、バイヤーが検証します。
- JWKS ディスカバリー、SSRF ルール、alg 許可リスト、失効セマンティクス、鍵ローテーションを上記の AdCP JWS profile と共有します。リクエスト検証は決して別の鍵目的を受け入れません: リクエスト署名 JWK は
"adcp_use": "request-signing"、"use": "sig"、"key_ops": ["verify"]、および異なるadcp_useを持つ他の JWKS エントリに現れないkidを宣言しなければなりません(MUST)。検証者は 4 つすべてを強制します。Agent key publication を参照。Webhook パスは、Webhooktagがドメイン分離を提供するため独自の明示的な緩和を持ちます。 - ガバナンスの Buyer identity resolution のアイデンティティブートストラップ依存を解決します: リクエスト署名を検証するセラーは暗号学的に確立された署名エージェントアイデンティティを持ち、署名エージェントのオペレータードメインをガバナンス検証ステップの brand.json 解決入力として使ってもよい(MAY)。
request_signing.supported: true を宣伝する任意のエージェントで実行される /compliance/latest/universal/signed-requests のユニバーサルなケイパビリティゲートストーリーボードで採点されます。ストーリーボードは下記の verifier checklist のすべてのステップとこのプロファイルのすべての正準化エッジルールを、/compliance/latest/test-vectors/request-signing/ のテストベクターに対して行使します。自身のエージェントに対して CLI グレーダーを実行するには Auth Graders を参照。
汎用の 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(adcp_use: "request-signing" 鍵で署名)の役割です。この分割は意図的です — 完全な根拠と、正準アーティファクトが証明可能である必要のあるツールの request-the-webhook パターンは Security Model: What gets signed を参照。
指定タスクのペイロードエンベロープレスポンス署名。 閉じたタスクのリストが、そのレスポンスペイロードを adcp_use: "response-signing" の下で暗号学的に署名されるものとして指定します。このプリミティブは RFC 9421 §2.2.9 トランスポートレスポンス署名と、要となる 2 つの軸で異なります:
- 署名の場所: HTTP レスポンスヘッダーではなく、レスポンスボディの中。
- 検証パス: レスポンスボディをパースし、次に JWS をエージェントの
jwks_uriで公開された応答エージェントのresponse-signingJWK に対して検証 — トランスポートヘッダー上の RFC 9421 ベース再構築ではない。
verify_brand_claimとそのバルクバリアントverify_brand_claims(Brand Protocol)。応答するブランドエージェントは、ブランドのadcp_use: "response-signing"鍵の下で JWS エンベロープとしてレスポンスペイロードに署名します。署名は方向非対称の信頼モデルの要です —verify_brand_claimtrust model と Building a brand agent — Signing setup を参照。
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 に一致する 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 の共有 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" 鍵にのみ適用されます。
トランスポートスコープ
読み取り呼び出しは bearer 認証のままです。読み取りトラフィックへの署名は、比例した利益なしに検証コストを追加します。署名の目的は状態変更オペレーションの完全性です。
クイックスタート: 3.0 でリクエスト署名にオプトイン
4.0 のフリップ前に 3.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をカバーしなければならない、してはならない、してもよいかを確認する。 - Ed25519 鍵ペアを生成:
openssl genpkey -algorithm ed25519 -out signing-key.pem。 - 公開鍵を JWK としてエクスポート。
"kid"、"use": "sig"、"key_ops": ["verify"]、"adcp_use": "request-signing"、"alg": "EdDSA"を追加。 - JWK をエージェントの
jwks_uri(brand.json のagents[]エントリで宣言された URL。エージェント URL のオリジンの/.well-known/jwks.jsonにデフォルト)で公開。 - 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 を参照。 /compliance/latest/test-vectors/request-signing/のコンフォーマンスベクター(AdCP バージョンごとに公開。ソースはstatic/compliance/source/test-vectors/request-signing/)でエンドツーエンド検証 — クライアントが正例ベクターのexpected_signature_baseに一致する署名を生成すれば完了。
get_adcp_capabilitiesでrequest_signing.supported: trueを宣伝。パイロット中はrequired_for: []のまま。相手方ごとに段階的にオペレーションを追加。- 変更系ルートで署名検証ミドルウェアを有効化。verifier checklist を実装 — 14 のチェックすべて(13 の番号付きステップとサブステップ 9a)、最初の失敗でショートサーキット。
required_forを設定する前に、パイロット相手方についてシャドウモード(検証してログ。失敗で拒否しない)で開始。最初の数週間は検証失敗をオペレーションではなくモニタリングで表面化。- 検証者に対してコンフォーマンス負例ベクターを実行 — 各拒否はベクターの記載された
error_codeを生成しなければならない(MUST)。ベクターのfailed_stepは情報的です。正しいエラーコードで拒否する実装は、内部ステップ番号が異なってもコンフォーマントです。
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 アクセスポリシーをバインドします(GCProles/cloudkms.signerを特定の cryptoKey にスコープ、AWSkms:Signを鍵 ARN で条件付け)。
@adcp/sdk(TypeScript)は sync/async パリティを持つ SigningProvider インターフェース、テスト用のインメモリプロバイダー、examples/gcp-kms-signing-provider.ts の GCP KMS リファレンスアダプターを出荷します。完全なウォークスルーは SDK signing guide を参照。
トリップワイヤーパターン — 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 のステップ 8 を参照)、リクエストと Webhook の両方を同じ鍵で署名するオペレーターは単一の "request-signing" エントリを公開します。Webhook に別個の鍵素材(影響範囲分離)を望むオペレーターは、別個の kid を持つ 2 つ目の "request-signing" 鍵を公開します — 分離は別個の adcp_use ではなく kid から来ます。adcp_use 値は常に文字列であり配列ではありません — 単一エントリに "adcp_use": ["request-signing","webhook-signing"] を公開することは受信者が拒否するスキーマエラーです:
adcp_use: "request-signing"、別個の kid、Webhook 鍵の侵害がリクエスト署名に及ばないよう Webhook 署名に使用。別個の kid 値はまた、相手方が 2 つの鍵を独立にキャッシュ・ローテーションできることを意味します。
AdCP RFC 9421 プロファイル
このプロファイルは、クロス実装の相互運用が扱いやすくなるよう、RFC 9421 を単一の正準形状に制約します。 カバーされるコンポーネント(すべての署名済みリクエストで REQUIRED):@target-uri 正準化は AdCP URL canonicalization rules に従います — 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 コンフォーマンスセットは、固定入力と期待出力、加えて不正オーソリティ拒否ケースで、アルゴリズムのすべてのルールを行使します。SDK はこのセットをすべてのコミットで実行すべきです(SHOULD)— 署名者間の正準化発散は、そうでなくなるまで黙っており、その後診断が痛い本番相互運用バグになります。
検証者は、カバーされるコンポーネントリストがリクエストタイプに必要なコンポーネントを省略する署名を拒否しなければなりません(MUST)。署名者は調整なしに追加ヘッダーをカバーしてはなりません(MUST NOT)— 余分なコンポーネントは、それらを含めない実装をまたいで署名を黙って無効にします。
署名パラメーター(Signature-Input パラメーター、すべて REQUIRED):
6 つのパラメーターすべてが REQUIRED です。検証者はいずれかが不在なら拒否しなければなりません(MUST、
request_signature_params_incomplete)。
アルゴリズム命名 — JWK vs RFC 9421。 各アルゴリズムの 2 つの名前はソーススペックで異なります。実装はこれらを十分頻繁に混同するので表が必要です:
検証者が
keyid を解決して JWK に "alg": "EdDSA" を見つけたとき、一致する sig-param 値は ed25519 です。実装は、それぞれ独立に許可リストを検証することに加えて、2 つが一致すること(JWK alg がマッピング表で sig-param alg に一致)を検証すべきです。ガバナンスプロファイルからのエッジランタイム根拠が適用されます — ES256 は EdDSA がランタイム設定を要するエッジ向けの代替です。
リクエストごとに 1 署名。 検証者はちょうど 1 つの Signature-Input ラベル(慣習的に sig1)を処理しなければならず(MUST)、リクエストに存在する追加のラベルを無視しなければなりません(MUST)。リレーされたリクエストに再署名する必要のある中間者は、上流ラベルに追加するのではなく置換しなければなりません(MUST)。完全なリレーチェーンセマンティクス(リレーが発信者の署名を保持したい場合)は #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 つの相互運用ハザード — 一部のプロキシが書き換える / と一部のヘッダーパーサーが構造化フィールドパラメーター区切りとして扱う = — を避けます。
検証者の要件:
- 署名者はパディングなし base64url のみを発行しなければなりません(MUST)。
+、/、=を含むSignatureまたはContent-Digest値を発行する署名者は非コンフォーマントです。 - 検証者はパディングなし base64url を受け入れなければなりません(MUST)。検証者はこの明確化より前の相手方との相互運用のため、純粋な標準 base64 トークンも寛容にデコードすべきです(SHOULD、
+→-次に/→_に変換、次に末尾=を除去、次に base64url デコード)。この寛容は AdCP 3.2 で削除予定の互換性の便宜です — それに依拠する署名者はそれまでにパディングなし base64url に移行しなければなりません(MUST)。 - 検証者は、アルファベットを混在させる任意のトークン(同じトークン値内の
[+/=]内の任意の文字 AND[-_]内の任意の文字)をrequest_signature_header_malformedで拒否しなければなりません(MUST)。混在アルファベットトークンは曖昧です:A+B-は「標準 base64 文字を変換」と「base64url デコード」ステップの順序によって異なるバイトにデコードされ得、検証者間で異なるContent-Digestバイトは、攻撃者があるバリデーターが受け入れ別のが拒否するダイジェストミスマッチを仕込むことを許します。 - コンフォーマンスベクターの
expected_signature_baseフィールドはバイナリ値エンコードから独立です — 正準署名ベースバイトを含み、ヘッダーフィールドエンコードではありません。発行されるSignatureトークン自体のみがエンコードされます。
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 オペレーションリストとは別の名前空間で宣言します:
一致する値は 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 を参照)を運ぶ場合、そのピンは権威的です: 検証者は、jwks_uri の内容に関わらず、keyid がピン留めセットにない任意の署名を拒否しなければなりません(MUST)。エージェントホストの JWKS は、パブリッシャーピンが存在するときは常に助言的です。これはエージェントドメイン侵害ウィンドウを閉じます — エージェントのドメインを乗っ取る攻撃者は、パブリッシャーのピンが依然受け入れを規定するため、エンドポイントとその宣伝された鍵の両方を黙ってスワップできません。パブリッシャーは、委任スコープに変更系オペレーションを含む任意のエージェントについてピン留めすることが要求されます。ローテーションとキャッシュセマンティクスは adagents.json ルールを参照。
各リクエスト署名 JWK エントリは宣言しなければなりません(MUST):
クロス目的鍵再利用は禁止され、上記の明示的な 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 鍵
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 が与えられたとき:
- 検証者は署名エージェントの URL をその brand.json の
agents[]エントリに解決します。ディスカバリーは以前のオンボーディングから来てもよく(MAY)、レジストリキャッシュから来てもよい(MAY)が、正準のワイヤー上ブートストラップはエージェントのget_adcp_capabilitiesレスポンスのidentity.brand_json_urlフィールドです — Discovering an agent’s signing keys viabrand_json_urlを参照。 - エージェントの
jwks_uri(またはエージェントのurlのオリジンの/.well-known/jwks.jsonにデフォルト)を Webhook URL validation に従う SSRF 検証付きでフェッチ。JWKS キャッシュ TTL は失効リストポーリング間隔で上限が制限される。 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 だけが与えられたとき、検証者はエージェントの署名鍵を次で解決します:
Aのget_adcp_capabilitiesレスポンスを Webhook URL validation に従う SSRF 検証付きでフェッチ(HTTPS のみ — URLAは呼び出し元が供給し、Webhook コールバックに使う同じアドレスファミリー + プライベート IP フィルタリングを通らなければならない)。到達不能/タイムアウトではrequest_signature_capabilities_unreachableで拒否。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)。- オリジンバインディング。 エージェント URL
Aのホスト eTLD+1 はbrand_json_urlのホスト eTLD+1 と等しくなければならない(MUST)。eTLD+1 計算はピン留めされた日付付き Public Suffix List スナップショットを使わなければならない(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 マルチテナントデプロイ)に向ける共有テナントスプーフィングベクトルを閉じる。 brand_json_urlの brand.json を Webhook URL validation に従う SSRF 検証付きでフェッチ。検証者はこのフェッチでリダイレクトに従ってはならない(MUST NOT、このプロファイルの他所で文書化されたauthoritative_locationの単一リダイレクト切り出しはそのフィールドにスコープされ、brand.json ブートストラップに継承されてはならない)。推奨予算: 接続 5 秒、総デッドライン 10 秒、ボディ上限 256 KiB。成功フェッチのキャッシュ TTL は JWKS 失効ポーリング間隔で上限が制限されなければならない(MUST、鍵ローテーションが古い brand.json でマスクされないように)。負のレスポンス(404、ネットワーク失敗)は 60 秒以上キャッシュされてはならない(MUST NOT)— 誤設定を修正するオペレーターが完全な失効サイクルの間ロックアウトされてはならない。urlがAとバイト等価なagents[]エントリを見つける(このステップで正準化なし — ガバナンス JWS のiss-to-brand.json 一致と同じルール、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で拒否。- 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にデフォルト。
- セルサイド Webhook 配信のみ — すなわち、セラーがメディアバイ配信についてバイヤーへのアウトバウンド Webhook に署名: パブリッシャーの
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で拒否。- JWKS をフェッチ、
kidを見つけ、既存の RFC 9421 プロファイル(verifier checklist のステップ 7 以降)に従って検証。
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)— 構造化形状は検証者制御でも、攻撃者が影響を与えられる文字列。
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 を要するものはない。
クイックスタート: brand_json_url ベースの検証者を実装
上記の request-signing quickstart をミラーします。エージェントごとに一度実行 — 結果の agents[] エントリ、jwks_uri、JWKS はステップ 4 の TTL ルールに従ってキャッシュされます。
- 署名エージェントの URL
Aのケイパビリティをフェッチ。これはプロトコルレベル呼び出し —Aに対する生の HTTPGETではなく、エージェントの宣言されたトランスポート(MCPtools/callまたは A2A スキル呼び出し)を介してget_adcp_capabilitiesを呼び出す。エージェント URL は JSON ケイパビリティドキュメントではなくプロトコルエンドポイント。Webhook URL validation に従う SSRF セーフトランスポートを使う: HTTPS のみ、アドレスファミリー + プライベート IP フィルタリング、リダイレクトなし、予算{ connect: 5000, total: 10000, body: MAX_CAPABILITIES_BYTES, maxRedirects: 0 }。 identity.brand_json_urlを読む。 不在(かつリクエストが署名済み)または非 HTTPS ならrequest_signature_brand_json_url_missingで拒否。- eTLD+1 オリジンバインディング。 ピン留め PSL スナップショットを使って
eTLD+1(A)とeTLD+1(brand_json_url)を計算。ベンダー済みの日付付きスナップショットでtldts(TS)、publicsuffixlist(Python)、または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.COMvsexample.com、または U-label vs A-label)は正当なトラフィックを黙って拒否する。 - 同じ SSRF ルール + リダイレクトなし、ボディ上限
MAX_BRAND_JSON_BYTES、接続 5 秒、総 10 秒でbrand.jsonをフェッチ。重複キーを拒否する strict JSON パーサー(例: TS のsecure-json-parse、重複で raise するobject_pairs_hook付きの Python stdlibjson.JSONDecoder、Go の重複キーチェックと組み合わせたencoding/jsonDecoder.DisallowUnknownFields)でパース — 重複キーはステップ 14 がリクエスト面で閉じるパーサー差分ベクトルで、同じトラストルートドキュメントが検証者間で 2 つの異なる形状にパースされてはならない(MUST NOT)。重複キー検出でrequest_signature_brand_json_malformedで拒否。成功レスポンスを JWKS 失効ポーリング間隔まで(それより長くない)キャッシュ。失敗は最大 60 秒キャッシュ。 urlがAとバイト等価なagents[]エントリを見つける(正準化なし)。ミスでrequest_signature_agent_not_in_brand_json、複数一致でrequest_signature_brand_json_ambiguousを拒否。- 一致したエントリから
jwks_uriを解決 — セルサイド Webhook 配信のみ、オペレーターのjwks_uriよりパブリッシャーのadagents.json signing_keysピン(存在する場合)を優先。他のすべての(面、役割)タプルは、一致したエントリのjwks_uri(デフォルト:Aのオリジンの/.well-known/jwks.json)を使う。 - 一貫性チェック。 ケイパビリティ
identity.key_originsの下で宣言されたすべての面/目的について、解決されたjwks_uriホストと宣言オリジンの両方にcanonicalizeOrigin()(ASCII 小文字 + IDNA-2008 A-label)を適用し、バイト比較(パブリッシャーピン由来の特定の(エージェント、面/役割)タプルのみスキップ)。適宜request_signature_key_origin_mismatch/_missingを拒否。 - verifier checklist のステップ 8 以降にハンドオフ — JWKS をフェッチ(同じバイト予算
MAX_JWKS_BYTESと 5/10 秒の接続/総デッドライン)、kidを見つけ(ここでステップ 7 のプリアンブルですでに解決済み — 検証者チェックリストのステップ 7 はディスカバリープリアンブル自体)、RFC 9421 に従って検証。
/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):resolveAgent(url)は{ agentUrl, brandJsonUrl, agentEntry, jwksUri, jwks, identityPosture, consistency, freshness, trace }を返す。getAgentJwks(url)は JWKS のみの高速パス。createAgentJwksSet(url, opts)はjoseのjwtVerifyに渡すJWTVerifyGetKeyを返す。 - Python(
adcp):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 を 1 呼び出しで実行するワンショットヘルパー。 - Go(
adcp-go):ResolveAgent(ctx, agentURL) (*AgentResolution, error)はAgentURL,BrandJSONURL,AgentEntry,JWKSUri,JWKS,IdentityPosture,Consistency,Freshness,Traceフィールドを持つ構造体を返す。VerifyRequestSignature(ctx, req, opts) (*VerifiedIdentity, error)は TS/Python のワンショットをミラー。
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 のステップ 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ケイパビリティにあり、ANDSignature-Inputヘッダーが存在せず、AND 呼び出し元がこのオペレーションについて検証者が受け入れる他のクレデンシャル(bearer、API キー、mTLS)を提示しない場合、request_signature_requiredで拒否。このブランチに入る未署名リクエストは決してチェックリストに入りません。未署名だが他の方法で認証された呼び出し元を規定するルールは 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)— 存在するが壊れた署名は署名者の意図を示します。黙ったフォールバックはダウングレード攻撃を可能にします。
-
Signature-InputとSignatureヘッダーを RFC 9421 §4 に従ってパース。不正なら拒否。 -
created,expires,nonce,keyid,alg,tagのいずれかがSignature-Inputパラメーターから不在なら拒否(request_signature_params_incomplete)。 -
tagが正確にadcp/request-signing/v1でないなら拒否(request_signature_tag_invalid)。 -
algが許可リスト(ed25519,ecdsa-p256-sha256)にないなら拒否。ライブラリのデフォルトに頼ってはならない(request_signature_alg_not_allowed)。 -
expires ≤ created、created > now + 60 s、expires < now − 60 s、またはexpires − created > 300 sなら拒否(request_signature_window_invalid)。 -
カバーされるコンポーネントが
@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で拒否。 -
keyidを Agent key publication を介して JWK に解決。検証者が署名エージェントのキャッシュされたエージェント → JWKS マッピングを持たない場合、このステップの前に Discovering an agent’s signing keys viabrand_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[]エントリに解決できないなら拒否。 -
JWK の
useが"sig"、key_opsが"verify"を含み、adcp_useが"request-signing"に等しいことを検証。任意の不一致(不在のadcp_useを含む。非コンフォーマントとして扱わなければならない)で拒否(request_signature_key_purpose_invalid)。 -
Transport revocation リストを確認。
keyid∈revoked_kidsなら拒否(request_signature_key_revoked)。検証者が grace 内に失効リストをリフレッシュしていないならrequest_signature_revocation_staleで拒否。 9a. keyid ごとの上限チェック。 keyid ごとのリプレイキャッシュ上限を確認。このkeyidについて上限に達しているならrequest_signature_rate_abuseで拒否。暗号検証(ステップ 10)の前に実行 — ステップ 9 と同じ根拠: 上限を枯渇させる侵害されたまたは誤設定された署名者が、増幅された Ed25519/ECDSA 作業を検証者に強制してはならない(MUST NOT)。keyid解決(ステップ 7)の後に実行し、上限状態オラクルが検証者がすでに認識をコミットした鍵についてのみ応答するように — 9a を早く実行すると、攻撃者が JWKS に公開されていない keyid を含む全 keyid 空間で検証者内部のレート制限状態を探れる。 -
上記のプロファイルに従い
@target-uri正準化 AND@authority導出を適用した後、カバーされるコンポーネントを使って RFC 9421 §2.5 に従い正準署名ベースを計算。@authorityルールは要: 検証者は、存在する場合 HTTP/2+ の:authority疑似ヘッダーから、そうでなければ受信した HTTP/1.1Hostヘッダーから@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)。 -
content-digestがカバーされている場合、受信したボディバイトからダイジェストを再計算して比較(ミスマッチでrequest_signature_digest_mismatch)。 -
リプレイキャッシュに対してノンスを確認(Transport replay dedup を参照)。
(keyid, nonce)がリプレイキャッシュ TTL 内で見られている場合拒否(request_signature_replayed)。 -
ステップ 1-9、9a、10-12 がすべて通過した後にのみ、
(keyid, nonce)を TTL =(expires − now) + 60 s(+60 秒はステップ 5 で適用したスキュー許容に一致)でリプレイキャッシュに挿入。この挿入はステップ 14 のボディ整形式チェックの前に発生しなければならない(MUST)。不正なボディ上に有効な署名を運ぶキャプチャされたフレームが、各リトライで暗号検証 CPU を燃やすためにリプレイできないように — ノンスは、ボディ形状に関わらず、暗号学的に有効なフレームの最初の目撃で燃やされる。 -
ボディ整形式性。 検証者は重複オブジェクトキーを含むボディを拒否しなければならない(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_key自体が重複する(異なるパーサーが異なるキーを見る)リクエストボディはここで拒否され、決してキャッシュに到達しない。別個の冪等性層監査は不要。 14a. Strict-parse 要件。 チェックは重複キーを露出するパーサーを使わなければならない(MUST)— それらを黙って破棄する last-wins/first-wins のデフォルトはこの要件を満たさない。Webhook 検証者チェックリストのステップ 14aの言語ごとの strict-parse エスケープハッチ列挙がここに同一に適用される。 14b. ロギング規律。 検証者はrequest_body_malformed拒否で完全なリクエストボディバイトをログすべきではない(SHOULD NOT)。keyid、ノンス、バイト長、特定の重複キー名のみをログ。Webhook 検証者チェックリストのステップ 14bのキー名サニタイズルール(最初の非印字文字で<sanitized:N>に切り詰め、32 バイト以下の最後の UTF-8 コードポイントに切り詰め、数を 4 で上限)がここに同一に適用される — 攻撃者制御バイトチャネルはリクエスト面で同じ形状を持つ。
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 を参照)と意図的にペアにします — 侵害鍵イベントは、それを引き起こした攻撃者にも読めても、オペレーターにとって大きくあるべきです。
上限の要となる不変条件。 秘密鍵なしの外部トラフィックは上限を増やせません: リプレイキャッシュ挿入はステップ 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 に入り、オペレーションが
required_forにあるかに関わらず暗号学的メリットで評価されます。 - 不正な署名は、チェックリストプリアンブルの不正署名ルールに従い、フォールバックをとにかくブロックします。壊れた署名は署名者の意図を示し、bearer に黙ってダウングレードしてはなりません(MUST NOT)。
warn_for はこのルールで変わりません: 未署名リクエストについてすでに非拒否で、ロールアウト中の署名済みだが無効な署名をモニタリングシグナルとして表面化し続けます。
相手方のケイパビリティ面で 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 をカバー。ボディ保持を妨げるトランスポートを、対応する制約ではなく修正すべきバグとして扱う。
レガシーインフラのため本当にボディバイトを保持できない検証者は 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 のステップ 12 は(keyid, nonce) ごとの重複排除を要求します。無制限のセットはメモリと DoS リスクです。
- 各エントリの TTL = ウィンドウ検証で適用した対称クロックスキュー許容に一致する
(expires − now) + 60 s。典型的な TTL ≤ 360 秒(5 分 + 60 秒スキュー)。 - TTL 退避付きの
(keyid, nonce)でキーされたインメモリ LRU、期待リクエストレート × 最大署名有効性でサイズ。 - 署名者ごとに約 10K req/秒を超える場合:
EX = remaining_validity_seconds + 60の RedisSETNX。 - 分散検証者(マルチリージョン): リージョンごとのリプレイキャッシュは許容。これが可能にする唯一の攻撃はリージョンをまたぐ
(expires − now + 60 s)内の単一リプレイで、約 6 分に制限され、攻撃者が中間ルーティングを制御する場合のみ有効。
(keyid, nonce) 値をリプレイキーとして使ってはなりません(MUST NOT)— それらは正当なエージェントトラフィックを拒否する偽陽性を生みます。
keyid ごとの上限。 濫用的または侵害された署名者が一意のノンスで検証者メモリを枯渇させるのを防ぐため、検証者はリプレイキャッシュに keyid ごとのエントリ上限を強制しなければなりません(MUST)。推奨上限: keyid ごとに 1,000,000 エントリ。上限超過で、検証者はその keyid からの新しい署名を request_signature_rate_abuse で拒否しなければならず(MUST)— 黙って退避してはならず — オペレーターにアラートすべきです(SHOULD)。上限に達することは侵害された鍵または著しく誤設定された署名者を示すからです。黙った退避が危険なモードです: まさに検証者が攻撃下にあるときにリプレイウィンドウを作ります。keyid ごとの上限は総キャッシュ上限とは別: 検証者は多くの行儀の良い署名者を介して正当に総上限に達し得ますが、keyid ごとの枯渇は明白に攻撃シグナルです。上限チェックは verifier checklist のステップ 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 を参照)。リクエスト署名鍵について:
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 ブロックを介して署名サポートと呼び出しごとの要件を宣伝します:
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 が規定 — 特に、有効なフォールバッククレデンシャルを提示する未署名リクエストは受け入れられ、署名を無条件にしたいセラーはそのオペレーションで他のクレデンシャルタイプを拒否するようフォールバック認証器を設定しなければならない(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のスーパーセット。
- 署名準備を発表: オペレーションを
supported_forに追加。相手方は署名を開始できるが、しなくても何も変わらない。 - シャドウモードに昇格: オペレーションを
warn_forに移す。検証者は検証失敗をログ。トラフィックは影響なし。オペレーターは失敗率を監視してデバッグ。 - 強制: 失敗率がオペレーターの閾値を下回ったら
required_forに移す。そのオペレーションへの未署名または無効署名リクエストは今拒否される。
required_for: [] で出荷し、選択的に設定します。warn_for は強制に切り替える前の推奨プレプロダクション停止です。4.0 ではプロトコルが規範的に required_for が検証者がサポートするすべての支出コミットオペレーションを含むことを要求し、それらのオペレーションに covers_content_digest: "required" が推奨されます。
トランスポートエラータクソノミー
401 でWWW-Authenticate: Signature error="<code>" に返され、SDK 検証者が型付きエラーとして表面化する安定コード。命名パターンは governance taxonomy に一致し、SDK エラー処理が対称になります。
サーバーは安定コードを超えて内部検証詳細をエコーしてはなりません(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 で説明された非推奨 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 ユースケースで統合するのに安全でありません。
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):
鍵分離は別個の
kid を介してオプション — 別個の目的ではない。 Webhook トラフィックを別個の鍵素材で署名させたい(Webhook 鍵侵害がリクエスト署名に及ばないように、または 2 つを独立にローテーションするように)オペレーターは、別個の kid を持つ 2 つ目の adcp_use: "request-signing" 鍵を公開し、それで Webhook に署名します。両方の鍵は同じ adcp_use を運びます。検証者は Signature-Input の kid で正しいものを解決します。分離を達成するのに専用の Webhook 鍵目的は不要です。
非推奨:Webhook を検証するバイヤーは、adcp_use: "webhook-signing"は非推奨で将来のメジャーバージョンで削除予定(#5555 で追跡。正確なウィンドウは WG/RFC 決定)。検証者は後方互換性のためそれを依然受け入れなければならない(MUST、"webhook-signing"鍵の下で署名された Webhook はクリーンに検証される)が、新しい署名者は"request-signing"鍵のみで公開・署名すべき(SHOULD)。
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 つの上書き付きでリクエスト署名と同一:
JWKS ディスカバリー。 バイヤーはすでに使っている AdCP 統合からセラーのエージェント URL を知っています。バイヤーは解決します:
- セラーエージェント URL
A→Aのオペレータードメインの/.well-known/brand.jsonを Webhook URL validation に従う SSRF 検証付きでフェッチ。brand.json 解決は 1 リダイレクト(authoritative_locationまたはhouseリダイレクトバリアント)に従って停止。 - フェッチした brand.json で、
urlがAとバイト単位で一致するagents[]エントリを見つける。 - そのエントリの
jwks_uri(またはAのオリジンの/.well-known/jwks.jsonにデフォルト)を SSRF 検証付きでフェッチ。JWKS キャッシュ TTL は失効リストポーリング間隔(フロア 1 分、上限 30 分)で上限が制限される。長時間実行のタスクフローは JWKS ローテーションをまたぐ。検証者はタスクの寿命の間単一の JWKS スナップショットをピン留めしてはならない(MUST NOT)。 - インカミング
Signature-Inputのkeyidをフェッチしたセットの JWK に解決。kidミスでは、webhook_signature_key_unknownで拒否する前に 1 回再フェッチ(再フェッチ間の 30 秒クールダウンに従う)。ミス時再フェッチパスはタスク中の鍵ローテーションを扱う要のメカニズム — それをスキップするクライアントは正当なローテーション後配信を拒否する。
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 に従って)9421 署名されることを要求しなければならず(MUST)、request_signature_required(required_forオペレーションに使うのと同じコード — Transport error taxonomy を参照)で拒否します。署名済みリクエストがボディに暗号学的にコミットするとき、authenticationブロックは署名も無効化せずに注入または除去できません。リクエスト署名を全くサポートしないセラーはこのルールを強制する方法がなく、前の項目の log-and-alarm 姿勢にフォールバックします — 3.0 移行注記であり免除ではない: request-signing migration timeline は 4.0 で支出コミットオペレーションにリクエスト署名を必須にし、その時点で未署名のみのセラーはなくなります。 - バイヤーは、
authentication.credentialsで登録した後に 9421 署名済み Webhook を受け取ったとき、またはauthenticationなしで登録した後に HMAC 署名済み Webhook を受け取ったとき、黙ってダウングレードするのではなくwebhook_mode_mismatchで拒否してアラームしなければなりません(MUST)。 拒否が安全特性です。アラームはテレメトリ — アラームするがペイロードを受け入れるバイヤーは、すでにミスマッチした署名スキームに権限を渡しています。拒否は安定エラーコード付きの HTTP401として表面化し、送信者側のリトライロジックが同一にリプレイするのではなくインシデントレスポンスにルーティングできます。 - バイヤーは、9421 をまだ実装していないセラーと相互運用するとき、オンボーディングで HMAC モードを帯域外で交渉すべきです(SHOULD)。 オペレーターレコードでの耐久性のある相手方ごとのモード選択は、リクエストごとのフィールドのように MITM 変更可能ではありません。
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 つのプロファイルを区別します。
-
Signature-InputとSignatureヘッダーを RFC 9421 §4 に従ってパース。不正なら拒否(webhook_signature_header_malformed)。SignatureまたはSignature-Inputが他方なしに存在する場合、同じコードで拒否 — 推測可能でなくバインドされたペア。 -
created,expires,nonce,keyid,alg,tagのいずれかがSignature-Inputパラメーターから不在なら拒否(webhook_signature_params_incomplete)。 -
tagが正確にadcp/webhook-signing/v1でないなら拒否(webhook_signature_tag_invalid)。バイト単位一致、ケースフォールディングなし。 -
algが許可リスト(ed25519,ecdsa-p256-sha256)にないなら拒否。ライブラリのデフォルトに頼ってはならない(webhook_signature_alg_not_allowed)。 -
expires ≤ created、created > now + 60 s、expires < now − 60 s、またはexpires − created > 300 sなら拒否(webhook_signature_window_invalid)。 -
カバーされるコンポーネントが
@method,@target-uri,@authority,content-type,content-digestのすべてを含まないなら拒否(webhook_signature_components_incomplete)。content-digestは REQUIRED。ポリシーブランチはない。 -
上記の JWKS ディスカバリーステップで
keyidを JWK に解決。kidミスでは、拒否(webhook_signature_key_unknown)前に 1 回再フェッチ(再フェッチ間 30 秒クールダウン)。keyidが署名者の brand.json の特定のagents[]エントリに解決できないなら拒否。 -
JWK の
useが"sig"、key_opsが"verify"を含み、adcp_useが"request-signing"であることを検証 — Webhook はエージェントのリクエスト署名鍵で署名される(Key publication を参照)。非推奨の"webhook-signing"値も後方互換性のため受け入れなければならない(MUST)。他の任意の結果でwebhook_signature_key_purpose_invalidで拒否: 不在のadcp_use、欠落したverifykey_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 を参照 — で鍵目的失敗には使われません。) -
Transport revocation リスト(署名目的をまたいで再利用)を確認。
keyid ∈ revoked_kidsなら拒否(webhook_signature_key_revoked)。検証者が grace 内にリフレッシュしていないならwebhook_signature_revocation_staleで拒否。 9a. keyid ごとの上限チェック。 Webhook リプレイキャッシュ上限を確認。超過ならwebhook_signature_rate_abuseで拒否。リクエスト署名と同じ安価な拒否の根拠で、暗号検証(ステップ 10)の前に実行。 -
リクエスト署名プロファイルに従い
@target-uri正準化 AND@authority導出を適用した後、カバーされるコンポーネントを使って RFC 9421 §2.5 に従い正準署名ベースを計算。@authorityルールは Webhook セキュリティの要: 検証者は、存在する場合 HTTP/2+ の:authority疑似ヘッダーから、そうでなければ受信した HTTP/1.1Hostヘッダーから@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)。 -
受信したボディバイトから
content-digestを再計算して比較(ミスマッチでwebhook_signature_digest_mismatch)。REQUIRED — ポリシーブランチなし。 -
リプレイキャッシュに対してノンスを確認。
(keyid, nonce)がリプレイキャッシュ TTL 内で見られている場合拒否(webhook_signature_replayed)。 -
ステップ 1-12 がすべて通過した後にのみ、
(keyid, nonce)を TTL =(expires − now) + 60 sでリプレイキャッシュに挿入。この挿入はステップ 14 のボディ整形式チェックの前に発生しなければならない(MUST)。不正なボディ上に有効な署名を運ぶキャプチャされたフレームが各リトライで暗号検証 CPU を燃やすためにリプレイできないように — ノンスはボディ形状に関わらず暗号学的に有効なフレームの最初の目撃で燃やされる。この順序が保持する要となる上限不変条件はステップ 14b の後に文書化。 -
ボディ整形式性。 検証者は重複オブジェクトキーを含むボディを拒否しなければならない(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)を使う。
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 の衝突キーを知る診断価値はほぼゼロ。
- Python: stdlib
idempotency_key のアプリケーション層重複排除は、重複した副作用から保護するため署名検証(ステップ 13)の後に実行されます。
Webhook ごとに 1 署名。 検証者はちょうど 1 つの Signature-Input ラベルを処理し、追加のラベルを無視しなければなりません(MUST)。
Webhook リプレイ重複排除のサイジング
Webhook のリプレイ重複排除は 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 任意のサイズのオペレーターに自動スケール(小さな検証者は低い比例フロアを得、エンタープライズ検証者は比例的に大きいものを得る)。
threshold_tuning_overdueイベントをログまたはアラームすべきです(SHOULD)。これはオペレーター調整義務に、オペレーターの勤勉さだけに頼るのではなくテスト可能・監査可能なフックを与えます。 アラームペイロードは、オペレーターのトリアージが正しい脅威形状に応答できるよう、どの節(a、b、c、d)がトリップしたかを名指さなければなりません(MUST)。ここでのアラームは、集約上限がトリガーする前にスローバーン分散侵害パターンを捕捉します — 集約上限でwebhook_signature_rate_abuseが発火すると、キャッシュはすでに満杯で、すべての正当な署名者が拒否されています。アラームは自動失効ではなくインシデントレスポンスにルーティングすべきです(SHOULD): 「攻撃」と「新しいセラーのバッチをオンボーディング」の区別シグナルはオペレーターコンテキストで、マシン導出可能ではなく、アラームでの自動失効は DoS ベクトルを作ります(正当な新規署名者オンボーディングを駆動する任意の当事者がアラームをトリップして大量失効を引き起こせる)。
- ある署名者が到達できるすべてのエンドポイントにわたって単一の論理リプレイキャッシュを共有(Redis / 共有重複排除サービス — プロセスごとインメモリではない)。エンドポイント A が挿入した
(keyid, nonce)がステップ 12 実行前にエンドポイント B に見えるように。または - 正準宛先 URL をリプレイキーに含める、重複排除を
(keyid, canonical destination URL, nonce)にスコープ。正準形は リクエスト署名プロファイルに従う正規化後の@target-uri(スキーム小文字、ホスト IDNA 正規化、デフォルトポート省略、フラグメント除去)。
(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 の他のすべてのルールがそのまま適用されます: 単一プロセス検証者のインメモリ LRU、高ボリュームでの Redis SETNX、分散デプロイのステップ 13 でのアトミック挿入-上限チェック。
Webhook の失効とローテーション
署名者はリクエスト署名に使うのと同じ結合失効リストを介して失効を公開しなければなりません(MUST)— Transport revocation を参照。オペレーターオリジンごとの単一リストがガバナンス署名、リクエスト署名、Webhook 署名鍵をカバーします。 HMAC→9421 移行。 HMAC から 9421 に移行するバイヤーは、セラーが切り替えを確認したら HMAC 検証者を無効化しなければなりません(MUST)。両検証者を同時に実行することは、HMAC パスを元の 5 分リプレイウィンドウ + バイヤーが切るのを忘れた時間の分、悪用可能なままにします。「念のため」の運用姿勢は非推奨パスを意図された非推奨を過ぎてライブに保ちます。セラーは以前に 9421 に移行された相手方からのauthentication ブロックを拒否し、拒否をログすべきです(SHOULD)。切り替えウィンドウ中、バイヤーは両検証者を実行してもよい(MAY)が、どちらのスキームの下でも同じ論理イベントが同じ (sender identity, idempotency_key) タプルにマップされるよう単一の重複排除キースペースを維持すべきです(SHOULD)— 混在モード配信下の重複排除スコープは Reliability セクションを参照。
Webhook エラータクソノミー
コードは request-signing error taxonomy と並行し、SDK エラー処理が 2 つのプロファイルを区別するようwebhook_ プレフィックス付き。バイヤーはこれらのいずれでもセラーに 401 を返してもよい(MAY)。セラーのリトライループは同じ署名バイトでリプレイするので、この表のすべてのコードは送信者にリトライ不可 — 署名失敗、オーソリティミスマッチ、モードミスマッチはすべてリトライで同一の出力を生む — HTTP セマンティクスがリトライを許可しても。
検証失敗のリトライセマンティクス。 少なくとも 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 移行タイムライン
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 署名(RFC 9421)—
request-signing鍵を使用。レガシーadcp_use: "webhook-signing"値は非推奨(依然受け入れ、削除保留 — 非推奨注記のフォローアップイシューを参照) - 指定タスクレスポンスペイロード JWS —
adcp_use: "response-signing"(上記の Designated-task payload-envelope 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 変更はロードマップに蓄積されます。
3.x で署名を出荷する実装は、実トラフィックに対してエンドツーエンドパスを検証するため、4.0 の前に検証者側
required_for を選択的に(相手方ごとパイロット、その後より広いロールアウト)有効化すべきです(SHOULD)— これがエコシステム全体の破壊なしに 4.0 移行を実現可能にするものです。
リクエスト検証者リファレンス(TypeScript)
説明目的のみ。verify9421 と parseSignatureInput コールバックはプロトコル固有の正準化と署名検証をカプセル化します。実装は /compliance/latest/test-vectors/request-signing/ の AdCP コンフォーマンステストベクターに対して検証された特定の RFC 9421 ライブラリをピン留めすべきです。
予算検証
コミット前に予算を検証します:トランスポートセキュリティ
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、nginxssl_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 チェーンへのピン留め(中間ピン)は許容。特定のリーフ証明書へのピン留めは非推奨。
インバウンドサーバー側ヘッダー
includeSubDomains を設定しなければなりません(MUST)。支出コミット AdCP エンドポイントを提供するドメインは HSTS プリロードリストに提出すべきです(SHOULD)。
クライアント / アウトバウンド TLS ハードニング
アウトバウンドフェッチのコードパス(ガバナンス JWKS、brand.json、失効リスト、Webhook 配信、アグリゲータープロキシ)は次をしなければなりません(MUST):- ホストごとの固定上限と全体の固定上限を持つ接続プールを使う。無制限のプールはリソース枯渇面。
- TLS ハンドシェイク時間をデフォルトで 10 秒、総リクエスト時間を 30 秒で上限 — 相手方が供給する URL はさもなくばタールピット DoS ベクトル。
- 接続を SSRF 制御を通過した IP アドレスにピン留め — SSRF チェックと実際の接続の間の DNS 再解決が TOCTOU バイパスが着地する方法。
- セキュリティ機微なフェッチでリダイレクトを拒否。JWKS、brand.json、失効リスト、Webhook コールバックのフェッチはリダイレクトに従ってはならず(MUST NOT)、brand.json 解決ルールはすでに「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 が認証メカニズムのとき:- クライアント証明書 SAN / Subject は、
adagents.jsonまたはbrand.jsonで宣言されたバイヤーの登録済みドメインに一致しなければなりません(MUST)。任意のヘッダーフィールド(X-Forwarded-Client-Cert,X-Client-DNなど)に頼ることは明示的に禁止されています — ヘッダーフィールドは誤設定プロキシをまたいで注入され得ます。 - 終端エッジ(ロードバランサー、メッシュサイドカー)は、検証済み証明書アイデンティティを、サーバーが認証できるクラスタ内チャネルで AdCP サーバーに転送しなければなりません(MUST)。未認証のサイドカーヘッダーはバイパス — mTLS をエンドツーエンドでデプロイするか、クラスタ内チャネルをピン留めします。
- クライアント証明書はオペレーターが運用する CRL または OCSP レスポンダーに対してチェックされなければなりません(MUST)。「私たちが発行した」は「まだ有効」と同じではありません。
プライベートネットワークとメタデータ保護
このセクションのトランスポート制御は、相手方が供給する URL の SSRF 制御を代替しません。相手方 URL へのすべてのアウトバウンドフェッチは SSRF ルールを適用しなければなりません(MUST)— 非 HTTPS を拒否、予約範囲(クラウドメタデータアドレスを含む)の IP を拒否、リダイレクトを拒否、サイズと時間に上限。URL が169.254.169.254 を指すなら TLS は無用です。
このセクションが置き換えないもの
トランスポートセキュリティは天井ではなくフロアです。完璧な TLS スタックでも次を置き換えません:- アプリケーション層のボディ完全性(リクエスト署名と Webhook コールバック)— TLS はワイヤーを保護し、侵害された中間者後のペイロードは保護しません。
- ガバナンス証明(署名付きガバナンスコンテキスト)— TLS は、バイヤーのガバナンスエージェントがこの支出を認可したかをセラーに伝えません。
- 冪等性(Request Safety)— TLS は、送信者がネットワークタイムアウト後にリトライするのを防ぎません。
入力検証
リクエスト検証
すべてのユーザー提供入力を検証します:SQL インジェクション防止
常にパラメーター化クエリを使います:監査ログ
必須ログイベント
すべてのセキュリティ関連イベントをログします:ログ保持
- セキュリティログ: 最低 90 日(365 日推奨)
- 金融ログ: 7 年(コンプライアンス要件)
- アクセスログ: 最低 30 日
セキュリティチェックリスト
パブリッシャー(AdCP サーバー)向け
- 強力な認証を実装(OAuth 2.0、API キー、または mTLS)
- すべてのデータベースクエリでエージェントとアカウントの分離を強制
- 金融オペレーションに冪等性を実装
- 厳格なスキーマ検証ですべての入力を検証
- すべての通信に TLS 1.3+ を使用
- Webhook 署名を暗号学的に検証
- すべてのセキュリティイベントを改ざん不可能にログ
バイヤーエージェント(AdCP クライアント)向け
- 認証情報をセキュアな鍵管理システムに保管
- 認証情報を 90 日ごとにローテーション
- すべての AdCP 通信に HTTPS を使用
- パブリッシャーからのレスポンスを検証
- 異常な支出パターンのアラートを実装
オーケストレーター(マルチエージェント、マルチアカウント)向け
- 各エージェントの認証情報を別々に保管(暗号化)
- すべてのクエリでエージェントとアカウントのフィルタリングを強制
- データベースで行レベルセキュリティを使用
- すべてのオペレーションをエージェントとアカウントのアイデンティティ付きでログ
- エージェントごとのレート制限を実装
次のステップ
- Security Model: このリファレンスが実装する脅威モデルと 5 層防御の物語は Security Model を参照
- Webhooks: Webhook セキュリティパターンは Webhooks を参照
- Error Handling: 認証エラーは Error Handling を参照
- Orchestrator Design: マルチテナントセキュリティは Orchestrator Design を参照