TLS 1.3の0-RTTとHTTP 425 Too Early:リプレイをサーバー側でどう扱うか

TLS 1.3のearly dataには接続をまたぐリプレイがないという保証はありません。RFC 8446とRFC 8470をもとに、0-RTTの限界、Early-Dataヘッダー、425を返すべき場面を整理します。

TLS 1.30-RTTHTTP 425リプレイ攻撃Webセキュリティ

TLS 1.3の0-RTT(early data)を有効にすると、再訪したクライアントはハンドシェイクの完了を待たずに最初のHTTPリクエストを送れます。その代わり、そのリクエストは攻撃者にコピーされて再送されうる状態になります。RFC 8446は0-RTTデータについて「接続をまたいだ非リプレイの保証はない」と明記しています。この記事では、そのリスクがどこから生じるのか、そしてRFC 8470が定義するEarly-Dataヘッダーと425 Too Earlyステータスコードでサーバーが何をできるのかを、標準文書の記述に沿って整理します。

0-RTTの仕組み

RFC 8446の2.3節によると、クライアントとサーバーがPSK(pre-shared key)を共有している場合、クライアントは最初のフライトにアプリケーションデータを載せられます。PSKは外部で事前に配布するか、以前のハンドシェイクで受け取ったセッションチケットから得ます。セッションチケットを使う後者がセッション再開(resumption)です。

Client                                   Server
ClientHello
+ early_data
+ pre_shared_key
(Application Data*)   -------->
                                     ServerHello
                               {EncryptedExtensions}
                                      + early_data*
                                      {Finished}
                      <--------  [Application Data*]
(EndOfEarlyData*)
{Finished}            -------->

丸括弧()で示したearly dataは、PSKから導出した鍵だけで暗号化されます。ServerHelloを待たないことが速さの源であり、弱さの源でもあります。

*は状況によってのみ送られるメッセージや拡張を表します。RFC 8446の4.5節によると、クライアントがEndOfEarlyDataを送るのは、サーバーがEncryptedExtensionsにearly_data拡張を含めてearly dataを受け入れた場合だけで、そうでなければ送ってはなりません(MUST NOT)。RFC 8446の原文の図では(EndOfEarlyData)に*が付いていませんが、これを訂正するエラッタ(Errata ID 8794)が「Held for Document Update」の状態で登録されているため、上の図に反映しました。

同じ節は、0-RTTデータのセキュリティ特性が通常のTLSデータより弱いと注意しています。

  • 提示されたPSKから導出した鍵のみで暗号化されるため、前方秘匿性(forward secrecy)がありません。
  • 通常の1-RTTデータはサーバーのRandom値によってリプレイから守られますが、0-RTTデータはServerHelloに依存しないため、接続をまたいだ非リプレイの保証がありません。 TLSクライアント認証やアプリケーションプロトコル内の認証で保護されたデータほど、この点が重要だとRFCは補足しています。

保証されることもあります。同じ接続内でサーバーが同じ0-RTTデータを二度処理することはなく、攻撃者が0-RTTデータを1-RTTデータに見せかけることもできません(別々の鍵で保護されているためです)。

TLSが防ぐもの、防がないもの

RFC 8446の8節は脅威を二つに分けています。

  1. 単純な複製:ネットワーク上の攻撃者が0-RTTデータのフライトをそのままコピーして再送します。
  2. クライアントの再試行の悪用:サーバー群が状態を共有しない複数のゾーンに分かれている場合、攻撃者は同じClientHelloとearly dataをゾーンAとゾーンBの両方に送れます。Aは0-RTTを受け入れて処理し、Bは0-RTTを拒否してフルハンドシェイクを要求します。攻撃者がAのServerHelloを遮断すると、クライアントはBとハンドシェイクを完了し、おそらくリクエストを再送します。システム全体としては二重に処理されます。

