ぼちぼち日記

おそらくプロトコルネタを書いていることが多いんじゃないかと思います。

錆びついたTLSを滑らかに、GoogleによるGREASE試験

0. 短いまとめ

  • 長い間、TLSのクライアント・サーバ間で使用するTLSバージョンを合意する際に、 不完全なサーバ実装によって version intolerance が発生することが問題になっていました。
  • TLS1.3ではこの version intolerance の影響を最小化するため、新しい version negotiation の仕組みを取り入れました。
  • Googleは、GREASE(Generate Random Extensions And Sustain Extensibility)という仕様をChromeに実装し、TLSサーバのバグで通らない拡張やフィールド値で問題が発生しないか試験を始めました。
  • パケットキャプチャが好きな人は、Chromeが 0x[0-f]a0x[0-f]a の見慣れない値をCipherSuiteやTLS拡張に使っているのを見つけても驚かないよう気をつけて下さい。

1. はじめに

 確か半年前も同じような事を言っていましたが、TLS1.3の仕様策定が大詰めを迎えています。
しかし大詰めの段階に来ているにも関わらず、最近のドラフト(draft-15/16)になって、これまでのTLS仕組みを大きく変えるような変更がいくつか導入されました。

この大きな変更は、Netscape社のSSL3時代から20年近く、これまでずっと積み重なってきた技術負債をできるだけここで一掃したいという思いの現れです。しかし一方で、ガチガチにミドルボックスで縛られた現状のインターネットと不完全な実装を持つ多数のTLSサーバの影響を考えるとそうそう一筋縄ではいきません。事情や経緯を知らない人がこの変更点を見るとなかなか理解しがたい仕様になっているでしょう。

今回は、TLS1.3で採用された大きな変更のうち、 Version Negotiation について取り上げたいと思います*1。そしてGoogleは、こんな苦労をもうしないよう、将来のTLSバージョンアップや機能拡張に備えるべくGREASEという仕様ドラフトを提出しました。既にBoringSSLやChromeに実装され、先週よりChrome Canaryで試験運用を始めました。これについても詳しく紹介します。

2. TLS Version Intolerance

 TLSのクライアントとサーバでどのTLSのバージョンを使うのか? この合意(Version Negotiation)は、TLSハンドシェイクの初期で行う非常に重要なステップです。しかし、これほど基本的で重要な機能なのに、現実のTLSの Version Negotiation は不完全で、随分昔(2003年以前ぐらい)から問題が発生していることが知られています。これを Version Intolerance と呼んでいます。日本語だと「バージョン不耐性」でしょうか?なんかよい訳が思い浮かびません。

ちょうど昨日Mozilla のTim Taubert氏のブログ

TLS version intolerance - Working around bugs in legacy TLS stacks - Tim Taubert

で version intoleranceが詳しく取り上げられました。そこで紹介されている Bulletproof TLS Newsletterを書かれているHanno Böck氏のプレゼン資料

TLS 1.3 and Version Intolerance

にも詳しく解説されています。ここに敢えて私が付け加えるようなことはないのですが、後の説明がわかりやすくなるよう簡単に図にして解説してみます。

2.1 TLSの正常な Version Negotiation

 TLSのVersion Negotiationは、ClientHello/ServerHelloのハンドシェイクの最初のやり取りで行います。クライアントは自身がサポートしている最高のTLSバージョンをサーバに伝えます。サーバは、自身がサポートしているTLSバージョンからクライアントがサポートしているものを選んで返します。

TLS1.2のクライアントとTLS1.2のサーバ間では簡単で、ClientHelloとServerHello供にTLS1.2のバージョンをやり取りして合意して完了です。

f:id:jovi0608:20161002085404j:plain

一方、クライアントとサーバ側のサポートが違う場合(サーバ側がTLS1.1までサポートしていない時)、サーバはClientHelloでTLS1.2を受信しますが、TLS1.2をサポートしていないので、ServerHelloでTLS1.1をクライアントに返します。クライアントはそれを受けて、クライアント・サーバ間でTLS1.1を合意します。

f:id:jovi0608:20161002085419j:plain


実はこれをちゃんと実装していないサーバが多く存在していて問題になっています。

2.2 不完全な実装のTLSサーバによる Version Intolerance

 何がちゃんと実装されていないのでどういう問題が起きるのかは様々ですが、一例としてよく挙げられているのは、サーバが自身がサポートしていないTLSバージョンのClientHelloを受信すると接続を切断してしまうというという実装の問題です。

Fallbackをサポートしているクライアントは、Version Intoleranceの問題でClientHelloが切断されるとサーバにオファーするバージョンを下げてもう一回接続に行きます。これを繰り返してサーバがサポートしているバージョンまで合致すると、やっとハンドシェイクが進みます。

f:id:jovi0608:20161002085436j:plain

最終的に接続できるから良いかと思うかもしれませんが、何回も初期接続を試みるのでTLS通信が確立するまで時間がかかります。なによりクライアントは本当にサーバが切断したのか判断つかないので、もし中間攻撃者によって意図的に切断されてダウングレードさせられているのかもしれません。もし脆弱性のあるTLSバージョンまでダウングレードさせられると問題です。

