2018年09月25日 火曜日

IIJ-II 技術研究所 技術開発室の山本です。現在技術開発室は、私を含めた4人で構成されており、主にプログラミング言語Haskellを使って開発を進めています。今回の話題である TLS(Transport Layer Security) 1.3 もHaskellで実装しました。

4年の歳月をかけて議論されてきたTLS 1.3ですが、この8月にめでたく仕様がRFC 8446となりました。貢献者リストに私の名前が載っていることを聞きつけた広報から、ブログ記事の執筆依頼がありましたので、TLS 1.3の標準化や実装の話について書いてみます。

なぜTLS 1.3を標準化する必要があったのか理由を知りたい方は、「TLSの動向」という記事や「TLS 1.3」というスライドを読んで下さい。

TLS 1.3の標準化

インターネットで使われているプロトコルは、IETFという団体で仕様が議論されて策定されます。IETFには、誰でも参加できます。

取り扱っているテーマはざまざまで、テーマごとに分科会(ワーキンググループ)が作られます。分科会は、プロトコルの仕様の草稿(インターネットドラフト)を書きます。草稿を元にして議論を続け、草稿を改定していきます。分科会で仕様に関して合意ができ、さらに最終的な検証に合格できれば、仕様がRFCとして公開されます。

TLS 1.3の草稿は、実に29回発行されました。それぞれの草稿で何が変わったか知りたい方は、「TLS 1.3の標準化動向」というスライドを見て下さい。私が参加したのは、草稿18のときです。118ページもある草稿なので、最初から丁寧に読んで行くと息切れしてしまいます。

そこでまず、ネットワーク上を流れるパケットの書式から理解しようと、Haskellでパーサーを書き始めました。パケットの書式は、以下のようにCライクな構文で定義されています。

struct { ProtocolVersion legacy_version = 0x0303; /* TLS v1.2 */ Random random; opaque legacy_session_id<0..32>; CipherSuite cipher_suites<2..2^16-2>; opaque legacy_compression_methods<1..2^8-1>; Extension extensions<8..2^16-1>; } ClientHello; 1 2 3 4 5 6 7 8 struct { ProtocolVersion legacy_version = 0x0303 ; /* TLS v1.2 */ Random random ; opaque legacy_session_id < 0..32 > ; CipherSuite cipher_suites < 2..2 ^ 16 - 2 > ; opaque legacy_compression_methods < 1..2 ^ 8 - 1 > ; Extension extensions < 8..2 ^ 16 - 1 > ; } ClientHello ;

パーサーを実装すると、パースできない定義に出会います。そう、書式の定義が間違っているのです。IETFでは、こういう書式を手書きして、機械的な検証はしないという文化です。そこで、パーサーを書けば、書式の間違いをたくさん発見できます。

パースした結果は、さらにHaskellのコードに変換しました。Haskellはよく設計された言語であり、コンパイラを通すことで、すべての場合が網羅されているかを検査できます。この一手間を加えることで、重複や漏れを発見できます。

草稿が更新されるたびに、新しい間違いが紛れ込むので、あるときついに私は叫びました：

オレが TLS 2.0 syntax validator だ。(半分本気) — 山本和彦 (@kazu_yamamoto) September 7, 2016

注：このころ、次のTLSのバージョンは2.0になる予定でしたが、最終的には1.3が選ばれました。

昔のIETFで使われていたツールは、メールだけでした。このため問題を指摘しても、忘れさられてしまうことがたびたび起こりました。最近では GitHub が活用されています。問題点は issue として記録されます。私は発見した誤りを直すプルリクエストを度々送りましたし、分かりにくい部分に関しては改善案を付けて issue に登録しました。

こういうことを繰り返しているうちに、TLS 1.3 の編集責任者である Eric さんから「貢献者リストに乗せるから、自分で名前を追加してプルリクエストを送って欲しい」と言われたのです。また、貢献者の一員としてTシャツもいただきました：

TLS 1.3 Tシャツ

TLS 1.3の実装

私のTLS 1.3の実装ですが、Haskellで書かれたTLSライブラリを拡張する方法を取りました。このTLSライブラリは、SSL 2からTLS 1.2までをサポートしていました。

