QUICをゆっくり解説(5):2回目以降のハンドシェイクと0-RTT

2021年08月25日 水曜日


【この記事を書いた人】
山本 和彦

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

「QUICをゆっくり解説(5):2回目以降のハンドシェイクと0-RTT」のイメージ

前回はクライアントとサーバが初めてコネクションを確立する際に実行されるハンドシェイクについて説明しました。まだ、コネクションの切り方を説明してないので恐縮ですが、今回は1回目のコネクションが終了した後の2回目以降のハンドシェイクについてお話しします。

事前共有鍵

前回使ったQUICのハンドシェイク図を少し修正して以下に示します。

色の意味はこうでした。

  • 薄い灰色:イニシャル鍵で暗号化されている
  • 濃い灰色:ハンドシェイク鍵で暗号化されている
  • 黒色:アプリケーションデータ鍵で暗号化されている

イニシャル鍵は、クライアントが乱数的に作ったサーバのコネクションIDから生成されました。

ハンドシェイク鍵やアプリケーションデータ鍵は、鍵交換で共有した鍵を元に作られるのでした。実は、これらの鍵の生成には、もう1つ入力があります。それは、事前共有鍵(PSK: Pre-Shared Key)と呼ばれています。

1回目のハンドシェイクでは PSK は存在しないので、0のバイト列が使われます。長さは、利用するハッシュ関数で決まります。

PSKは、ハンドシェイク鍵やアプリケーションデータ鍵と同様に、鍵交換で共有した鍵と前回のPSKを使って生成されます。

セッションデータ

2回目以降のハンドシェイクで、1つ前のハンドシェイクで共有した情報を使うと、次の3つの利点が享受できます。

  • どの種類の暗号を使うかなどの折衝を省略できます
  • サーバ認証を省略できます (クライアントは CerfiticateVerify の検証、サーバは CerfiticateVerify の生成を省略できます)
  • 0-RTT データを送れます (最初のパケットに、アプリケーションのデータを暗号化して付加できます)

1つ前のハンドシェイクの情報としては、少なくとも以下を保存する必要があります。

  • サーバ名 (SNI)
  • 共通鍵暗号の種類
  • ハッシュ関数の種類
  • 鍵交換の種類
  • ALPNの値
  • PSK

これをセッションデータと呼ぶことにしましょう。クライアントが接続するサーバの数は限られていますので、セッションデータは単にメモリなどに覚えておけば済みます。一方、サーバは多数のクライアントから接続されるため、セッションデータは大量になり、(保存できる数に上限を設けるなど)適切に管理する必要があります。セッションデータを管理する方法として、以下の2つが考えられます。

  • セッションID方式:セッションデータをなんらかのDBに保存し、それを取り出すためのキーとしてセッションIDを発行してクライアントに渡す
  • セッションチケット方式:セッションデータをそのサーバだけが復号できるように暗号化してクライアントに渡す

それぞれ利点と欠点があります。

セッションID方式の利点は、あるセッションデータが1回しか使われないように実装できることです。セッションデータを取り出す際に、同時に削除すればよいのです。これでリプレイ攻撃を防止できます。欠点は、セッションデータを格納するためにリソースが消費されてしまうことです。

セッションチケット方式の利点は、セッションデータの保存をクライアントに委託するので、リソースが消費されないことです。欠点は、チケットが1回しか使われないように実装できないことです。

実装によっては、一方だけを実装していたり、両方を実装していたりします。両方を実装しているサーバを利用するサーバ管理者は、設定でどちらを使うか選択することになります。

TLS 1.3では、セッションIDもセッションチケットもクライアントにとっては単なるバイト列です。どちらであるかは、生成したサーバしか知りません。(TLS 1.2では、両者は異なる方式として区別できました。) このバイト列をサーバがクライアントに送信する際は、NewSessionTicket という(TLS 1.3の)ハンドシェイクメッセージが使われます。

上記の図に示したように、QUIC ではNewSessionTicketが1-RTTパケットに格納されて送られます。クライアントは、「自分で生成した PSK」と「サーバから受け取った NewSessionTicket」を紐づけて保存しておく必要があります。

2回目のハンドシェイク

ようやく、2回目のハンドシェイクを説明する準備が整いました。

クライアントは、作成したDH公開鍵をClientHelloに収めます。また、「自分で生成した PSK」と「サーバから受け取った NewSessionTicket」を思い出し、ClientHelloにNewSessionTicketの情報を付加してサーバに送ります。

サーバは、NewSessionTicketからPSKを取り出します。セッションID方式ならDBを検索すればよいですし、セッションチケット方式なら復号すればよいですね。ここで、PSKと鍵交換で共有した鍵が得られますので、ハンドシェイク鍵、アプリケーションデータ鍵、そして次のPSKが生成できます。

ServerHelloには、作成したDH公開鍵に加えて、NewSessionTicketを受理したという情報を格納して送ります。サーバ認証は省略されますので、CertificateやCertificateVerifyは送信しません。

ServerHelloを受け取ったクライアントは、鍵交換で共有した鍵が得られ、また覚えておいたPSKがあるので、ハンドシェイク鍵、アプリケーションデータ鍵、そして次のPSKが生成できます。

0-RTT

0-RTTとは、最初のパケットでアプリケーションデータを送信する機能でした。この時点では、鍵交換は終わっていません。そこで、暗号化の鍵は、PSKのみから生成されます。以下の図に示すように、0-RTTで送るアプリケーションデータは、0-RTTパケットに格納され、Initialパケットと共にサーバに送られます。

0-RTTを利用すると、リプレイ攻撃を受ける懸念があります。リプレイ攻撃を防ぐためには、サーバ側ではセッションデータを1回しか利用できない仕組みを実現する必要があります。私が知っている解決策は、セッションID方式を採用し、セッションデータを1回利用したら必ず削除する方法です。

山本 和彦

2021年08月25日 水曜日

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

Related
関連記事