TLS1.2が仕様化された後、このような状況がいくつか見られました。その結果、幾つかのブラウザベンダーは、TLS1.2の対応サーバがある程度普及するまでデフォルトでTLS1.2の通信をオフにしなければなりませんでした。

3. TLS1.3はやっぱり通らない

 TLS1.3でも同じ状況が起こることが懸念されてました。SSLLabでは2012年4月よりTLS protocol torelance を測定しています。その詳しい内容が、

TLS Version Intolerance in SSL Pulse – Network Security Blog | Qualys, Inc.

で公開されています。ここでその図を引用させてもらいます。

f:id:jovi0608:20161002085451j:plain

当初SSLlabは、TLSのRecord LayerとClientHelloの両方共に同じバージョンでサーバに対してTLS1.3とTLS2.152のProtocol Intoleranceをチェックする試験を行っていました。TLS1.3では12%、TLS2.152では60%以上のサーバが接続を拒否しています。この数値は悲惨です。

しかし2015年5月に record layer のバージョンをTLS1.0に固定した試験に変更すると急激に下がりました。2016年7月時点ではTLS1.3で3.2%のサーバが version intolerant であると統計結果が出ています。実はClientHelloのrecord layer中のバージョン指定には明確な規定がないですが、record layerのバージョンのtoleranceは大変厳しいのがわかります。TLS1.3では record layer自体はもう意味がなく、互換性のためだけに付けておく無駄な5バイトになっています。なのでTLS1.3の record layerのバージョンはTLS1.0(0x0301)に固定されました。

Googleも独自に go 実装で Alexaのトップ100万のサイトを調査しました。その結果、1.63%のサイトがTLS1.3のClientHelloを切断したということです。そうなると3.2%〜1.6%がTLS1.3の version intoleranceの見積もりになります。この数字が大きいか小さいか、判断が別れるところです。

その数パーセントの中に大規模なアクセスを受けているサイトがあれば、ユーザへの影響は大きいでしょう。TLS1.2までは問題なく動作していたということで、TLS1.3にしてまず真っ先に問題を疑われるのはクライアント側になります。

このままではTLS1.2の時と同様にTLS1.3の仕様化完了してもしばらくは default off にし、時間を掛けてTLS1.3を deploy していかないといけないかもしれません。できればそれは避けたいところです。

4. 新しいTLS1.3のVersion Negotiation

 そこで TLS1.3では、新しい Version Negotiationの仕組みを採用しました。

それは従来のClientHelloで指定するバージョン番号を legacy_versionとしてTLS1.2(0x0303)で固定し、その代わり新しいTLS拡張でSupported Versionsを規定してそこにTLS1.3のクライアントがサポートするTLSバージョンのリストを記載します。TLS1.3のサーバは、ClientHelloのProtocol Versionフィールドを無視して、Supported VersionsのTLS拡張の方を見てTLSのバージョンを選びます。

f:id:jovi0608:20161002085504j:plain

従来のTLS1.2までのサーバは、Supported Versions拡張を知らないので、これまでのTLS1.2のClientHelloが来たと思って騙されて処理を継続します。これならTLSの新バージョンの導入による version intoleranceの影響を最小限にすることができます。
Googleの測定ではこの方式にすると 0.017% までintoleranceが落ちるという結果がでました*2。これなら十分いけます。

 

しかし、TLS1.3の仕様をよくよく見返してみると、TLS1.3の ClientHelloフィールドのうち半分が実際には不要で互換性維持のためだけに存在する固定値になってしまいました。あぁデザインがきれいじゃない。

f:id:jovi0608:20161002085517j:plain

まぁなんとも言えないもどかしさです。しかしClientHelloのフォーマットを維持してデータ形式上TLS1.xとの互換を保たないとTLS透過性はもっと悲惨なものになるでしょう。そしてClientHelloのProtocolVersionを固定化し、従来のTLSのVersion Negotiationを捨て去ることは、TLS1.3一回限りではなく今後TLS1.4以降でもずっと続く話になります。大きな決断をすることになりました。

5. 実際のTLS1.3のハンドシェイク

 ということで Supported Versionsを使ったTLS1.3のハンドシェイクを見てみます。既にChrome Canary で draft-15なんですがSupported Versionsが実装されたので実際のパケットを見てみましょう。
なお TLS1.3(0x0304)が使えるのは、最終的にTLS1.3の仕様化が完了した後からです。draft段階での相互接続試験においては、使用するバージョンは1オクテット目を0x7f、2オクテット目をドラフト番号にしたバージョン番号を使います。今回Chromeは、まだ draft-15 なので 0x7f0e がバージョン番号になります。まずは ClientHelloから、

f:id:jovi0608:20161002085532j:plain