TLS 1.3は仕様が簡潔であるため、暗号の部品がそろっていて、かつ TLS1.3だけを作るなら、それ程難しくないと思います。難しいのは共存です。SSL 2からTLS 1.2まで、各バージョン間の仕様の違いは大きくなく、差分を吸収するコードは小さくて済みます。しかし、TLS 1.3は、それまでのバージョンとはまったく異なるプロトコルです。TLS 1.3は最終的にTLS 1.2などと共存しやすい仕様に落ち着きましたが、草稿18のころは大変でした。

TLS 1.3は最新の暗号技術を使います。「暗号の部品がそろっていて」と書きましたが、私は既存の暗号ライブラリ(cryptonite)に手を入れることから始める必要がありました。

TLS 1.3の草稿は頻繁に変わりますので、それに追従するのも大変です。標準化の最終場面では、OpenSSLとHaskell TLSが最新の草稿を最初に実装して相互接続性を検証し、仕様の誤りを発見するのが恒例となっていました。

相互接続性の検証にも時間がかかります。私は、OpenSSL、BoringSSL、NSS、および picotlsに対して、4つのハンドシェイクモードをテストしていました。草稿が変わるたび、また各実装が大幅な修正を入れるたびに、相互接続性を確認しました。通信ができなくなったら、そのコードを書いた人と議論し、何が間違っているのかを特定しました。

これらの作業は根気はいりますが、自分が頑張ればよいという意味では気が楽です。自分だけではどうしようもないのが、マージです。本家にマージしてもらうには、気に入ってもらえないといけません。

私がマージに対してとった対策は、以下の通りです。

TLS 1.2でも利用できる改造は、個別に切り出して、早めにプルリクエストを送る

最終的な大きな差分を意味のあるパッチ群に再構成し、複数のプルリクエストにして送る

レビュアーにとって、大きなパッチは理解するのが大変ですので、なるべく小さくする必要があります。また、そのコードがどういう風に紆余曲折して作られたかにも興味はありません。紆余曲折した歴史はばっさり捨て去って、レビュアーに分かりやすいように再構成するのが肝要です。文章を人に見せる前には推敲するように、コードはレビューしてもらう前には推敲するのが大切だと思います。

レビュアーであるOlivierさんには丁寧にレビューしていただき、たくさんのコメントをもらいました。一つ一つ対応して、すべてのプルリクエストがマージされました：

My TLS 1.3 implementation has been fully merged to the TLS library in #Haskell.

Yay! — 山本和彦 (@kazu_yamamoto) September 12, 2018

実際のプルリクエストが見たい方は、TLSライブラリのissue 282を見て下さい。

実装の現状

執筆時点で、Firefox の安定バージョン62はRFC8446のTLS1.3には対応していませんが、次の63から対応するようです。Chrome も同様に、次のChrome 70から対応するとのことです。それぞれNightlyやCanaryでは対応していますので、待ちきれない方はインストールしてみてください。

テストサーバとして、私のサイト(https://www.mew.org/)を使っていただいて構いません。私が書いた TLS 1.3 のコードが動いています。ブラウザが、どのTLSのバージョンを使ったか調べるには、以下のようにします。

Firefox: サイトにアクセスした後、鍵マーク → “>” → “More Information”

Chrome: 開発者ツールを表示してサイトにアクセスした後、“Security”

以前にTLS 1.2で接続した場合は、ブラウザがそれを記憶している可能性がありますので、何回かリロードする必要があるかもしれません。

メジャーなブラウザの対応により、クライアント側ではTLS 1.3は急速に普及するでしょう。問題は、サーバ側ですね。サーバ側でよく使われている OpenSSLですが、TLS 1.3 をサポートしたバージョン1.1.1がリリースされました。

また、WireSharkも少なくともバージョン2.6.3ではTLS 1.3をサポートしています。Firefox/Chrome/OpenSSLに出力させたキーログファイルを指定すれば、TLS 1.3で暗号化された通信をWireSharkで復号化できます。参考までに、キーログファイルの例を載せておきます。