🔑

URLにおけるフラグメントと鍵の受け渡し

2021/09/20に公開

概要

インターネット上のリソースを指し示すために使われる URL [1]は、スキームや接続先、場所を示す複数の要素で構成されている。このうち、末尾の # から始まる要素である フラグメント は、一般的なブラウザにおいてサーバに送信されないという特殊な性質を持つ。この記事では、その特徴を利用したアプリケーションの例と、フラグメントを活用する際の注意点について触れる。

フラグメントとは

URLは、その上位概念としてのURI - RFC 3986またはURL Standardによってその構文が定義されている。URL構文のうちフラグメントはURLの末尾に置かれる # から始まる要素で、クライアント側で処理されることを意図した文字列である。RFC 7230ではリクエストにおけるリソースの指定から除外される旨が明記されており、今日の一般的なブラウザでもそのように動作する[2]

通常のHTMLドキュメントでは、フラグメントは同じドキュメント内のある場所を示しており、ブラウザで開くとその位置まで移動するという挙動を示す。以下の例では「コメントを読む」をクリックすると、アドレスバーに表示されたURLの末尾に #super_fragment が付加され、「フラグメントは便利です」に移動する。ブラウザやサイトによっては、瞬時に移動するのではなくスクロールのアニメーションを表示するかもしれない。

<a href="#super_fragment">コメントを読む</a>
<!--
  長大な文書
-->
<div id="super_fragment">フラグメントは便利です</div>

SPAなどのHTML上で実装されたアプリケーションでは、JavaScriptによって画面遷移が制御されており、アプリケーションの状態をURLにマッピングするためにフラグメントが使われる場合がある。例えば、 https://example.com/#!/user/amane/edit というURLを開くと、 amane というユーザの情報を編集する画面が表示されるように設定できる。アプリケーション内のある場所を示すという意味では、通常のHTMLドキュメントと大きな違いはないとも言えるだろう。

さて、このようなフラグメントの使い方は、あくまでURLが示すリソースをさらに絞り込むための副次的な情報としての利用に留まっており、サーバに送信されないという特殊な性質を十分に活かしきれていない。ある情報をクライアントが知りつつサーバが知らないという状況は、エンドツーエンド暗号化(E2EE)とよく似ている。

E2EEとは

エンドツーエンド暗号化(End-to-End Encryption / E2EE)とは、他者[3]を経由したり、他者[4]が保管・転送するデータ――プライベートなチャットや音声通話・クラウドストレージ上の機密情報などを含むが、それに限らない――を暗号化する際に、データを利用者/送信者/受信者だけが持つ鍵で暗号化する技術である。これにより、通信を傍受しうる必ずしも信頼できない他者が、勝手にその内容を閲覧することを(運用上ではなく)原理的に防いだり、たとえサーバからデータが流出してもその内容を保護することができる。

HTTPSやSMTPSなどのTLSを用いた経路暗号化では、経路上でデータを傍受される心配はなくなるが、依然としてサービスの提供者はそのデータを閲覧することができる。これらのプロトコルはサーバ上のリソースを特定し、利用するためにリクエストを送信するので当たり前の挙動に思えるものの、サーバにリソースの内容を知られたくないケースも多々存在する。リソースの所有者はサービス提供者と必ずしも一致しないからだ。例えば、クラウドストレージに保管するデータは利用者の所有物であり、プライベートなチャットの内容は送信者と受信者のみが知るべき情報である[5]。このようなアプリケーションでデータを安全に保つために、E2EEが役立つ。

クラウドストレージにおけるE2EEについては、単に自分が保管・閲覧する目的のみで使用しており、誰かと共有する必要がないのであれば、手元で自由に暗号化してからアップロードするだけでよい。例えば、VeraCryptなどでコンテナを作成して通常のファイルとして同期すれば、管理者からは無意味なバイト列にしか見えないだろう[6]

一方で、受信者が存在するチャットや音声通話のような状況では、お互いに使用する鍵についての合意が必要不可欠である。素朴にはサーバが鍵の仲介を行うことで実現できるが、E2EEの定義を満たすには、受け渡す鍵をサーバに知られないまま共有するための工夫が必要となる。Signalで採用されている二重ラチェットアルゴリズムはその実現方法のひとつだが、ここでは深く触れない。

URLを使った鍵の受け渡し

URLを用いた簡易的なE2EEの実装――リソースに付随する鍵の共有――として、フラグメントを利用することができる。簡易的というのは、この手法が別の暗号化手法や信頼できるチャネルによる通信と組み合わせて使われる補助的な手段であることを示しており、単に手元のデータを送信するだけならそのチャネルで直接送信した方がよい。