Record LayerのバージョンはTLS1.0です。ClientHelloのProtocol Versionは、TLS1.2。これで従来のTLS1.2までのサーバを騙します。Supported Versions拡張は43番が割り当てられています。そこにクライアントがサポートするTLSバージョンのリストが5つ記載されます。最初の 0x1a1a は後述するGREASEの値。それから draft-15のバージョン 0x7f0e, TLS1.2 0x0303, TLS1.1 0x0302, TLS1.0 0x0301 が続きます。一応記載順には意味を持たせないことになっています。

続いてServerHello、

f:id:jovi0608:20161002085548j:plain

Record LayerはTLS1.0で固定化したままです。Supported Versionsのリストからサーバが draft-15(07f0e)を選択して Protocol Versionに記載してクライアントに返します。これで TLS1.3の Version Negotiationの完了です。

6. バグで錆びたTLSを滑らかにするGREASE

こんな悲しいTLSの状況で頼みの綱はTLS拡張しかない。しかし楽観視できない、TLS拡張の処理にもバグが固定化して錆びついてしまう。

Googleの Adam Langlay氏は

ImperialViolet - Cryptographic Agility

において

There's a lesson in all this: have one joint and keep it well oiled.
(これについて解決法は、どこか一つを繋げて十分に潤滑させておくことしかない)

と書いています。そこでGoogleのエンジニアDavid Benjamin氏が、GREASE(Generate Random Extensions And Sustain Extensibility)

https://datatracker.ietf.org/doc/draft-davidben-tls-grease/

というドラフトを少し前に提出しました。日本語に訳すと「ランダムな拡張を生成して、拡張性を維持する」ということでしょうか。

これは、2バイトの0x[0-f]a[0-f]aの16個データからランダムにいくつか抽出し、毎回ClientHelloのCipherSuiteやTLS拡張や値にこのランダム値を入れ込んでサーバに送ってしまおうという仕様です。

現状のドラフトでは、CipherSuite値、ALPN拡張の値、supported group(TLS1.2ではEC group)の値、2つのTLS拡張(0バイト長、1バイト長)、supported versionsの値(TLS1.3のみ)の6領域を対象としています。

本来はTLSサーバが知らない未定義の拡張やフィールド値を受信しても基本無視して処理するのがTLSの仕様で求められる挙動です。GREASEは、未定義値を毎回ランダムに送信することによって、TLSサーバのバグを早期に発見し、将来拡張で利用する時に問題が発生するのを未然に防ごうという狙いです。

 

文字通りGREASEは、TLSの拡張やフィールド値の利用がバグ実装の固定化で錆びつかないようグリースを塗り続けるという比喩を表しています。

 

これも既に Chrome Canaryに実装されているので、実際にパケットを見てみるとよくわかります。Chome CanaryのTLS1.2のClientHelloを見てみましょう。

f:id:jovi0608:20161002085602j:plain

CipherSuiteの先頭に0x3a3aの見慣れない値が入っています。これがGREASEです。

 

ちなみに、続く0x16で始まるUnknownな CipherSuite は、Googleが只今絶賛検証中の耐量子コンピュータの鍵交換 CECPQ1 を使った CipherSuite です。これは、djb の考案した楕円関数 curve25519を使ったECDHE(x25519)と new hope というring-LWE方式の格子暗号を組み合わせた鍵交換方式です。これはこれですごく面白いのですが、解説するとめちゃくちゃ長くなるので、いつかの機会に。

 

続いてTLS拡張に入っているGREASEを見てみます。

f:id:jovi0608:20161002085614j:plain

0バイト長の0x2a2aの拡張が頭に1バイト長の0x1a1aのタイプ値を持ったTLS拡張が追加されています。他にクライアントがサポートする楕円関数の種類を広報する EC group 拡張の先頭にも 0xaaaa のGREASE値が入っています。
ALPNへのGREASEの実装はドラフトに記載されていますが、まだのようです。Supported Versionsに関しては、先の TLS1.3のCLientHelloを見てみれば Supported Versions拡張の先頭に0x1a1aのGREASEが入っているのがわかります。 

BoringSSLの実装では、これらのGREASE値をClientのRandomフィールドの頭の1バイトずつをシードにして生成しています(頭の4bit値)。よって、ClientRandomが変わると毎回値が変わることになります。

GoogleはGREASEによってどの程度問題が発生しているのか統計を取って、今から将来のTLSのバージョンアップや機能拡張に備えている試験を始めたわけです。

最後に、全国数X万人のパケットキャプチャ好きなエンジニアの諸君へ

今後ChromeTLSパケットに 0x[0-f]a[0-f]aの見慣れないUnknownフィールドや値を見かけたとしても驚かないように。それはGoogleが、インターネット中のTLSサーバが錆びつかないようグリースを塗っている様子なんです。

*1:この他にもCipherSuiteの構造も大きく変わっています。

*2:Googleは、ChromeのTLS1.3の事前試験でTLS1.3で必須となっている署名アルゴリズム(RSA-PSS)のTLS拡張値でNSSのバグを踏んでハンドシェイクが失敗することを見つけました。0.017%にはこのNSSのバグの影響によるものがいくつか含まれているようです。