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

# URL 正準化

> 2 つの URL が識別子として比較されるあらゆる場所で AdCP が使う正準化ルール — リクエスト署名、認可マッチング、レジストリルックアップ。

AdCP はいくつかの場所で URL を識別子として比較します: リクエスト署名プロファイルの `@target-uri`、`adagents.json` の `authorized_agents[].url` エントリ、TMP `AvailablePackage` の `seller_agent.agent_url`、`format-id` と `ProviderEntry` の `agent_url`、その他 URL がプライマリキーである任意のレジストリ。単一の正準化アルゴリズムがこれらすべてを統治するため、どのサーフェスがルックアップをしていても、バイト単位で異なるが意味的に等しい 2 つの URL は等しく比較されます。このページはそのアルゴリズムの権威ある拠点です。[リクエスト署名プロファイル](/docs/building/by-layer/L1/security#署名付きリクエストトランスポート層) はそれを引用し、トランスポート固有の拡張を追加します。

## アルゴリズム

正準化は、この順序で RFC 3986 §6.2.2（構文ベースの正規化）と §6.2.3（スキームベースの正規化）を適用します。実装はすべてのステップを適用し、結果をバイト単位で比較しなければなりません（MUST）。

1. **スキームを小文字化**（`HTTPS` → `https`）。スキーム自体は保持されます — `http` と `https` は異なる形式に正準化され、識別子比較で一致してはなりません（MUST NOT）。

2. **ホストを小文字化。** IDN ラベルについては、**UTS-46 Nontransitional processing（`CheckHyphens=true`、`CheckBidi=true`、`UseSTD3ASCIIRules=true`、`Transitional_Processing=false`）** を使って Punycode A-label（ACE 形式）に変換します（`bücher.example` → `xn--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@host` → `host`。次の authority 形状は不正な形式で拒否されなければなりません（MUST） — 生成者はそれらを発してはならず（MUST NOT）、比較者はそれらを拒否しなければなりません（MUST）:
   * userinfo だがホストなし: `https://user@/p`
   * ホストがまったくない: `https:///p`、`https://: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=1` → `https://host/?x=1`）。

6. **パーセントエンコーディングを正規化。** 16 進数を大文字化（`%2f` → `%2F`）。パーセントエンコードされた unreserved 文字をデコード（RFC 3986 §2.3 に従い `ALPHA / DIGIT / "-" / "." / "_" / "~"` なので `%7E` → `~`、`%2Dfoo` → `-foo`、`%41` → `A`）。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）。

## どこに適用されるか

| Surface                                       | Comparison                                                                  | Reference                                                                                                                                            |
| --------------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| リクエスト署名                                       | `@target-uri` 正準出力が署名・検証される                                                 | [署名付きリクエスト（トランスポート層）](/docs/building/by-layer/L1/security#署名付きリクエストトランスポート層)                                                                         |
| TMP セラー認可                                     | `seller_agent.agent_url` 対 `authorized_agents[].url`                        | [TMP Sync-Time Validation](/docs/trusted-match/specification#sync-time-validation)                                                                   |
| TMP プロバイダー解決                                  | `ProviderEntry.agent_url` 対ルーターの登録済みプロバイダーエンドポイント                           | [TMP Product Integration](/docs/trusted-match/specification#product-integration)                                                                     |
| `adagents.json` ルックアップ                        | 「このエージェントはこのプロパティに認可されているか？」を問う任意の呼び出し元                                     | [adagents.json スキーマ](https://adcontextprotocol.org/schemas/v3/adagents.json)                                                                         |
| `format-id` 解決                                | `format-id.agent_url` 対エージェントがそのフォーマットに公開する URL                             | [format-id スキーマ](https://adcontextprotocol.org/schemas/v3/core/format-id.json)                                                                       |
| `adagents.json` `authoritative_location` 間接参照 | ポインターをたどる。ターゲット URL は同じ方法で正準化されなければならない（MUST）                               | [Managed networks](/docs/governance/property/managed-networks#security-considerations)                                                               |
| 来歴検証者許可リスト                                    | `verify_agent.agent_url` 対 `creative_policy.accepted_verifiers[].agent_url` | [Provenance Verification](/docs/governance/creative/provenance-verification#the-verifier-contract-seller-publishes-buyer-represents-seller-confirms) |
| URL プライマリキーを持つ任意のレジストリ                        | 正準形式がキー。生の入力はキーでない                                                          | -                                                                                                                                                    |

## 署名プロファイル拡張

[リクエスト署名プロファイル](/docs/building/by-layer/L1/security#署名付きリクエストトランスポート層) は、このアルゴリズムの上にトランスポート固有のルールを重ねます:

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

## 適合性ベクター

[`canonicalization.json`](https://adcontextprotocol.org/compliance/latest/test-vectors/request-signing/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 はトランスポート完全性保証を持たない。