抽象的には、以下のような手順でE2EEを実現する。

  1. 送信者: データ (D)を自由に暗号化してデータ (E)を作る
  2. 送信者→サーバ: データ (E)をサーバにアップロードする
  3. サーバ→送信者: データ (E)に紐付くリソースURLを渡す
  4. 送信者: 復号に必要な情報をフラグメントとしてURL (R)に付加してURL (S)を作る
  5. 送信者→受信者: 信頼できるチャネルを通じてURL (S)を渡す
  6. 受信者→サーバ: URL (S)からフラグメントを取り除いてリクエストする[7]
  7. サーバ→受信者: URL (R)に紐付くデータ (E)を渡す
  8. 受信者: フラグメントの情報を用いてデータ (E)を復号してデータ (D)を得る

5を除く処理はサーバが提供するアプリケーション上で行うのが一般的である。多くの場合、JavaScriptを用いて暗号化や復号といったクライアントでの処理を実現している。悪意あるサーバなら、鍵を取り出して不正に送信するような処理を紛れ込ませることは可能だが、少なくともサーバ上で行われる不正とは異なり解析・検知が可能である。

ここからは、実際に簡易的なE2EEとしてフラグメントを用いているアプリケーションをいくつか紹介する。

フラグメントを通じて鍵を受け渡す例

MEGASyncといったプライバシーを重視するクラウドストレージサービスでは、ファイルの保管にE2EEを用いるのはもちろんのこと、URLでファイルを共有する際にもフラグメントを使用して安全性を保っている。フラグメントより前の部分はサーバ上の場所を示す単なるIDとして機能しており、フラグメントは受け取ったリソースを復号するための鍵としてクライアント側でのみ使用される。

なお、このテキストファイルはフラグメントを含めた共有URLが公開されているため、もはやE2EEとしては機能していない。

フラグメントを使わずに鍵を受け渡す例

フラグメントを使わずにパスで鍵を共有しているため、現状の実装では安全性が低いものの、URLを通じて鍵を受け渡すアプリケーションのアイデアとして有用な例を紹介する。

enzyptはEthereumを使って支払いを検証する典型的なDappである。販売者が示したURLからEthereumを支払うと、コンテンツをダウンロードするための情報を受け取ることができる。具体的には、以下のような手順で販売および購入を行う。

  1. 販売者: 販売したいコンテンツを暗号化する
  2. 販売者→IPFS: 販売したいコンテンツをアップロードする
  3. 販売者→enzypt: コンテンツの名前、値段、支払いを受けるEthereumアカウント、IPFSのハッシュと鍵の一部 (A)を送信する
  4. enzypt→販売者: コンテンツを指すenzypt上のIDを通知する
  5. 販売者: enzypt上のIDと鍵の一部 (B)を付加し、購入用URLを取得する
  6. 販売者→購入者: 信頼できるチャネルを通じて購入用URLを送信する
  7. 購入者→enzypt: enzypt上のIDを用いて鍵の一部 (A)を含むコンテンツのメタデータを受け取る
  8. 購入者→enzypt: 支払いに用いるEthereumアカウントの公開鍵を通知する
  9. enzypt→購入者: 公開鍵に紐付けたランダムな文字列を通知する
  10. 購入者→enzypt: ランダムな文字列を秘密鍵で署名して送信する
  11. enzypt→購入者: トランザクションに付加すべき情報を通知する
  12. 購入者→Ethereumネットワーク: 指定された金額を支払う
  13. 購入者→enzypt: Ethereumのトランザクションハッシュを通知する
  14. enzypt→Ethereumネットワーク: トランザクションを確認して支払いを検証する
  15. enzypt→購入者: 支払いが検証できた場合はIPFSのハッシュを通知する
  16. 購入者→IPFSノード: 購入したコンテンツをダウンロードする
  17. 購入者: 鍵の一部 (A) (B)を用いて復号する

ただし、13は正当な購入者でなくともトランザクションハッシュさえ一致すればコンテンツをダウンロードできてしまうため、URLを公開して販売する用途には向いていない[8]。本来はダウンロードの前に都度8~10のような署名の検証を挟むべきである。

また、購入のたびにトランザクションを発行してチェックする単純な実装なので、ETHの価格変動や手数料の面であまり実用的ではないかもしれない。メタトランザクションの上で適切なトークンを動かした方がよいだろう。

フラグメントを活用する際の注意点

フラグメントはサーバに送信されない自由度の高い要素ではあるものの、仕様上または実用上の制限や注意点がいくつか存在する。

使用できる文字種

RFC 3986URL Standardで定義されている通り、フラグメントは # を含むことができない。

fragment = ( pchar / "/" / "?" )
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
pct-encoded = "%" HEXDIG HEXDIG
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "
" / "+" / "," / ";" / "="
RFC 3986

