QUICスタックとTLSライブラリの関係とOpenSSLの状況
はじめに
QUICはTLSv1.3に相当するセキュリティを標準装備すると説明されます。図1はよく参照されるスタック構成ですが、TLSがQUICスタックの内部に埋め込まれています。縦に積み上げられた “スタック” になっていません。TLSの埋め込みは何を意味しているのでしょうか?本稿の前半ではTLSとQUICの関係と、TLSライブラリの使われ方をTLS over TCPと比較しながら解説します。後半ではOpenSSLのQUIC対応の状況についてふれます。
なお本稿で処理の流れを追う際は送信を中心に取り上げます。受信についても逆順で同様の処理が必要ですが解説は省略しています。
QUICとTLSv1.3の関係
TLSには大きく分けて、ハンドシェイクプロトコルとレコードプロトコルがあります。前者は暗号スイートの調停や鍵交換、各種パラメータ交換などのネゴシエーションに用いられるプロトコルです。後者は平文のアプリケーションデータから暗号化されたデータを作る演算の仕方や、作ったデータのしまい方の取り決めです。
QUICが利用するのは、TLSv1.3のうちハンドシェイクプロトコルです。レコードプロトコルに相当する部分は、QUICでは独自の方式を採用しています。QUICはTLSv1.3の一部だけを使うだけと言えます。
TLS over TCP通信のおさらい
QUICの前にTCPでのTLS通信についておさらいします。TLS over TCP通信ではTLSライブラリは信頼性が担保されたTCPコネクションの利用を前提とすることができます。TLSライブラリはアプリケーションから渡されたストリームデータのブロック分割と暗号処理に専念し、送信処理はソケットへ書き込むだけです。一方、TCPスタックからみたTLSライブラリをみると、ペイロードにデータを渡してくるアプリケーションに過ぎず、TCPスタックはTLSライブラリの暗号化処理にも一切関与しません。図1の左側のスタックを改めて見てみると、TLSとTCPはきれいに垂直分業されています。
QUICでの暗号処理の概略
TLSv1.3から流用したハンドシェイクプロトコル
QUICがTLSv1.3のハンドシェイクプロトコルを“利用する”とはメッセージのフォーマットを流用し、ハンドシェイクメッセージのビット列を組み立てることと、受信したハンドシェイクメッセージが正しいか検証させることです。すると、メッセージを誰が受送信するのか?と疑問が浮かびますが、メッセージの受送信はQUICスタックの役割です。この関係を表しているのが、RFC9001 “Using TLS to Secure QUIC” にあるfigure 4 (本稿では図2)です。
初期接続のハンドシェイクを例に取り、クラアントでのQUICスタックとTLSライブラリ間のやりとりを順に書き下してみます。
- QUICスタックは、TLSライブラリにハンドシェイクメッセージのビット列 (CRYPTOフレーム)を作ってもらい、それを格納したClient Initialパケット作る
- QUICスタックは1. で作ったInitialパケットを暗号化して相手ホストへ送信する(※)
- QUICスタックは、相手ホストから返答されたハンドシェイクメッセージ(Server Initialや後続のHandshakeパケットから取り出したCRYPTOフレーム)をTLSスタックに渡して検証してもらう。TLSライブラリがOKを返せばハンドシェイク成立。
※ Initialパケットに対しても暗号化処理が行いますが、最初の一発目のパケットの暗号鍵だけはソルトと呼ばれる仕様で決められている固定値を使います。誰でも解読できるので、一発目のパケットは暗号化ではなく難読化しているだけと言えます。
1~3のハンドシェイクの過程においてQUICスタックとTLSライブラリ間でインタラクションが行われています。TLS over TCPではハンドシェイク処理全体をまるっとTLSスタックに任せることができますが、QUICではハンドシェイクが一段づつ進行する度に、QUICスタックによる受送信やハンドシェイクメッセージの鍵情報やQUIC特有のパラメータ情報へのアクセスが行われます。
QUICパケットの暗号化
鍵の導出
ハンドシェイクが成立して鍵交換が行われると、その鍵を元にデータ暗号化のため新たな鍵を導出する手順が続きます。TLSv1.3ではアプリケーションデータを暗号化するための鍵 (key)と初期ベクトル (IV)の二つを導出しますが、QUICでは暗号化する対象ごとに鍵を分けています。ペイロードを暗号化するための鍵 (quic key)、初期ベクトル (IV)に加えてヘッダを暗号化するための鍵 (quic hp: header protection)の三つを作ります。暗号化対象ごとに鍵を分けて導出する手続きはQUIC独自であり、TLSv1.3での手順ではありません。なお鍵導出 (HKDF: Key Drivation Function base on HMAC)の計算自体はOpenSSLをはじめ多くのTLSライブラリが標準的に持つ機能が使えます。
暗号化の範囲
必要な鍵が揃ったのでQUICパケットの暗号化に取り掛かります。図2のQUICスタックの下側に伸びる “QUIC Packet Protection” の機能ブロックの役割です。
図3は、TLS over TCPとQUICの暗号の適応範囲の違いを表現しています。上部のTLS over TCPでは、アプリケーションが渡してきたストリームデータを逐次暗号処理し、適当なサイズにちぎってTCPペイロードに入れます。このときTCPヘッダは暗号化の対象外です。一方、QUICでは、二段階の暗号化 (#1 Payload Encryption, #2 Header Encryption) によって、QUICヘッダも含めてQUICパケット全体を暗号化します。
ペイロードの暗号化
送信時の処理を例に、一段階目のQUICペイロード暗号化(図3の#1 Payload Encryption)から順に見てきます。
図4は入力情報に対する演算フローを可視化してくれています。RFC9000の著者の一人 MozillaのMartin Tomson氏によるQUICセキュリティの解説です。アプリケーションからのストリームデータを受け取ったQUICスタックは、AEAD (Authenticated Encryption with Associated Data)と呼ばれる認証付き暗号方式を適用します。AEADへ四つの情報を入力し、保護されたペイロードを作ります。なお、AEAD内部で適用される暗号アルゴリズムはネゴシエーションで決め、AES-GCMやChaCha20-Poly1305が用いられます。
- Packet Payload (P): 平文のアプリケーションデータ。Pは暗号化の意味
- Key: ペイロード暗号化のための鍵 (本稿ではquic keyと表記)
- Nonce: 初期ベクトル(IV)とパケット番号をXOR演算して作った使い捨ての値 (Nonce: Number of Once)
- Packet Header (A): 補助データとしてヘッダを入力。Aは認証の意味
3. について補足すると、QUICの暗号処理はパケットごとに行われます。初期ベクトル (IV)だけしばらく使い続けると大量パケット収集による推測につながってしまうため、パケットごとに異なる値であるパケット番号を元にして一回限りの使い捨て値 (Nonce)を作って暗号化を行います。また4. ではヘッダを補助データとして入力していますが、暗号化ではなく改ざん検知のために使います。
ヘッダの暗号化
次は二段階目のQUICパケットヘッダの暗号化を見ていきます。図3の#2 Header Encryptionの部分です。ヘッダが縦縞の模様になっていますが、コネクションIDと必要最低限の制御情報だけを平文に残して、残りを暗号化する様子を現しています。マスク (Mask)と呼ばれる情報を作り、ヘッダのうち隠したい部分にだけXOR演算でかぶせます。もう一度マスクをかぶせれば元のヘッダに戻りますが、復号する側では鍵を知らなければ同じマスクを作ることができません。
図5の演算フローでは、Cipher (中身はAES-ECBまたはChaCha20)への入力が二つあります。
- Key: ヘッダを暗号化するための鍵 (本稿ではquic hpと表記)
- Sample: 暗号化したペイロードから一部を取り出したもの
特に、Sampleには重要な工夫があります。ペイロードの暗号化ではパケット番号を使うことでパケットごとに異なるナンスを得ることができました。しかし、ヘッダの暗号化ではパケット番号も暗号化の対象とします。そのためナンスとして使うことができません。そこで、暗号化したペイロードがほぼランダムなビット列となることを利用して、ペイロードの一部をナンスとして取り出す方法を採用しています。RFC9000の著者であるfastlyのJana Iyengar氏による記事 “QUICの成熟” では、この画期的なアイデアが奥一穂氏によるものと言及しています。その記事はQUICにTLSを取り込む過程も詳細に解説しており、議論の経緯とあわせて読めば本稿よりずっと正確に理解できるでしょう。
なぜヘッダまで暗号化するの?
上記では暗号化する方法を見てきましたが、そもそも、なぜヘッダまで暗号化するか理由をふれていませんでした。
TLS over TCPの通信においてTCPヘッダは暗号化されず制御情報が丸見えです。経路途中にある装置 (ミドルボックス)がTCPコネクションを識別して様々な制御をすることができます。例えばファイヤウォール/IDS/IPS, DPI, NATはその代表例です。
TCPのような、なにか特定のプロトコルを解釈する装置が普及したとします。その後に、より新しく、より良いプロトコルが考案されても、導入済み装置にとっては、新プロトコルのトラフィックを未知のものと解釈して通過させずに破棄してしまう可能性があります。エンドホストでのアプリケーション入れ替えに比べて、一度ネットワークインフラに入った装置を隅々まで更新することは、とても手間がかかります。新しいプロトコルの普及を阻害する “硬直化” と呼ばれる問題です。そのためQUICは現在のインターネットで一定の透過性が見込めるUDPの上に構築しつつ、かつミドルボックスが原理的に制御にできないようにヘッダの制御情報を可能な限り暗号化します。
暗号化する制御情報としてパケット番号が選ばれているのも、ミドルボックスの介入を防ぐためです。シーケンシャルに増加する情報を単純な固定鍵で暗号化すると推測されやすく、コネクション追跡を試みる装置が出現してしまうかもしれません。そのような装置の実装をあきらめさせる工夫です。
QUIC暗号処理とTLSライブラリの関係まとめ
QUICスタックはTLSライブラリは密にインタラクションしながらネゴシエーション実行とQUICパケットを組み立てを行っています。特にペイロードとヘッダが互いに参照し合いながらパケットが作り出されていく様子は非常に技巧的です。
ヘッダに配置する制御情報は、QUICスタックが主となって管理するコネクションやストリームの状態を参照して生成される情報です。制御情報の管理が暗号処理が密接な関係にありQUICスタックとTLSライブラリを垂直にきれいに分業させるのは難しいです。冒頭の図1のQUICの中にTLSが埋め込まれた図になるのは自然な構成です。
なお、QUICスタックは、AEADとその中身の演算アルゴリズムなどはTLSライブラリ (OpenSSLであればlibcryptoにある関数群)をつど呼び出しながら実装されています。TLSv1.3のレコードプロトコルとは全くの別物ですが、TLSライブラリに強く依存しながらQUICパケットは生成されています。
OpenSSLへのQUIC拡張の提案
MicrosoftとAkamaiらが中心となりOpenSSLをQUICに対応させたフォークが開発されています。本稿ではMicrosoftとAkamaiによるフォークを “OpenSSL+quic”と表記します。Googleが自社のQUICスタックQUICHEの開発のため、BorningSSLに拡張APIを設けたのですが、OpenSSL+quicはそれらをオリジナルのOpenSSLに移植した形となっています。
QUIC用拡張APIの一つが、前節で述べたQUICスタックとTLSスタック間のインタラクションです。オリジナルのOpenSSLは一連のハンドシェイクの処理がひとまとめで進行するAPIとなっており、ハンドシェイクの各段階ごとに進行を制御したり、途中過程で鍵情報やQUIC特有のパラメータへのアクセスを行うことができなかった点を、OpenSSL+quicでは拡張しています。APIリファレンスは、BoringSSLのドキュメントのQUIC integrationの節に詳細が書かれています。
2019年4月、MicrosoftとAkamaiから、OpenSSL projectのアップストリームに戻すプルリクエストが投げられました。プルリクエストに始まる議論では、MicrosoftとAkamaiだけでなく、OpenSSLを利用するQUICスタック開発者やアプリケーション開発者からの多数のコントリビューションもあり、実用に耐える実装に仕上がりつつありました。実際に、OpenSSL+quicはMicrosoft自身のMsQUICから使うことができたり、ngtcp2と組み合わせてnode.jsのQUIC対応やApache Traffic Serverに利用されたり、Nginx-quicで利用できるなど実利用が広がっています。
しかし、2021年10月 OMC (OpenSSL Managment Committe) はプルリクエストを否決してしまいました。OpenSSL projectがQUICスタックを独自に実装する方向ですが、記事執筆時点では、何がどう実装されるのかの議論は始まっておらず、スケジュールも含めて見通しは不透明です。OpenSSLはTLSライブラリの参照実装として大きな役割を果たしてきましたが、今回の対応は数々のQUICスタックがOpenSSLを参照する行為を拒否する形になってしまいました。すでに実利用が進みつつある状況で、OpenSSLの方向性が混迷してしまったことに落胆の声があがっています。
ngtcp2がサポートするTLSライブラリ
ngtcp2は、TLSライブラリとしてOpenSSL+quic, BoringSSL, GnuTLS をサポートしています。ngtcp2がアプリケーション開発者に見せてくれるAPIは、TLSライブラリの違いを、なるべく見えないように隠蔽してくれています。TLSライブラリ依存部分が書き分けられており、QUICスタックがTLSライブラリをどのように呼び出すのか、横並びで比較しやすいです。例えば、OpenSSL+quic, BoringSSLのAPIはよく似ていますが違う点もあるのがわかります。なお、GnuTLSも作法に違いはありますがQUICに必要なAPIを持っています。実際のコードを見ることで理解が深まりました。素晴らしい実装をOSSとして開発してくれる開発者に感謝します。
おしまいに
ところで、TLSの高速化のためハードウェアアクセラレータの適用が思い浮かびます。 Netflixのようなハードウェアを活用した大容量Webサービスもあり、kTLSとNICのTOE (TCP Offload Engine)と組み合わせて、TCPパケットを解釈できるNICハードウェアに暗号処理をオフロードする形で実現しています。
QUICとTLSが密結合で動くことを考えると、ハードウェアへのオフロードは、TLSだけでなくQUICもセットにする必要があるかもしれません。しかし、アプリケーションとして実装するQUICの思想とは完全に逆行しますし、実装対象がミドルボックスではないものの、QUICを解釈するハードウェアは、みずから進んで硬直化を招いていると思えてきます。ソフトウェアの柔軟性とハードウェアの高速性のジレンマを感じるところです。少なくともプログラマブルで容易にアップデートできるハードウェアを使うことは必須です。
課題はたくさんありますが、HTTP/3に完全に移行した時代を想定し、爆速のWebサービスを実現したいと思う方、ご連絡をお待ちしています。
参考にした資料
- QUIC Security | NDSS QUIPS Workshop (2020/02)
https://docs.google.com/presentation/d/1OASDYIJlgSFg6hRkUjqdKfYTK1ZUk5VMGP3Iv2zQCI8/ - QUIC用APIを実装したOpenSSL forkの登場 (quictls/openssl) | ASnoKaze blog (2021/03/07) https://asnokaze.hatenablog.com/entry/2021/03/07/233636
- QUICの暗号化と鍵の導出について | ASnoKaze blog (2019/04/22)
https://asnokaze.hatenablog.com/entry/2019/04/22/000927 - HTTP/3 and QUIC: Past, Present, and Future | Akamai Blog (2021/06/21)
https://www.akamai.com/blog/performance/http3-and-quic-past-present-and-future - THE QUIC API OPENSSL WILL NOT PROVIDE | Daniel Stenberg blog (2021/10/25)
https://daniel.haxx.se/blog/2021/10/25/ - QUICむけにAES-GCM実装を最適化した話 (1/2)&(2/2) | Kazuho’s Weblog (2020/06)
http://blog.kazuhooku.com/2020/06/quicaes-gcm-12.html http://blog.kazuhooku.com/2020/06/quicaes-gcm-22.html