Skip to main content
AdCP はいくつかの場所で URL を識別子として比較します: リクエスト署名プロファイルの @target-uriadagents.jsonauthorized_agents[].url エントリ、TMP AvailablePackageseller_agent.agent_urlformat-idProviderEntryagent_url、その他 URL がプライマリキーである任意のレジストリ。単一の正準化アルゴリズムがこれらすべてを統治するため、どのサーフェスがルックアップをしていても、バイト単位で異なるが意味的に等しい 2 つの URL は等しく比較されます。このページはそのアルゴリズムの権威ある拠点です。リクエスト署名プロファイル はそれを引用し、トランスポート固有の拡張を追加します。

アルゴリズム

正準化は、この順序で RFC 3986 §6.2.2(構文ベースの正規化)と §6.2.3(スキームベースの正規化)を適用します。実装はすべてのステップを適用し、結果をバイト単位で比較しなければなりません(MUST)。
  1. スキームを小文字化HTTPShttps)。スキーム自体は保持されます — httphttps は異なる形式に正準化され、識別子比較で一致してはなりません(MUST NOT)。
  2. ホストを小文字化。 IDN ラベルについては、UTS-46 Nontransitional processing(CheckHyphens=trueCheckBidi=trueUseSTD3ASCIIRules=trueTransitional_Processing=false を使って Punycode A-label(ACE 形式)に変換します(bücher.examplexn--bcher-kva.example)。処理モードのピン留めが重要です: ToASCII の前に非 ASCII 入力を ASCII 小文字化すると、UTS-46 正しい処理とは異なる A-label が生成され、TypeScript(url.domainToASCII)、Go(golang.org/x/net/idna)、Python(idna パッケージ — IDNA2003 である str.encode('idna') ではない)はモードのデフォルトで正当に分岐します。生成者によって ToASCII 正規化されていない生の非 ASCII バイトを含むホストは、比較者によって拒否されなければなりません(MUST) — 受信者は黙って再正規化しません。IPv6 リテラルについては、[] ブラケットを保持し、その内部の 16 進数を小文字化します([2001:DB8::1][2001:db8::1])。IPv6 ゾーン識別子(RFC 6874)は拒否されなければなりません(MUST) — ゾーン ID はノードローカルで、生成ホストの外では意味を持ちません。実装は [...] 内に %25 を含む任意の URL を拒否しなければなりません(MUST)。
  3. userinfo を除去。 user:pass@hosthost。次の authority 形状は不正な形式で拒否されなければなりません(MUST) — 生成者はそれらを発してはならず(MUST NOT)、比較者はそれらを拒否しなければなりません(MUST):
    • userinfo だがホストなし: https://user@/p
    • ホストがまったくない: https:///phttps://:443/p
    • 閉じブラケットが欠けたブラケット付きホスト: https://[::1/p
    • ブラケット外の素の IPv6 アドレス: https://fe80::1/p
  4. デフォルトポートを除去。 https には :443、http には :80。他のすべてのポートを保持(:8443)。
  5. パスに remove_dot_segments(RFC 3986 §5.2.4)を適用するが、連続するスラッシュはバイト単位で保持。 /a//b/a//b のままでなければなりません(MUST) — RFC 3986 はそれらを折り畳むことを義務付けず、保持することでパス混同攻撃サーフェスを閉じます: 一方が /admin//foo/admin/foo を折り畳み、他方が /admin//foo を異なる(潜在的により無防備な)ハンドラーにディスパッチする場合、攻撃者は 1 つの URL に署名または認可し、別のものを実行できます。URL ベースの認可をデプロイするサーバーは、影響を受けるルートでスラッシュ折り畳みを無効にしなければなりません(MUST)(nginx: merge_slashes off;、Express: 事前正規化しない、Go 1.22+ http.ServeMux: 受信パスを保持する明示的な http.Handler を使う)。パスが空でありかつ authority が存在する場合、/ を代入します(RFC 3986 §6.2.3。https://host?x=1https://host/?x=1)。
  6. パーセントエンコーディングを正規化。 16 進数を大文字化(%2f%2F)。パーセントエンコードされた unreserved 文字をデコード(RFC 3986 §2.3 に従い ALPHA / DIGIT / "-" / "." / "_" / "~" なので %7E~%2Dfoo-foo%41A)。reserved 文字はパーセントエンコードされたままにする(%3A%3A のまま、%2F%2F のまま)。パーセントエンコーディング正規化はパスとクエリに適用されます。ゾーン識別子はステップ 2 で拒否されるのでこのステップに到達しません。
  7. クエリ文字列をバイト単位で保持。 パラメーターを並べ替えてはならず(MUST NOT)、再エンコードしてはならず(MUST NOT)、+ をスペースとして解釈してはなりません(MUST NOT)。空のクエリを伴う末尾の ? は保持されます(https://host/p?https://host/p? に正準化され、https://host/p とは別)。? のない URL は ? なしのままです。クエリパラメーター順のみが異なる 2 つの URL は、等価ではなく異なる正準形式です。
  8. フラグメントを除去。 フラグメントは識別子比較に決して参加せず、RFC 9421 §2.2.2 に従いワイヤー上で送られません。
8 ステップすべての後、比較はバイト単位です。実装は比較の前に追加の変換を適用してはなりません(MUST NOT)。

どこに適用されるか

署名プロファイル拡張

リクエスト署名プロファイル は、このアルゴリズムの上にトランスポート固有のルールを重ねます:
  • @authority は正準化された authority から導出され、同じ正準化の後に HTTP/2 :authority 疑似ヘッダー(または受信した HTTP/1.1 Host ヘッダー)と比較されます。非署名の呼び出し元は URL のみから @authority を導出します。
  • 不正な形式の authority は、署名パスで request_target_uri_malformed で拒否されます。非署名の呼び出し元は独自の認可失敗コードを使います(例: TMP には seller_not_authorized)。
  • 受信した HTTP/2 リクエストに :authorityHost の両方が存在するとき、署名プロファイルは正準化後のバイト等価を要求します。これは署名固有のゲートです。なぜなら HTTP/1.1 Host は転送中に書き換えられうるからです。

適合性ベクター

canonicalization.json セットは、固定された { input_url, expected_target_uri, expected_authority } トリプルと、不正な形式の authority 拒否ケースで、上記のすべてのルールを実行します。非署名の呼び出し元は expected_target_uri のみと比較します — expected_authority は署名プロファイルが使う HTTP ヘッダー由来の形式です。上の表のサーフェスのいずれかを実装する SDK は、すべてのコミットでこのセットを実行すべきです(SHOULD)。正準化の分岐は、本番の相互運用バグがサーフェスするまで静かです。

よくある落とし穴

  • ToASCII の前に IDN を ASCII 小文字化。 Bücher.example を ASCII で小文字化 → bücher.example だが、UTS-46 正しいパスは元のバイトを処理しなければならない。TypeScript url.domainToASCII、Go golang.org/x/net/idna、Python の idna パッケージ(IDNA2003 である str.encode('idna') ではない)はモードのデフォルトで分岐する。上記の 4 つのフラグを持つ UTS-46 Nontransitional にピン留めする。
  • 連続するスラッシュの折り畳み。 /admin//foo/admin/foo は異なる正準形式。折り畳む生成者と折り畳まない比較者(またはその逆)はパス混同攻撃を開く。
  • クエリの再エンコード。 クエリ文字列の正規化は魅力的に見えるが禁止。?x=1&y=2?y=2&x=1 は異なる正準形式。
  • 空のクエリを伴う末尾の ? https://host/p?https://host/p は異なる。生成者が送ったものを保持する。adagents.json や類似のレジストリに URL を登録するパブリッシャーは、空クエリ形式を意図しない限り末尾の ? なしで貼り付けるべき。
  • フラグメント除去を忘れる。 フラグメントは識別子比較に決して参加しない。
  • http://https:// の混在。 スキームは強制ではなく保持される。authorized_agents[].url を登録するパブリッシャーは、パブリックインターネットで到達可能を意図するものすべてに https:// を使わなければならない(MUST) — http:// エントリは https:// 呼び出し元との一致に失敗し、その逆も同様で、非 HTTPS URL はトランスポート完全性保証を持たない。