A URL-fragment string must be zero or more URL units.
The URL units are URL code points and percent-encoded bytes.
The URL code points are ASCII alphanumeric, U+0021 (!), U+0024 ($), U+0026 (&), U+0027 ('), U+0028 LEFT PARENTHESIS, U+0029 RIGHT PARENTHESIS, U+002A (*), U+002B (+), U+002C (,), U+002D (-), U+002E (.), U+002F (/), U+003A (:), U+003B (;), U+003D (=), U+003F (?), U+0040 (@), U+005F (_), U+007E (~), and code points in the range U+00A0 to U+10FFFD, inclusive, excluding surrogates and noncharacters.
A code point is a Unicode code point and is represented as "U+" followed by four-to-six ASCII upper hex digits, in the range U+0000 to U+10FFFF, inclusive. A code point’s value is its underlying number.
A percent-encoded byte is U+0025 (%), followed by two ASCII hex digits. Sequences of percent-encoded bytes, percent-decoded, should not cause UTF-8 decode without BOM or fail to return failure.
URL Standard

例えば、Matrixで部屋に招待する際に用いられるサービスmatrix.toでは、フラグメントにMatrix上のアドレスを指定してURLをシェアする仕組みになっている。Matrix上のアドレスは、 ! で始まる内部的なID !sAsZJiLgwBDLSpbbPB:matrix.amane.moe や、 @ で始まるサーバ上のユーザ名 @amane:matrix.amane.moe# で始まる複数人が参加することを意図した公開アドレス #random:matrix.amane.moe が存在する。このうち # を使用する公開アドレスをシェアする場合は、 #%23 とパーセントエンコーディングしなければならない[9]

現状、多くのブラウザではフラグメントに # を含んでいても問題なく動作するが、仕様に反するURLであることは知っておくべきだろう。

使用できる文字数

RFC 2616によれば、URLの最大長は仕様として定められていない。サーバは長すぎるURLを処理できなければ414(Request-URI Too Long)を返すべきと記されているものの、フラグメントはサーバに送信されない文字列のため、単にブラウザの実装(ひいてはマシンのスペック)に依存する。

例えば、Internet Explorer 9以前は2083文字までしか入力できないという制限があったが[10]、今日の多くのブラウザでは仕様上の制限はなく、動作が遅くなったり表示が崩れたりするだけで済むだろう。ただし、URLを受け渡すチャネルによって最大長を制限される場合もあるため、フラグメントを通じて渡す情報をむやみに大きくしない工夫が必要である。

受け取ったデータの検証

フラグメントを通じて受け取ったデータをJavaScriptで処理する際に、それらを完全に信用するとやっかいな脆弱性に繋がる場合がある。以下の単純な例では、フラグメントにパスを指定して画像を表示することを意図しているものの、任意のスクリプトを注入できてしまう。

// #'onerror='alert(1) でアクセスするとアラートが出る
document.body.innerHTML = "<img src='" + location.hash.substring(1) + "'>";

フラグメントを通じて単一の復号キーを渡すのみであれば、英数字や一部の記号のみを使用できるようにすることで多くの攻撃を防ぐことができるだろう。複雑なデータもJSONやXMLによる表現をBase64などでエンコードしてから渡せばよいかもしれないが、デコードして得られた内容についてはやはり検証が必要である。

まとめ

  • URLの末尾を構成するフラグメントは、URL中でサーバに送信されない唯一の要素である。
  • フラグメントを適切に用いることで、ブラウザで簡易的なE2EEを実現できる。
  • フラグメントを活用する際は、使用できる文字種や文字数に注意して実装すべきである。また、不正なデータの入力に備えて十分に検証してから処理する必要がある。

転載元の「URLにおけるフラグメントと鍵の受け渡し」はCC-BY 4.0(https://creativecommons.org/licenses/by/4.0/)でライセンスされているため、この記事についても同じライセンスが適用されます。

脚注
  1. https://example.com/path/to/resource#fragment など。 ↩︎

  2. ウェブ上のリソースの識別 - HTTP | MDN ↩︎

  3. ここでは契約しているISPや経由する全てのネットワークのこと。 ↩︎

  4. ここではメッセンジャーやクラウドストレージといったサービスの提供者のこと。 ↩︎

  5. サービスの規約によってはこの原則が否定されるかもしれない。 ↩︎

  6. もちろん、コンテナ内の一部のファイルのみ同期したりできないので効率は悪い。 ↩︎

  7. この処理はブラウザが自動で行う。 ↩︎

  8. トランザクションハッシュが一致して、かつURLが公開されていると、購入者でなくともダウンロードして復号できてしまう。支払い済みのトランザクションハッシュは、支払い先のEthereumアカウントが分かれば容易に検索できる。 ↩︎

  9. 内部的なIDでシェアすることも可能だが、招待ページにそのまま表示されるので視認性が低い。 ↩︎

  10. Maximum URL length is 2,083 characters in Internet Explorer ↩︎

Discussion