一つ目の脅威に対して、RFCはサーバーが状態を共有して0-RTTデータを最大1回だけ受け入れるようにすることをSHOULDとし、8.1〜8.3節で方法を示しています。

  • 使い捨てチケット(8.1):有効なチケットを保存しておき、使われたら削除します。分散環境ではセッションDBの共有が必要になり、0-RTTの成功率を高く保つのが難しくなる場合があります。
  • ClientHelloの記録(8.2):ClientHelloから導出した一意の値(通常はrandom値かPSK binder)を一定の時間窓の中で記録し、重複を拒否します。起動直後のサーバーは、記録窓が起動時刻と重なる間は0-RTTを拒否すべき(SHOULD)とされています。
  • 鮮度チェック(8.3):チケットの経過時間を使い、古いClientHelloの0-RTTを拒否します。インターネット上のクライアントでは時計のずれや計測のばらつきを吸収するために10秒程度の窓が必要になるとRFCは述べつつ、鮮度チェックだけではその窓の中のリプレイを検出できないため不十分だと明記しています。

これとは別に、サーバーは同じ0-RTTハンドシェイクに対して各インスタンス(マシン、スレッドなど)が0-RTTを最大1回しか受け入れないようにしなければなりません(MUST)。これはリプレイ回数を「サーバーインスタンスの数」に抑える最低限の要件であり、リプレイがなくなるという意味ではありません。

二つ目の脅威については明確です。TLS層では防げず、アプリケーションが対処しなければなりません(MUST)。付録E.5も、8節の仕組みはクライアントデータを複数回受け取る事態を完全には防がないとまとめ、次のような攻撃例を挙げています。

  • 購入や送金など、副作用のある操作の重複実行
  • 保存した0-RTTメッセージを再送して順序を入れ替える(例:deleteをcreateの後ろに移す)
  • 別のキャッシュノードに再送し、別の接続で応答遅延を測ってリクエスト内容を推測するキャッシュタイミング攻撃
  • 大量のリプレイが可能な場合の、暗号処理時間の測定やレート制限システムの過負荷

つまりTLSスタックのアンチリプレイはリプレイ回数を抑える仕組みであり、「このリクエストは一度だけ効果を持つ」という保証はアプリケーションの責任です。

RFC 8470:HTTPサーバーが取れる三つの選択肢

RFC 8470の3節は、early dataを有効にしたサーバーがリプレイのリスクを抑える方法を三つ挙げ、どれも同等に有効だとしています。

  1. TLS層でearly dataを拒否する:TLSではearly dataをすべて受け入れるか、すべて拒否するかしか選べないため、early dataで届いたリクエストはすべて破棄されます。
  2. ハンドシェイク完了まで処理を保留する:正常に完了した接続のリクエストだけを処理します。リクエストごとに保留するかどうかを決められます。
  3. 425(Too Early)を返す:リプレイのリスクが高いと判断した個々のリクエストについて、early dataを使わずに再送するようクライアントに伝えます。

重要なのはその次の記述です。リクエストがリプレイに耐えられるかどうかはリソースごとに異なり、それを知っているのはオリジンサーバーだけです。メソッドの安全性は判断材料の一つですが、安全なメソッドでも副作用を生むリソースがあるため、それだけには頼れません。そのためRFC 8470は、オリジンサーバーがリソースごとにearly dataの可否を明示的に設定できるようにすることを推奨(RECOMMENDED)し、その情報がない場合はearly dataを拒否するか、ハンドシェイク完了前にはリクエストを処理しないようにしなければならない(MUST)と定めています。

リクエストの一部だけがearly dataで届き、残りがハンドシェイク後に届くこともあります。判断の基準は「いつ届いたか」ではなく、サーバーがリクエストの内容に基づいて動作を始める時点です。ヘッダーの解析やルーティングの決定は副作用がない可能性が高いものの、それ以上の処理はケースによります。

クライアント側のルール

RFC 8470の4節によると、ほかに情報がない場合、クライアントは安全なメソッドのリクエストだけをearly dataで送ってよく(MAY)、安全でないメソッドや安全性が不明なメソッドは送ってはいけません(MUST NOT)。RFC 9110の9.2.1節では、GET、HEAD、OPTIONS、TRACEが安全なメソッドと定義されています。

early dataを使うクライアントは、425を受け取ったらリクエストを再試行しなければなりません(MUST)。5.2節では、ユーザーエージェントは自動で再試行すべき(SHOULD)で、その再試行をearly dataで送ってはならない(MUST NOT)としています。

