HTTP/2から見えるTLS事情
これは HTTP/2 アドベントカレンダー19日目の記事です。
この記事はたくさんの資料を読んだ上で書きましたが、間違いとか勘違いとかがあるかもしれません。もしあれば、指摘していただけると幸いです。
実質的に必須となったTLS
HTTP/2は、HTTP/1.1と同じく、暗号化なし/ありのポートとして、80と443を使います。そのため、通信開始時にHTTP/1.1とHTTP/2をネゴシエーションするための仕組みが、HTTP/2で定められています。
このように仕様としては暗号化なしのHTTP/2が定義されていますが、Firefox や Chrome が TLS を要求するために、実質的は暗号化ありが必須となっています。これは、米国の監視プログラムPRISMに代表される広域監視(pervasive surveillance)に対抗するために、IETFがさまざまな通信にプライバシの強化を要求する方向に舵を切ったことが影響していると思われます。
この記事では、HTTP/2 を動機付けとして、TLSの最新事情について説明します。
HTTP/2が要求するTLS
ネットスケープコミュニケーションズ社が1994年に世に送り出した SSL 2.0 は、SSL 3.0(RFC6101) を経て IETF で標準化され TLS となりました。TLS は、1.0(RFC2246)、1.1(RFC4346) と改定されていき、最新のバージョンは 1.2(RFC5246) です。そして、現在 TLS 1.3 が標準化されようとしています。
端的に言えば、HTTP/2が要求するTLSは、来るTLS 1.3時代を見据えて、以下のように TLS 1.2の機能を限定して使っています。
再ネゴシエーションの利用禁止
2009年に再ネゴシエーションに関する脆弱性が発見されました。詳しくは「SSL/TLSのrenegotiation(再ネゴシエーション)における脆弱性」を参照してください。TLS 1.2 以前のすべての SSL/TLS にこの脆弱性があります。対策は、RFC5746で定められている拡張を実装することです。
この拡張を実装すれば、再ネゴシエーションは(今のところ)安全です。追記:クライアントからの再ネゴシエーションを許していると、DoS攻撃を受ける可能性があります。再ネゴシエーションは、サーバからのみ開始できるように設定すべきです。
では、なぜ HTTP/2 で禁止するのでしょうか?実は、再ネゴシエーションには2つの用途があります:
- 共有鍵の更新
- クライアント認証
再ネゴシエーションというと「共有鍵の更新のため」というイメージがあるかもしれませんが、実際にはクライアント認証のために使われていることの方が多いでしょう。
クライアント認証のための再ネゴシエーションは、以下のようなストーリーが一般的です:まず、あるブラウザがTLSでクライアント認証を要求しないコンテンツにアクセスします。そのページにあるリンクを辿ろうとすると、そのコンテンツはクライアント証明書を要求するよう設定されていました。この場合、サーバは再ネゴシエーションを開始し、ブラウザにクライアント証明書を要求します。
HTTP/1.1とTLSという異なる層が協調して動作できるのは、HTTP/1.1が同期的なプロトコルだからです。一方、HTTP/2は、通信路を複数のストリームが非同期に行き交っていますので、再ネゴシエーションを開始する安全なタイミングを決めるのは、簡単ではないと思われます。
このHTTP/2の要請から、TLS 1.3 では再ネゴシエーション自体が削られる予定です。そして、二つの機能を分け、それぞれの役割を果たす方式が定義される見込みです。
圧縮機能の利用禁止
圧縮機能を使うと、CRIME攻撃にさらされます。たとえば、攻撃者がいる公衆無線LANに被害者が接続してしまったとしましょう。攻撃者の目的は、この被害者からあるサイトのクッキーを盗むことです。攻撃者は、なんとかして被害者を特定のサイトに誘導し、被害者のブラウザに悪意のスクリプトをダウンロードさせます。ここで攻撃者ができることは、次の二つです。
このスクリプトは、HTTPヘッダを少しづつ変えなから、目標のサイトにHTTPリクエストを送り続けます。追加するヘッダによっては、その一部とクッキーの一部が一致する場合があるでしょう。圧縮が使われていれば、この重複が圧縮され、パケットのサイズが変わります。つまり、パケットの中身を見なくても、クッキーが予想できるのです。
以下に TLS 1.1 のデータ構造を示します。まず、平文(TLSPlaintext)は、こうなっています。
struct { ContentType type; ProtocolVersion version; uint16 length; opaque fragment[TLSPlaintext.length]; } TLSPlaintext;
TLSPlaintext を圧縮すると、TLSCompressed になります。
struct { ContentType type; /* same as TLSPlaintext.type */ ProtocolVersion version;/* same as TLSPlaintext.version */ uint16 length; opaque fragment[TLSCompressed.length]; } TLSCompressed;
最後に TLSCompressed が暗号文 TLSCiphertext に格納されます。
struct { ContentType type; ProtocolVersion version; uint16 length; select (SecurityParameters.cipher_type) { case stream: GenericStreamCipher; case block: GenericBlockCipher; } fragment; } TLSCiphertext; stream-ciphered struct { opaque content[TLSCompressed.length]; opaque MAC[CipherSpec.hash_size]; } GenericStreamCipher; block-ciphered struct { opaque IV[CipherSpec.block_length]; opaque content[TLSCompressed.length]; opaque MAC[CipherSpec.hash_size]; uint8 padding[GenericBlockCipher.padding_length]; uint8 padding_length; } GenericBlockCipher;
圧縮を使わないとき(圧縮方式が null のとき)は、TLSPlaintext を変更なしにコピーして、TLSCompressed を作成します。
必須の暗号スイート
TLS1.2の必須暗号スイートは、TLS_RSA_WITH_AES_128_CBC_SHA です。これは以下のように解釈します。
HTTP/2で必須のTLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256は、TLS 1.2 以降で使え、以下のように解釈します。
ECDHE と DHE
HTTP/2 では、TLSの鍵交換は短命楕円 Diffie Hellman に加えて、短命 Diffie Hellman (DHE)でもよいとされています。RSAに変わって、これらが要求されるのはなぜでしょうか? これにも広域監視が影響しています。
広域監視が、あるサーバのTLSの通信を長年に渡って保存していたとしましょう。そのサーバは鍵交換にRSAを使っていました。あるとき、サーバ機器の入れ替えがあり、そのサーバが破棄されたのですが、ディスクを破壊することを忘れました。このディスクを広域監視をしている人達が入手したとすると、RSAの秘密鍵を取り出せます。そして、記録した TLS の通信を復号化し、過去すべての通信内容を見ることができるようになります。
これに対抗するには、前方秘匿性(forward secrecy)を備えた鍵交換を利用する必要が有ります。短命楕円 Diffie Hellman や短命 Diffie Hellman の「短命」とは、この前方秘匿性を有していること表しています。これらの鍵交換では、一時的な秘密鍵(と公開鍵)を作り使い捨てます。そのため、ある時点での秘密鍵が漏洩しても、過去すべての通信が復号化されるという危険性はありません。
今年話題になった Heartbleed では、秘密鍵が漏洩される危険性が指摘され、実際に Heartbleed challenge で、秘密鍵が漏洩されることが実証されました。そもそも、個人的にはネゴシエーション以降利用しない秘密鍵をメモリ中に保存している実装がおかしいとは思いますが、世間的には秘密鍵の漏洩も実際に起こりうることをこの事件は印象付けたようです。
AES GCM と AEAD
上記のように TLS では、CBCモードのブロック暗号(GenericBlockCipher)とストリーム暗号(GenericStreamCipher)が定義されています。
TLS 1.0 以前のCBCモードに対する有名な攻撃としては、BEASTがあります。また、これまでの「MAC後暗号化」方式は脆弱であり、「暗号化後MAC」を使えというRFC7366も発行されています。さらに、これは SSL 3.0 に限定的な話ですが、POODLE という攻撃方法も発見されてしまいました。
一方、ストリーム暗号の実質上唯一の選択である RC4 にはたくさんの脆弱性が見つかっています。また、RC4の利用を禁止しようというドラフトもあります。
TLSを策定している人たちの間には、再ネゴシエーションや圧縮に加えて、ストリーム暗号やCBCモードのブロック暗号もダメなのではないかという空気があるようです。残された希望は、第三の暗号化 AEAD(Authenticated Encryption with Associated Data) です。
AEADとは、暗号化と同時に認証もする暗号方式の総称です。そのような AES のモードとしては、GCM (Galois/Counter Mode) や CCM (CBC MAC Mode) があります。TLS 1.2 では、AEAD 用の書式として GenericAEADCipher が定められています。
struct { ContentType type; ProtocolVersion version; uint16 length; select (SecurityParameters.cipher_type) { case stream: GenericStreamCipher; case block: GenericBlockCipher; case aead: GenericAEADCipher; } fragment; } TLSCiphertext; struct { opaque nonce_explicit[SecurityParameters.record_iv_length]; aead-ciphered struct { opaque content[TLSCompressed.length]; }; } GenericAEADCipher;
暗号文自体に認証データが埋め込まれているため、GenericStreamCipher や GenericBlockCipher には存在する MAC が、GenericAEADCipher にはないことに注意しましょう。
暗号強度
最も弱い環の原則を知っていますか? "A chain is no stronger than its weakest link" という英語の諺があります。全体の強度は、一番弱い部品によって決まるという意味です。全体の強度を上げるには、一番弱い部品の強度を上げなければならない。これが、最も弱い環の原則です。
暗号システムを構築する際も、弱い部品があれば、全体の強度がそれに押し下げられてしまいます。そこで、すべての部品の強度を合わせないといけません。この指標が暗号強度であり、共有鍵暗号のビット数を使って表されます。
以下、「デファクトスタンダード暗号技術の大移行(5)」より暗号強度の表を抜粋します。
暗号強度 | 80 | 112 | 128 | 192 | 256 |
共有鍵暗号 | 80 | 112 | 128 | 192 | 256 |
RSA | 1024 | 2048 | 3072 | 7680 | 15360 |
Diffie Hellman | 1024 | 2048 | 3072 | 7680 | 15360 |
楕円曲線暗号 | 160 | 224 | 256 | 384 | 512 |
ハッシュ | 160 | 224 | 256 | 384 | 512 |
もう一度、HTTP/2が要求する暗号スイート「TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (楕円曲線は P256)」を眺めて下さい。128ビットの暗号強度を要求していることがわかります。
この表から、Diffie Hellman よりも楕円曲線暗号を使った Diffie Hellman の方が、短いビット数で済むことが分かるでしょう。これが、短命 Diffie Hellman ではなく、短命楕円 Diffie Hellman が必須となっている理由です。
なお、楕円曲線のよいパラメータには(P256のような)名前が付いており、TLSのネゴシエーションのときに名前を指定するだけよいようになっています。一方、Diffie Hellman では、よいパラメータが定義されているものの、それが周知されていなかったり、TLSで使うような名前は定められていません。この現状を打開するためのドラフトも存在します。
TLS 1.3
TLS 1.3 では、再ネゴシエーション、圧縮、ストリーム暗号、そして CBCモードのブロック暗号が削られる予定です。このすっきりしたデータ構造を見て、うっとりしましょう。
struct { ContentType type; ProtocolVersion version; uint16 length; opaque fragment[TLSPlaintext.length]; } TLSPlaintext; struct { ContentType type; ProtocolVersion version; uint16 length; opaque nonce_explicit[SecurityParameters.record_iv_length]; aead-ciphered struct { opaque content[TLSPlaintext.length]; } fragment; } TLSCiphertext;
おまけ
SSL 2.0 の利用は推奨されていません。その理由は RFC6176を読んで下さい。また、SSL 3.0 の利用を推奨しなくするためのドラフトも存在します。