RFC 8446の付録E.5はTLS実装にも制約を課しています。アプリケーションが明示的に求めない限り0-RTTを有効にしてはならず、サーバーに拒否された0-RTTデータをアプリケーションの指示なしに自動再送してもいけません。また、ハンドシェイクが完了したかどうかをアプリケーションが判定できる手段を提供しなければなりません。サーバー側で「保留」戦略を実装できるのは、この要件があるからです。

プロキシやCDNの背後にある場合:Early-Dataヘッダー

CDNやリバースプロキシでTLSを終端すると、オリジンはリクエストがearly dataで届いたかどうかを知りません。一方ゲートウェイには、そのリクエストがリプレイされても問題ないかを判断する情報がありません。RFC 8470の5節で定義されたEarly-Dataヘッダーと425が、このすき間を埋めます。

GET /resource HTTP/1.1
Host: example.com
Early-Data: 1

ルールは次のとおりです。

  • クライアントとのTLSハンドシェイクが完了する前にリクエストを転送する中継者は、Early-Data: 1を付けなければなりません(MUST)。すでに付いているヘッダーを取り除いてもいけません。
  • 有効な値は1だけです。複数あったり不正な値だったりしても、サーバーは値1のヘッダーが一つあるものとして扱わなければなりません。
  • このヘッダーは中継者向けです。early dataでリクエストを送るユーザーエージェントは付ける必要がありません。early dataで送ること自体が、425を理解し再試行する意思があることを意味するからです。
  • オリジンがハンドシェイク完了を待っても、Early-Data: 1付きのリクエストは安全になりません。 前のホップですでにearly dataとして送られているためです。安全に処理できないなら425で拒否しなければなりません(MUST)。
  • レスポンスやリクエストトレーラーにこのヘッダーを含めてはいけません。

ゲートウェイ側の義務もあります(6.1節)。オリジンがEarly-Dataヘッダーを理解し425を正しく生成すると分かっていない限り、early dataで受け取ったリクエストを転送してはなりません(MUST NOT)。確信がない場合は、クライアントとのハンドシェイク完了まで転送を遅らせるか、自ら425を返すべきです(SHOULD)。425を受け取ったとき、中継者が受け取って転送したリクエストにすでにEarly-Dataヘッダーがあった場合は、その425をそのまま転送しなければなりません(MUST)。中継者自身がearly dataで受け取ったリクエストであれば、その接続のTLSハンドシェイク完了を待ったうえで自ら再試行してもかまいません(MAY)。

オリジンでの処理例

以下は特定のフレームワークに依存しない疑似コードです。要点は、early dataで処理してよいルートを許可リストとして明示的に管理することです。

# early dataで届いても処理してよいとリソース所有者が判断したルートだけを登録
EARLY_DATA_OK = {
    ("GET", "/api/catalog"),
    ("GET", "/static/app.js"),
}

# request.tls.used_early_dataは説明用の仮の属性で、実在するAPIではない。
# このリクエストのバイトが一つでもTLSのearly dataで届いていればTrue。
def handle(request):
    via_early_data = (
        request.headers.get("Early-Data") is not None  # 前のホップでearly data
        or request.tls.used_early_data                  # このサーバーがTLSを終端
    )
    if via_early_data and (request.method, request.route) not in EARLY_DATA_OK:
        return Response(status=425)  # クライアントはearly dataなしで再試行
    return route(request)

いくつか注意点があります。

  • request.tls.used_early_dataはプレースホルダーです。実際には、使っているTLSスタックがリクエストごとに「そのリクエストのバイトのうちearly dataとして受信したものがあるか」を記録し、アプリケーションに提供する必要があります。「ハンドラーの実行時点でハンドシェイクがまだ完了していないか」で代用してはいけません。early dataで送られたリクエストがハンドシェイク完了までバッファーに留まってから渡されることもあれば、一部だけearly dataで届き残りがハンドシェイク後に届くこともあり(3節)、順序どおりに届かないプロトコルではハンドシェイク完了後に届くこともあります(6.4節)。いずれの場合もその条件は偽になり、リプレイに弱いリクエストがそのまま処理されてしまいます。
  • Early-Dataの値がおかしくても1として扱う必要があるので、値の比較より存在の有無で判断するほうが正確です。
  • このサーバー自身がTLSを終端しているなら、425の代わりにハンドシェイク完了まで処理を保留する選択肢もあります。その場合、ハンドシェイク完了の状態は保留した処理を再開してよいかの判断にだけ使い、early dataかどうかの判定には使いません。またEarly-Data: 1付きのリクエストには保留は効きません。
  • サーバーは、リクエストがearly dataで届いたかEarly-Data: 1が付いている場合を除き、425を返すべきではありません(SHOULD NOT)。それ以外のクライアントが再試行できる保証はありません。
  • 425はデフォルトではキャッシュされません。
  • GETであっても、閲覧数の加算、クーポンの発行、ログイン用リンクの消費など状態を変えるルートは許可リストに入れないようにします。

運用で確認すること

インスタンス間の一貫性。 RFC 8470の6.2節は、ハンドシェイク前に処理すると安全でないリクエストについて、ゲートウェイを含むすべてのサーバーインスタンスが同じ判断をする必要があると強調しています。拒否、保留、425はどれも同等に有効なので、インスタンスごとに方法が違っても一貫しているとみなされます。ただし、同じデータを別のインスタンスが異なる形で扱いうるなら、ハンドシェイク完了前にそのデータに基づいて動作してはなりません(MUST NOT)。

高負荷時。 6.3節は、負荷のかかったサーバーはearly dataを受け入れて選別処理するより、TLS層で丸ごと拒否するほうを優先すべき(SHOULD)としています。503や425を返すとクライアントの再試行を招き、かえって負荷が増えることがあるためです。

恩恵を受けるオリジンがない場合。 6.1節は、Early-Dataヘッダーに対応したオリジンが一つもないなら、ゲートウェイでearly dataを無効にするほうが効率的だとしています。RFCの表現では、得られる性能上の利点はせいぜい控えめなものなのに、かかる労力は大きいということです。

HTTP/3。 RFC 8470は、この助言がQUIC上のHTTPの0-RTTにも当てはまると述べています。QUICのように順序どおりに届かないプロトコルでは、early dataがハンドシェイク完了後に届くこともあり、その場合は同じリクエストのリプレイを他のインスタンスが正しく扱うと信頼できるときに限り処理してよい(MAY)とされています(6.4節)。

425だけでは足りない理由

425とEarly-Dataは「early dataの段階で処理しない」ための仕組みです。RFC 8446の8節が挙げた二つ目の脅威、つまり0-RTTを拒否されたクライアントが1-RTTで再送したリクエストと、元のゾーンですでに処理されたリクエストが重なる状況は、HTTPの再試行全般の問題でもあります。RFC 8446自身も、再試行の挙動を持つクライアントを抱えるアプリケーションは、すでに何らかのリプレイ対策を必要としていると書いています。

決済、注文、送金のように重複実行が困るPOSTエンドポイントでは、early dataの遮断とは別に、アプリケーションレベルの重複排除が必要です。クライアントが生成したリクエスト識別子(一般にidempotency keyと呼ばれる方式)をサーバーに保存し、同じキーの2回目のリクエストには最初の結果を返す方法が実務でよく使われます。この方式はRFC 8446やRFC 8470が定義したものではなく、実務上の慣行です。メソッドの意味論そのものはHTTPのべき等メソッドの解説で扱っています。

チェックリスト

  • CDN、ロードバランサー、Webサーバーなど、各TLS終端ポイントで0-RTTが有効かどうかを確認します。
  • 有効なら、オリジンがEarly-Dataヘッダーを受け取れるか、425を返せるかを確認します。どちらもできないなら、early dataを無効にするほうがRFC 8470の助言に近い対応です。
  • early dataで処理してよいルートを許可リストとして明示します。メソッドだけで判断しません。
  • すべてのインスタンスが同じルールを使っているか確認します。
  • 二重に実行されては困る処理は、early dataの設定とは関係なく、アプリケーションレベルで重複排除します。

参照した標準

RFC 8470はメソッドの安全性の定義としてRFC 7231の4.2.1節を引用していますが、RFC 7231は現在RFC 9110に置き換えられているため、本文ではRFC 9110の該当節を基準にしました。特定のサーバーやCDN製品の設定方法やデフォルト値は製品やバージョンによって異なるため、この記事では扱っていません。

ブログへ