ぼちぼち日記

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

io.js-v1.0.0のリリースによせて

1. 祝 io.js-v1.0.0/1.0.1 のリリース

NodeやJSの情報にアンテナを張っている人なら知っているとは思いますが、昨日無事「io.js」がリリースされました。
リリース直前のバグ修正の追い込みやライブラリアップデートのごたごたは、かつてのNodeのリリースそのままでした。

今日のリリースを予言していたわけではないですが、実は昨年の8月初旬に、

ということを書いていました。
これがまさに現実になってしまったなぁ、と驚きと共に感慨深いものがあります。
Nodeの方は、今Julienが頑張ってチケットをクローズしており、近日中に Node-v0.11.15がリリースされる予定です。問題なければNode-v0.11.15のリリース2週間後にNode-v0.12 になるでしょう。

io.jsができた細かい経緯については、古川さんの「io.jsについて知っていること」が詳しいです。ただ長年Nodeコアの開発を見てきたこともあり、自分の目線で今回のio.jsリリースまでを振り返りたいと思います。

ただし今回のio.jsのフォーク騒動を当事者ではなく完全外から見ていた人なので、完全に自分の主観で書いています。当事者間でないとわからない部分も多々ありますので、その点ご留意してお読みください。

2. V8更新から見たio.jsフォークの経緯

なぜ8月頭にフォーク話を書いてしまったのか?
当初Node-v0.12で予定されていた機能の大部分は昨年4月頃に既に実装されていたのに遅々としてリリースされない状況を見てそう感じました。

今回改めてNodeでのV8の扱いを軸に時系列で書いていくとio.js がフォークに至った経緯などがよく見えてきます。

象徴的な部分、上表で注釈1,2で書いた部分は以下に書かれている項目です。

TJFは、Node on the Roadイベントで各地を回り、これまでのNodeのバージョンアップに伴うユーザの大変さを理解したと綴っています(Notes from the Road)。このような声を受けて、NodeのV8の対応(特にES6対応)には当初保守的に考えるようになったのでしょう。

対して技術志向を持つメンバーには、このような保守的な考えはなかなか受け入れにくいものだったと想像できます。進化し続けるV8にNodeが追従していくのは、バグフィックス等の安定性や性能の向上に寄与し、さらにブラウザでデフォルトで利用される予定のES6の新機能がタイムリーに利用できるという考えではなかったでしょうか。

V8の扱いは一つの要因で、他にもいろいろ理由はあると思いますが、こういった方向性の違いからio.jsが生まれたのではないかと思っています。

3. io.js-v1.0.xとNode-v0.12の相違

io.jsは v1.0.x なのに、なんでベータ扱いやねんと不思議に思われる方もいるでしょう。semverでは、 Public APIを規定した1.0.0からバージョンが始まります。今後 iojs-v1.0.x はパッチレベルの修正、iojs-v1.1.x は後方互換を保った変更、iojs-v2.x.y は後方互換を止めた変更といった形で開発が進むものと思われます。

3.1 iojs-v1.0.1 が Node-v0.12 と同じところ

io.js-v1.0.0 は、Node-v0.12 の Public APIと完全互換でリリースされ、 semver に従ってNode-v0.12との互換性が保たれる方針です。ただ現時点ではio.js の方が開発スピードが格段に速いため、バグフィックスなどによる挙動の違いなどが現れる可能性があります。

また io.js は、Node-v0.10系とは互換でないので気を付けてください。0.10系で既存のNodeアプリを使われている方は、 Node-v0.12(Node-v0.11.15)でも動作するか確認が必要です。

io.js のCHANGELOGは、Node-v0.10.35からの変更点が記載されています。CHANGELOGで書かれている部分の半分ぐらいは下記スライドで解説していますので、参考にしてください。

その他のCHANGELOGで書かれていることなど、いつになるかわかりませんが時間があるときにでもまとめられたらと思ってます。

3.2 iojs-v1.0.1 が Node-v0.12 と異なるところ

現状で表面的に異なる部分は、

  • V8: iojs-v1.0.1 は V8-3.31 ベース(Node-v0.12は V8-3.28)。外部的にはデフォルトで使えるES6の機能が異なります。
  • event モジュール: iojs に getMaxListenrs() メソッドが追加
  • domain モジュール: domain.run() に引数が使える。ドキュメントに廃止予定の告知
  • v8 モジュール: v8のヒープ情報や動的フラグ設定などの機能を追加(実験的API)

などです。

当初io.jsでは、fs.existや.fs.existSync のAPIを廃止していましたが、 Node-v0.12とのAPIとの互換性保持のためリリース直前に復活しました(でも今後廃止予定です)。 他にもマニュアル、バグフィックス、最適化、ビルド対応など細かい部分が io.js で変更されています。

4. で今後どうなる?

うーん、わかりません。
現状では、Joyent Node側は、リソースを一緒にして協力したい。 io.js側は Open Governanceモデルで迅速に開発を進めて、将来的に io.js から Node (1.0)へマージして欲しいという感じです。今後コミュニティがうまく両者が両立して使い分けられるのかにかかっているでしょう。良い形で両者が協力できるよう落ち着くことを祈るばかりです。

いずれにせよ、今回の io.js-v1.0.0のリリースと近日のNode-v0.12のリリースで両者の開発が短期間で大きく前進したのは確かです。個人的には、今年半ばに仕様化完了が予定されているES6の機能が充実していくことによって、io.jsやブラウザのJavaScriptの世界がどのように変わるのか非常に楽しみです。

Node-v0.10.34がはまったクロスルート証明書とOpenSSLの落とし穴

既に12月22日ですが、このエントリーは、Node.js Advent Calendar 2014の13日目のエントリーです。
いや私が書くの遅れたわけじゃないですけど…(言い訳)、ちょうどタイムリーなネタがあるので、先日リリースされたNode-v0.10.34で発生した(現在も継続している)問題について携わった経緯を自分の目線で書いてみます。

追記:日本時間の12/24にNode-v0.10.35がリリースされました。 http://blog.nodejs.org/2014/12/23/node-v0-10-35-stable/ 本記事の不具合も修正されています。

1. Node-v0.10.34リリース直後にissue発生

先週12/17にNode v0.10.34 (Stable)がリリースされました。10月中旬にPOODLE騒ぎでOpenSSLに対応した Node-v0.10.33 からおよそ2か月弱経ってのリリースです。

実は今回のリリース、Node-v0.10系のStable版のリリースですけど、自分的にはちょっとでかい変更をNodeにコミットしていたので、ここ1カ月程Nodeのコアチームといろいろやり取りを続けていました。心配性で、リリース後に自分の変更でなんか問題が出たらまずいなぁ、なんかあったらすぐ対応しないとあかんなぁ、と思っていたのでNode-v0.10.34のリリースは、いつもより注視せざる得ませんでした。

そんなところ、日本時間の12/18(木)の朝に無事Node-v0.10.34がリリースされました。

「やっとリリース、良かった」、「あんな苦労したのにこんなシンプルなリリースアナウンスかよっ」って関係者とDMを交わしつつリリース後の様子を見ていたところ、直後に githubに「node v0.10.34 causes untrusted cert errors #8894」という issueが飛び込んできました。

おっ、なにやら npm install phantomjs でエラーが出ているとのこと。試してみるとAWS の S3 サービスへのTLS接続で認証エラー(信頼されないサーバ証明書であるエラー)が発生しているようです。噂によるとTravis CIもなんか新Nodeで悲鳴をあげているらしい。
で、自分に関係あるかわからないけど早速調べることにしました。なによりまずは、エラーの再現コードの作成です。AWSのS3サーバにTLSで接続してみます。

// tls_s3.js: tls error check to s3.amazonaws.com
var tls = require('tls');
var client = tls.connect(443, 's3.amazonaws.com', function() {
  console.log('TLS connected');
  client.end();
});
client.on('error', function(err) {
  console.log(err);
});

そして新旧2つのNodeで動作結果を比べてみると、

$ ~/tmp/oldnode/node-v0.10.34/node tls_s3.js
[Error: CERT_UNTRUSTED]
$ ~/tmp/oldnode/node-v0.10.33/node tls_s3.js
TLS connected

うぅ、確かに新しいNode-v0.10.34だけエラーになっている。あかんわー、Node-v0.10.34。

実は「これ俺の変更箇所によるものじゃないよねー」ということが分かり少しほっとしちゃいました。でもAWSサービスへのTLS接続が軒並みエラーになるのは、こりゃ影響でかいでしょう。しばし調査を継続です。

2. エラーの原因は即判明、だが謎が残る

コミットログからエラーの原因が、「crypto: update root certificates」の変更にあるのは明白でした。

Nodeは、Mozillaが管理しているルート証明書データをNodeのソース内に埋め込んで利用しています。
今回、ちょうどNode-v0.10.34でこのルート証明書のデータを更新したので、それが問題を発生させたのです。おそらくAWSのS3サーバの証明書を発行したルート証明書が削除されているんだとすぐ予測できました。

「でもちょっと待て、単純にこのコミットをRevertするだけじゃダメや。どうしてエラーが発生したのかいくつか疑問点がある。もうちょっと調べるべきだ。」

そう思い、githubにすぐコメントを書くのをやめました。

  • 疑問1:なんでMozillaのデータからS3サービスのサーバ証明書を発行したroot CAが削除されちゃってるの?

これはすぐわかりました。Node-v0.10.34から1024-bitのRSA鍵長を持つルート証明書が削除されているからです。
Mozillaからは既にアナウンス「Phasing out Certificates with 1024-bit RSA Keys」が出ていました。このアナウンスは、1024-bitのRSA鍵長を持つセキュリティ強度の低いルート証明書をFirefox32から削除する予告です。既に今年の7月にこのルート証明書は削除され、AWSのS3サーバの証明書はこのルート証明書を元に発行されたのでしょう。そして今回ルート証明書までチェーンがたどれずにTLS認証エラーが出ていたと。openssl s_clientで見ると確かにこのルート証明書までチェーンが来ているふしがある。

でもちょっと待て、それならFirefox32がリリースされた9月頭にもっと騒動が起きているはず。なんで今になってNodeで問題になる? 試しにFirefoxでアクセスするとTLSの認証エラーは出ていない。

  • 疑問2:同じrootCAデータを使っているFirefoxでS3サーバにTLSアクセスしてもエラーがでない。なぜ??

うーん、こっちはすぐにはわからないー。そうこうしてる間に、US/CanadaにいるコアチームのJulienが気づき始めて返事が書き込まれました。僕の見立てと同じだ。io.jsをforkしたFedorもgithub上で問題切り分けに参加してきました。

3. 犯人はクロスルート証明書とOpenSSL

昼飯を食いながら Firefox のコミットログから bugzilla をたどっていくと、該当のVerisignのrootCAを削除した ticket でふとこんなコメントが目に入りました。
Bug 986005 - Turn off SSL and Code Signing trust bits for VeriSign 1024-bit roots
簡単に訳すと、

この変更から結局起きたこと:
https://lists.fedoraproject.org/pipermail/devel/2014-September/202127.html

端的に書くと、s3.amazonaws.com は古いルート証明書を指す中間証明書をまだ持っている。
NSSは、不必要な中間証明書を無視して、信頼できるG5のルート証明書へのチェーンを見つけ
ることができるけれども、他の証明書検証をするソフトは失敗するようだ。openssl はその
失敗するソフトの一つでしょう。
シマンテックは、これら古いルート証明書を指す中間証明書をサーバ設定から除くように
Amazonに連絡すべきだと思うし、他の顧客にも言うべきだと思う。

「おー、まさにこれだわー。OpenSSLの挙動の問題かよー、こりゃお手上げだ。」

そんな間に github 上で Fedor のコメントが進んでいます。英語での説明は時間もかかるし面倒くさいので、該当コメントのリンクとRevertじゃなくて1024-bitRSA鍵長を復活させるパッチを早速作って、github issueにコメントしました。

NodeのrootCA情報を格納するヘッダファイルは、実はcurlperl スクリプトを拝借したものです。削除されたrootCA証明書は、CKT_NSS_MUST_VERIFY_TRUST というレベルにカテゴライズされていたのでそれを含めたパッチです。まさか Node の修正で perl のパッチを書くとは思いもよらなかった(汗)…

後からちゃんと調べたんですが、bugzillaで書かれている「不要な中間証明書」は実はちゃんと使われる理由があったんですね。
この中間証明書は、クロスルート証明書と呼ばれるもので、2048-bitのRSA鍵長のrootCA証明書と1024-bitのRSA鍵長のrootCA証明書を両方とも有効にしてPKIの下位互換性を保つ用途で使われるものです。実際、S3のサーバの証明書チェーンは下図の様になっています。

NSSは上図の赤色のチェーンで検証を失敗すれば青色のチェーンにフォールバックするのに、OpenSSLは赤色のチェーンが失敗すればそのままTLSのサーバ認証がエラーになったままなんです。
つまりOpenSSLはクロスルート証明書の検証をちゃんと行えないのですね。OpenSSL弱いぞー。

4. やっぱ Fedor すげぇなぁ

まぁ自分としては、疑問1・2が解決したわけだし、OpenSSLの問題だから一時的に戻すしかないでしょと思い復活させるパッチを書いたんですが、直後にFedorから思いもよらぬ解決策が出てきました。
コメントを書いて数分後、Fedorから diff が示されました。
「へっ、OpenSSLのコードの diff? 直したっていうこと?」
実際試さないとコメント返せないやということで、急いでブランチ切り、パッチあて、ビルドし直し、テストコードを実行しました。
「おー!、エラーなし。」
私が問題の詳細を書いたリンクを示した直後にFedorがOpenSSLの該当箇所を特定しパッチを作成。そして私のテストがパス。

とつぶやいた瞬間でした(おぃおぃ、今ロシアは何時だよ)。

その後パッチはちょっと修正され、OpenSSLのバグトラッカーにパッチ
[PATCH] x509: skip certs if in alternative cert chain (http://rt.openssl.org/Ticket/Display.html?id=3637&user=guest&pass=guest)
が送られました。まぁいつものことですが、OpenSSLのチームからは今日まで返答なしです。実は私の別パッチも以前から放置状態のまま。OpenSSLをForkしたくなる気持ちもわかります。よくよく調べてみると今回と同じ現象も2年以上前に報告されて修正パッチも送られていましたが、放置されたままでした。(Bug: verification fails if muliple certification path (EV/Verisign) http://rt.openssl.org/Ticket/Display.html?id=2732&user=guest&pass=guest)

で、結局 Node ではv0.10に1024-bitのRSA鍵長のルート証明書だけ復活させることになりました。
src: re-add 1024-bit SSL certs removed by f9456a2 #8904
OpenSSLの修正をどうするかは今後の検討事項でしょう。次のNode-v0.10.35ではこの問題が直る予定ですので、Nodeのバージョンアップはしばらくお待ちください。他にもtimerのバグとかも修正されてます。

残念なのは Node-v0.11.15が翌日の12/18にリリース予定だったんですが、これらの問題の余波で延期になったようです。Node-v0.11.15で問題なければ2週間後の2015/1/1頃にNode-v0.12がリリースという計画は、おじゃん。来年以降に持ち越しです。

io.jsのFork騒動の中、今回のStable版リリース後のごたごたで、ホント Node-v0.12のリリースは一体全体どうなるのか、先行きはますます見えなくなりました。

Service WorkerとHTTP/2が切り開く新しいWeb Pushの世界

この記事は、HTTP2 Advent Calendar 2014の6日目のエントリーです(2日前にフライイング公開してます)。

1. はじめに、

HTTP/2仕様の標準化作業は、WGラストコールも終わり、今後IESGレビューやIETFラストコール等の大詰めの段階に来ました。来年のRFC化に向けてまだまだ予断を許しませんが、プロトコル設計自体の作業はほぼ完了し、後はすんなり行くことを祈るばかりです。

こんな状況なのに気が早いですが、もう既に標準化後を見据え、HTTP/2の機能を使った新しい仕組みを作る動きが始まっています。

そこで今回はHTTP/2技術の応用として、HTTP/2の「サーバプッシュ機能」と今ホットなブラウザの新技術「Service Worker機能」を組み合わせた次世代のプッシュ機能「Web Push/Push API」について書いてみたいと思います。
ただ、個人的に色々タスクがいくつか立て込んでいて、アドカレのブログを書いている場合じゃないとひどく怒られそうなので、手短に内容を書いていることをお許し下さい(担当者様ごめんなさい)。 と書きつつ長くなってしまった(汗)…

(注意事項) Web Push/Push API仕様ともに現在仕様策定中です。今後の仕様変更で本記事の内容が変わる可能性が大きいです。その点十分ご留意してお読みください。

2. Web Push の概要

まず初めにWeb Pushはどのような仕組みで行われるのか。その概要を一枚の図にしました。ちょっとごちゃごちゃ説明文を図に書いていますが登場人物は3人、

  • クライアント(ブラウザには、Webアプリケーション(Web App)、Service Worker、Push APIが動いています)
  • Webアプリケーションのサーバ(app.example.jp)
  • プッシュサーバ(push.example.jp)

です。

ざっとした流れは、

  1. クライアントからプッシュサーバの登録情報(endopoint, registrationID)をアプリサーバに通知。
  2. アプリサーバが、プッシュ通知をHTTP PUTのリクエストボディに付けてプッシュサーバに送信。
  3. プッシュサーバは、プッシュ通知をチャネルで区別し、送信クライアントを選定。
  4. プッシュサーバは、HTTP/2のサーバプッシュやGCM(Google Cloud Message)など利用してクライアントにプッシュ通知を送信。プロトコルは別にWebSocket/SSEでも構いません。
  5. クライアントは、プッシュサーバからプッシュ通知を受けると、Service Worker上でPushイベントが発生。ArrayBuffer, blob, json, textの形式でプッシュデータを取り出せるようになる。
  6. プッシュされたデータはキャッシュ更新なり、postMessageでDOMに渡したり、クライアント上でいかようにでも処理することができる。

といった段取りになります。*1

Web Pushは、クライアントとプッシュサーバ間をHTTP/2で接続し、クライアント上のService WorkerがHTTP/2サーバプッシュを介してプッシュ通知を受ける仕組みです。*2

次に、Web Pushで使われる2つの基本技術、HTTP/2サーバプッシュとService Workerについて軽く解説します。

3. HTTP/2サーバプッシュ機能とは、

SPDYやHTTP/2の目玉機能の一つに、サーバプッシュ機能が挙げられます。
サーバプッシュは、サーバ側がリクエストを受けるとその後に続くリクエストを先取りし、サーバ側から画像等のコンテンツを送り込む機能です。送り込まれたコンテンツは、クライアントのキャッシュ領域に保持されます。実際にサーバへリクエストが行われることなく、クライアントはキャッシュ領域からコンテンツが読み込みます。サーバプッシュによってクライアントのリクエストが減り、Webの表示が更に高速化されることが期待されています。

HTTP/2では従来のSPDYのサーバプッシュの仕組みを改良し、新しくPUSH_PROMISEというフレームが新設されました。このPUSH_PROMISEフレームは、サーバからコンテンツを送るストリームを事前に予約することができるため、SPDYより柔軟なタイミングでコンテンツを送り込むことが可能になりました。

サーバプッシュはHTTP/1.1の時代にはない新しい仕組みです。その効果は非常に期待されているのですが、まだSPDYでも実環境で十分使われているわけではありません。サーバプッシュ機能を十分に活用するのは大変で、これからGoogleTwitter等の大手がサーバプッシュを試験するページやアプリを増やしていくでしょう。もし chrome://net-internals/#spdy の出力で偶数番号のストリームを見かけたらサーバプッシュが使われているものと思ってください。今後どういう場面でサーバプッシュが効果的に使われるのか注目です。

4. Service Worker とは、

Service Workerは、最近動きのあるブラウザテクノロジーの中で最もホットな新機能の一つです。ネットワークプロキシとしても働くService Workerは、従来のAppCacheの欠点を克服し、真のオフラインファーストを実現できる技術として期待されています。
Service Workerの詳細を書くと記事の分量が膨大になってしまいますし、時間的にもちょっと無理です。英文ですがちょうど先日Service Workerに関するHTML5Rocksの記事が公開されましたので、興味のある方はこちら「Introduction to Service Worker」をお読みください。(誰かHTML5のアドカレで書いてくれないかなと期待してます)
各種ブラウザの実装状況等はこちらhttps://jakearchibald.github.io/isserviceworkerready/で見ることができます。

そんな今大注目の Service Workerですが、実はAppCacheの代用用途だけでなくバックグラウンドのコンテンツ同期やプッシュ通知などの応用が検討されています。しかもService Worker の通信は、セキュリティ上の観点からHTTPS通信が必須化されています。これはHTTP/2の利用環境としては最適です。さらにHTTP/2の仕様上では明示的にストリームのタイムアウトが規定されていないため、同時にオープンできるストリームを調整することによってプッシュサーバの大幅なスケールアップが図れます。
そういう背景から、HTTP/2のサーバプッシュ機能とService Workerを組み合わせた次世代のWeb Pushの仕組みの検討が始まりました。

5. Web Pushの背景とユースケース

5.1 Web Pushの標準化が始まる

今年の10月にIETFのraiエリア(Real-time Applications and Infrastructure)でwebpush ワーキングループ(Web-Based Push Notifications)が新設されました。
このWGの元々の始まりはWebRTCで相手にプッシュ通知を行う仕組みがないという動機からでした。その要件としてWebRTCのアプリが起動している時はもちろん、アプリのフォーカスが外れていたり未起動な場合、ネットワーク通信が切れている時など様々な場面に有効にプッシュ通知が働く機能が求められます。
この提言は大きな賛同を得られました。しかし、この要件は特にWebRTCだけに必要なものではないため、より一般的なWebアプリのプッシュ通知の仕組みを作ろうということなりこのWGが作成されました。
現在、プッシュ通知のプロトコル部分は IETF の webpush WGで、W3Cではブラウザ側のAPI仕様を検討する Push APIという役割分担がされています。Push APIの議論の様子や仕様ドラフトは https://github.com/w3c/push-api/https://w3c.github.io/push-api/ で見ることができます。

5.2 想定される Web Push のユースケース

では、Web Push はどういう場面で使われることを想定して仕様策定が進むのか?
Push APIの仕様ドラフトに記載されているユースケースを読んでみるとだいたいイメージが付きます。簡単に訳してみると、

  1. ユーザが現在webappを利用中で webappからプッシュ通知を受け取る場合
    • これは通常の使い方
  2. ユーザがwebappを利用していないが、ブラウザウィンドウやWeb Worker内でwebappが実行されていてプッシュ通知を受け取る場合
    • SNSやmessage,web feed等のプッシュ通知を受け取るような場合を想定。
  3. ブラウザウィンドウ内でもwebappが利用されていないが、プッシュ通知を受けたらwebappを起動できるようなことがあると良い場合
    • WebRTCの入電コールでWebRTCのwebappを立ち上がるような場合を想定。
  4. 複数のwebappが実行されていて、そのうち要求されたアプリにだけプッシュ通知をしたい場合
  5. 一つのブラウザ内で同じwebappが複数のインスタンスで実行されていて、特定のインスタンスに対してプッシュ通知をしたい場合
    • 複数のメールアカウントを違うウィンドウで立ち上げているような場合を想定。
  6. 異なるブラウザで同じwebappが複数のインスタンスで実行されていて、複数の特定インスタンスに対してプッシュ通知をしたい場合
    • 同一の電子メールアカウントを2つのブラウザで立ち上げているような場合を想定。
  7. 異なるブラウザで同じwebappが複数のインスタンスで実行されていて、全てにプッシュ通知をブロードキャストしたい場合

と7つのユースケースが挙げられています。
Web Pushは、複数のブラウザ、複数のアプリインスタンス、アプリの起動・停止中等様々な場面でもプッシュ通知が行えることが想定されています。

6. Service Workerで使うPush API

まずブラウザ側のフロントエンド開発者が実際に触るクライアント側のPush APIがどうなるのか見てみます。
大きくService Workerへのプッシュ機能の登録(register)、アプリサーバへの登録情報の通知(distribution)、プッシュ通知の受信の3つに分かれます。
Push API仕様に記載されているサンプルコード例をちょっと変えたものが下記です。Service WorkerはPromiseインターフェイスを持つので割と見やすく非同期処理が行えます。

// https://app.example.jp/serviceworker.js
this.onpush = function(event) {
  console.log(event.data);
  // ここでIndexedDBにdataを書き込んだり、開いているウィンドウにdataを送ったり、
  // Notificationを表示したりなどができる。
}

// https://app.example.jp/webapp.js
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
  serviceWorkerRegistration.pushRegistrationManager.register('/serviceworker.js').then(
    function(pushRegistration) {
      console.log(pushRegistration.registrationId);
      console.log(pushRegistration.endpoint);
      // ここではアプリケーションサーバがプッシュサービスへプッシュ通知を行うのに必要
      // な登録情報を利用できるようになりました。例えばXMLHttpRequest等を使って、
      // その登録情報をアプリケーションサーバに送信します。
    }, function(error) {
      // 開発中はコンソールにエラーログを出し修正の手がかりにしたりします。
      // サービス環境では、エラー情報をアプリケーションに通知したりするようにするのが
      // いいのかもしれません。
      console.log(error);
    }
  });
});

流れとして、

  1. pushRegistrationManager.register()を使ってServiceWorkerにプッシュ機能の登録する。
  2. 登録が成功したら endpoint(プッシュサーバのURL)と registrationIDが付与されます。これをアプリサーバ側にAjax等で送る。(この部分はコメント記述のみ)
  3. Service Worker側はプッシュ通知を受けると、Pushイベントが発生します。
  4. Pushイベントオブジェクトのdataプロパティに受信したデータが格納されています。 ArrayBuffer(), blob(), json(), text() のメソッドを使ってアプリサーバから送られた形式に変換します。

Push APIの仕様ではプッシュサーバの登録時には何かしら確認UIが出て、勝手に登録されないようになる予定です。

ここでは、一つ重要な手続きが抜けています。プッシュサーバの指定はどうするのか?
実はこれまだ未定です。現時点では明確に定められていません(Push API仕様にはいくつか方法が例示されています)。様々なプッシュ通知を集約して一元的に処理するプッシュサーバの機能はとってもビジネスな臭いがします。

8. HTTP/2 サーバプッシュを用いた Web Push の仕組み

さぁやっとHTTP/2アドカレ本来のテーマ、HTTP/2 サーバプッシュを用いた Web Push の仕組みです。以下に続く7つのステップでHTTP/2のサーバプッシュを用いたWeb Pushが動きます。
draft-thomson-webpush-http2-01Push API W3C Editor's Draft 03 December 2014をもとにしたWeb Pushの仕組みについて書きます。
繰り返しますがまだ仕様策定中で不明なところも多く、また試験実装も完了していないので、そこのところ留意してお読みください。

Step 1: 登録

クライアントの Service Worker がプッシュサーバに登録リクエストを POST で送ります。登録が成功したらサーバは 201(Created) のコードで monitor と subscribe のURLを返します。

Step 2: 接続

Step 1で返されたLocationヘッダから次に再度プッシュサーバへのGET接続をします。これはプッシュサービスを利用する継続用の接続で、サーバはレスポンスを返しません。ロングポーリング再びです。

Step 3: subscribe

アプリ毎にプッシュ通知を受けるChannelを作成してsubscribeします。Step 1で受信した subscribe URLへクライアントからPOSTします。プッシュサーバ上ではアプリ用のPush Channelが作成されます。今後このChannelを通じてメッセージのやり取りが行われます。

Step 4: monitor

クライアントからStep 3で作成した Push Channel をモニターするGETリクエストを送信します。URLはStep 1で得たものです。プッシュ通知を受ける継続用のリクエストなのでサーバはレスポンスを返しません。

Step 5: distribute

クライアントからアプリケーションサーバ側にプッシュサービスの登録情報を渡します。この登録情報は、アプリサーバがプッシュ通知を行うendpointのURLと認証用の登録ID(registrationID)の2つです。実はregistrationIDをどう生成して渡すのかまだ明確に規定されていませんが、プッシュサーバのsubscribe時に渡されるんじゃないかと想像しています。

Step 6: deliver #1

アプリケーションサーバはクライアントへプッシュ通知するイベントが発生したら、渡されたendpointとregistrationIDを使ってプッシュサーバのチャンネルにデータをPUTします。この辺の仕様の詳細は、仕様規定の範囲外になっています。

Step 7: deliver #2

プッシュサーバは、クライアントとの間のHTTP/2接続上でサーバプッシュを使ってプッシュ通知を行います。どんなURLにするのか詳細は未定ですが、おそらくプッシュ通知毎にサブチャネルのID(1)とかを振ることになるのでしょう。サーバプッシュのレスポンスボディ部にプッシュデータを含むことが規定されています。サーバプッシュによってプッシュ通知を受けたクライアントは、Service Worker上のpushイベントを通じてデータを入手します。

一連のWeb Pushの仕組みは以上です。

9. 現状の実装状況

最後に現状の実装状況を紹介します。

Chromeでは、GCMと連動したPush APIの実装が進んでいます(Intent to implement: Push API)。

Firefoxでは、Push APIの前身となるSimple Push APIの開発がされています(まだ実際に試していません)。
実はFirefoxでは、HTTP/2 サーバプッシュによるPush APIを作るためのとっかかりの実装が既に完了しています。(参考ブログ:「Firefox gecko API for HTTP/2 Push」
これは HTTP/2 のサーバプッシュから受けたデータをFirefoxの内部API nsIHttpPushListener で受けられるようにする実装です。nsIHttpPushListener は Addon 等でも使えるので Firefox36 以降で実際試してみることができるでしょう。普段ならここでサンプルAddonと実行結果でも載せるんですが、今回時間がないので勘弁してください(どなたか動作検証していただけると助かります)。今後この nsIHttpPushListener と Service Worker の実装を組み合わせて、Push APIを作っていくものと思われます。

HTTP/2とService Worker、この2つのホットな最新技術を組み合わせた次世代のWeb Pushの実現はもう少し先の世界ですが、これからが楽しみです。

*1:実際には後述するようプッシュサーバへの登録やsubscribeといった事前の手続きが必要になります。

*2:仕様をHTTP/2の利用に限定するかどうかは議論中です

不正なSSL証明書を見破るPublic Key Pinningを試す

先日のエントリー 「TLSとSPDYの間でGoogle Chromeがハマった脆弱性(CVE-2014-3166の解説)」で予告した通り、今回不正なSSL証明書を見破る Public Key Pinningの機能について解説します。

Public Key Pinning は2種類の方法があります。あらかじめブラウザーソースコードに公開鍵情報を埋め込む Pre-loaded public key pinning と、サーバからHTTPヘッダでブラウザに公開鍵情報を通知するHTTP-based public key pinning (HPKP)の2つです。

Chromeは既に両者の機能を実装済ですが、ちょうど近日リリースされる Firefox 32 の Stable バージョンから Pre-loaded public key pinning が実装されました。Firefox32リリース記念としてこのエントリーを書いてみたいと思います。*1

1. 不正なSSL証明書の脅威

SSLの証明書を発行する認証局(CA)は、本来非常に高いセキュリティでシステムの運用管理や監査もされているものですが、近年世界中でこれだけ(一説では600以上あるとの話も)認証局の数が多くなってしまうと、世界中でいろいろ事故・事件が起こります。ざっと調べてみると以下の通りで、

2011年 イギリスのComodoが不正侵入を受け、不正なSSL証明書が発行される。
2011年 オランダのDigiNotarが不正侵入を受け、不正なSSL証明書が発行される。
2013年 フランスの政府系認証局ANSSI傘下の中間認証局から、不正なSSL証明書が発行される。
2014年 トルコのTURKTRUSTが誤った中間CAの証明書を発行し、不正なSSL証明書が発行される。
2014年 インドのNational Informatics Centre(NIC)の認証局(CA)を経由して、不正なSSL証明書が発行される。

だいたい毎年1件ぐらいニュースになっています。
こんな状況が将来続けて起こることを知ってか知らずか、Googleは2011年5月Chrome13より Public Key Pinning という機能を使って不正に発行されたSSL証明書を見破る仕組みを実装しました。
ImperialViolet/Public key pinning (04 May 2011)
Publick Key Pinningは、本物の証明書の公開鍵データのハッシュ値をブラウザにあらかじめ登録し、ブラウザがTLS接続する際に実際のサーバから送信されてくる証明書の公開鍵データのハッシュ値と比較して、不正な証明書の利用を検知、防止する機能です。

ブラウザのソースコードにPinning情報を埋め込むやり方は流石にスケールしないので、HTTPヘッダのやり取りでブラウザにPinning情報を登録させる HTTP-based public key pinning (HPKP)機能がIETFの websec ワーキングループで議論されています。現在仕様ドラフトが提出され、IESGレビュー中で RFC化目前です。

上記のリストに挙げた不正発行の多くは、このPublic Key Pinning機能によって検知され、直ちにCA証明書の失効措置がとられました。これまでの攻撃は、Googleなど有名な大手サービスサイトのドメインを狙ったものですが、攻撃者はこの Public Key Pinning機能を警戒し、今後はもっとローカルな標的型になることが予想されます。
そこそこの機密情報を扱うサイトの管理者は、今のうちから Public Key Pinning の検証や準備などしていても損はないでしょう。

そもそも「こんな不正証明書の検知・防止機能が必要になってしまう Web の PKIって現状はどうなんよ? 」というご意見もあるでしょう。それに言及すると本記事の範囲を大きく逸脱するので今回は止めておきます。

2. 不正な証明書を検知するとどうなる?

まずは試してみましょう。ChromeやFirefox32以降を使っている方は、
https://pinningtest.appspot.com/
にアクセスしてみましょう。Chromeでは、

Firefox(32以降)では、

の画面が出るはずです。Public Key Pinningのチェックが働いている証拠です。
もし普段の利用でGoogleTwitter等のサービスにアクセスしてこの画面が現れたらヤバイです。途中のネットワーク経路でMITM攻撃を受けている可能性があるので、早急に調査が必要です。この画面をよく覚えておきましょう。

3. Pre-loaded Public Key Pinning

Pre-loaded Public Key Pinning は、Chrome13、Firefox32からサポートされています。ブラウザのソースコードにPinning情報を埋め込む方式で、現在 GoogleTwitterMozillaDropbox等限られたサービスのドメインのみ登録されています。最新の登録リストは、

のファイルに書かれています。
どうブラウザに登録されているか Chrome では chrome://net-internals/#hsts の query domain を行うと確認ができます。

現状、Pre-loaded Public Key Pinning の登録申請条件や方法は、Chrome/Firefoxとも非公開の様ですが、ブラウザベンダと特別な契約にないと登録してもらえないと思われるので、一般の方は特に関係はないでしょう。ただ、どのサイトがいつ登録されたかという情報ぐらいは知っておきたいものです。
Chromeは相変わらずソース以外に公開情報は少ないですが、Firefoxには、
https://wiki.mozilla.org/SecurityEngineering/Public_Key_Pinning
で情報が提供されています。Firefoxでは、security.cert_pinning.enforcement_level の設定値で検知レベルを変更できるようです。

4. HTTP-based public key pinning (HPKP)

Chromeは既にStable版(37)でサポート済です(いつからかちょっと調べきれませんでした)。 Firefoxは未サポートで、現在実装中です。
Bug 787133 - Implement Public Key Pinning Extension for HTTP

HPKP仕様の特徴について、以下に簡単にまとめてみます。*2

4.1 Public-Key-Pinsヘッダを利用

HPKPを使うのは、下記のようなレスポンスヘッダをサーバからクライアントに送信するだけです。

Public-Key-Pins: max-age=300; includeSubDomains; pin-sha256="2cZbvylT1JOfFhVTZbepcnkwAWW4qoi7IPVW5TGYqtM="; pin-sha256="ruOT7A2K2hkuWLxpair4bMUPU2MS1bVnUFwSKTqTvlk="

ただしセキュア通信上(TLS)でヘッダのやり取りすることが必須です。SSL接続を強制するHSTSヘッダは必須ではありませんが、併用することが推奨されています。
max-ageは、Pinning情報の有効時間でアクセスする度に更新されます。includeSubDomainsは文字通りサブドメインも対象に含むかどうかを表すフラグです。他に仕様では、エラーレポートを送信するURIも記載できますが、まだChromeには実装されていません。

4.2 完璧防御じゃない TOFU (trust-on-first-use)

一番最初に Public-Key-Pins ヘッダをやり取りする際のMITM攻撃には脆弱です。これは Pre-loaded に分があると言えるでしょう。

4.3 チェック対象は SPKI(Subject Public Key Info) フィールド

Public Key Pinning チェックのキモとなるハッシュ値は、証明書全体のハッシュ値ではなく、証明書のSPKIフィールドの部分のハッシュ値を取ってチェックします。

これは、同一のキーペアから複数別の証明書(SHA1,SHA256等)が発行される場合などを想定しているからです。公開鍵単体だけだと同じ鍵で別のアルゴリズムを使う攻撃を受ける可能性があり、SPKIは公開鍵のアルゴリズム情報を含んでいるため対応することができます。また、あまり褒められた運用じゃないですが、更新時に同一のCSRを使って証明書を発行すればPinningの更新する必要はありません。

ハッシュ方法は仕様上 sha256のみですが、当初より Googlesha1 を使っていたため、Chromeでは sha1 も使えるようです。このハッシュ値base64エンコードしたものをヘッダ値に入れます。

ハッシュ値は複数記載でき、Pinningのチェックは証明書チェーンの内どれか一つのハッシュ値が合致すれば良いので、どれを使うかは運用ポリシー次第です。そのうちベストプラクティスとか出てくるのを期待しましょう。

4.4 ユーザが登録したroot証明書から発行されたものはチェック対象外

ブラウザにビルトインされた root 証明書までのチェーンをチェックします。ユーザが独自に登録した root証明書から発行された証明書は Pinning のチェックをしません。企業による MITM Proxyの利用等が想定されるからです。このおかげで Pinning の検知テストが正式な認証局から発行されたものじゃないとできず、結構テストのハードルが高くなりました。

4.5 Backup PINの登録が必須

事前に更新PINの追加をし忘れて、うっかり証明書を変更してしまい、ユーザがPinningエラーでつながらない、max-ageで expire するまでPINが更新できないといったような運用トラブルを回避するために Backup PIN の登録が必須になっています。Backup PIN は、証明書チェーンをたどって行って全てにマッチしないPINが一つでも存在することが条件です。CSRでバックアップPINを作っておくことも可能(後述)です。いずれにせよトラブル時に慌てないようちゃんと訓練しておくことが推奨されています。

4.6 レポーティング機能

仕様では、Pinningチェックのレポートだけ行う Public-Key-Pins-Report-Only ヘッダも定義されていますが、Chrome/Firefoxともまだ実装されていません。ただ現状何もしないわけではなく、ChromeGoogleのサービスに対する不正検知のみGoogleのサイトへレポートを送信するよう実装されています。Firefox は統計情報を取得し、その情報は、
http://people.mozilla.org/~mchew/pinning_dashboard/
で公開されています。

4.7 Pre-loaded/HPKPの併用

仕様では実装依存になっていますが、 Chrome は HPKPを先にチェックするようになっています。

5. 実際にHPKPを試す

実際に Chorme と Node.js を使って HPKP を試してみましょう。まずは Pinning情報の生成です。
openssl コマンドから SPKI情報のみ取り出し、 DER形式に変換し、sha256のハッシュ値を計算して Base64 にします。

$ openssl x509 -in server.cert -pubkey -noout | openssl pkey -pubin  -outform der | openssl dgst -sha256 -binary |openssl base64

これで完了。次にバックアップPINです。CSRを作っておいて、同様にPinning情報生成します。

$ openssl req -in backup.csr -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary |openssl base64

このデータを使うとサーバ側はこんな感じOKです。念のためHSTSヘッダも同時に付与しておきます。

var server = https.createServer(opts1, function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain',
                      'Strict-Transport-Security': 'max-age=300; includeSubDomains',
                      'Public-Key-Pins': 'max-age=300; includeSubDomains; pin-sha256="2cZbvylT1JOfFhVTZbepcnkwAWW4qoi7IPVW5TGYqtM="; pin-sha256="ruOT7A2K2hkuWLxpair4bMUPU2MS1bVnUFwSKTqTvlk="'
                     });
  res.end('Hello HTTPS Server in Pinning');
}).listen(port, function() {
  console.log('Listening server');
});

Chrome でアクセスして、 chrome://net-internals/#hsts でPinning情報が登録されているか確認します。

おー、ちゃんと登録されています。パチパチ。
で、本当にチェックできているのか試験です。でもこれがハードルが高いです。テストするには、

  1. SSLエラーがない。
  2. ブラウザのビルトインの root 証明書へのチェーンである。

といった条件が必要です。自己署名証明書や独自インストールしたroot CAからの発行されたものもダメ、正式認証局から発行されたけど Common Name が違うやつを使ってもダメです。まさかCAに不正侵入して不正証明書を入手するわけにもいかないので、Pinning登録したドメインサブドメインで違うCAから90日間有効のテスト証明書を入手してテストしました(購入前評価じゃなくてごめんなさい)。

そのサーバにアクセスしてみましょう。

いけました。

バックアップPINの登録が必須化されているよう、一度ドツボにハマると運用が大変な Public Key Pinning ですが、将来大規模なCAの事件・事故が起こるとも限りません。すぐ導入とはいかないまでも、事前に検証・評価などを行って、転ばぬ先の杖として準備しておきましょう。

*1:でも解説はFirefoxがまだ未実装のHPKPが中心です。

*2:draft-ietf-websec-key-pinning-20をベースとします

TLSとSPDYの間でGoogle Chromeがハマった脆弱性(CVE-2014-3166の解説)

1. はじめに、

Googleがハマったシリーズ第3弾。今度は Chrome脆弱性がテーマです。

先日(8月12日頃)突然Google ChromeでSPDYの機能が一旦停止されました。CloudFareの人が気づいてspdy-devへのMLの問い合わせし、すぐGoogleのaglさんからの返事で計画的で一時的なものであることがわかりました。
twitterfacebookもSPDYが全て使えなくなり非常に驚いたのですが、直後に Chrome の Stable/Beta/Dev チャンネルがアップデートされ、ほどなくして問題なくSPDYが使えるようになりました。

この理由は公式には明らかにされていませんが、Chromeのリリースアナウンスにヒントがありました。そこには、

High CVE-2014-3166: Information disclosure in SPDY. Credit to Antoine Delignat-Lavaud.

という脆弱性の修正項目がリストアップされています。おそらくGoogleは、脆弱性対策をしたChromeのバージョンアップを安全に行うため、一時的にSPDYの利用を停止したのではないかと推測されます。

Creditの記載からこの脆弱性は、直前の8月6日に米国で開催されたBlackHat 2014でのAntoine Delignat-Lavaud氏のセッション「The BEAST Wins Again: Why TLS Keeps Failing to Protect HTTP」が元ネタです。

このセッションの発表資料や論文・動画は、The BEAST Wins Again: Why TLS Keeps Failing to Protect HTTPに掲載されています。ただBlackHat講演時はまだChromeが未修正だったこともあり、SPDYの脆弱性に関する詳細な記載や動画は公開されていませんでした(現在は動画のみ掲載)。

現在の Chrome Stableは緊急避難的な脆弱性の修正で、根本的な修正は作業中でした。修正パッチはビルド後のエラーで数回 revert されましたが、やっと昨日のCanary(39.0.2129)で無事脆弱性対応が完了したようです。
そこで今回、このChrome脆弱性(CVE-2014-3166)がどういう原因で発生して、どう対策されたのかについて解説をしてみたいと思います。

tl;dr

この記事はTLSPKI・SPDYについて基礎的な知識を持つ比較的上級者向けのものです。この辺の知識がない初心者の方は、読んでもわけわからず、退屈してしまうかもしれないので注意して下さい。また当初、この脆弱性の鍵となる Public Key Pinning機能について一緒に説明を盛り込むつもりでしたが、あまりに長文になってしまう恐れがあり、断念しました。近いうちに別記事を出しますのでお待ちください。

2. CVE-2014-3166の脆弱性はどういうものか?

CVE-2014-3166について調べてると、 CVEデータを見ても詳細がはっきりしないです。概要を和訳すると、

Google ChromeWindows, OS X, Linux の 36.0.1985.143 以前と Android の 36.0.1985.135 以前バージョンでは、Public Key Pinning実装がSPDY接続の特性を正しく考慮していないため、遠隔の攻撃者が複数のドメイン名を利用することによって重要な情報を入手することができる。

と書いてあるだけです。発表資料や論文を読むとなんとなく想像できますが、脆弱性修正後の動画「Impersonation exploit against SPDY connection pooling」を見るとよくわかりました。もう少し詳しく書くと、

悪意がある攻撃者が、不正なSSL証明書を使い、クライアントのDNS名前解決を操作できると、Googleのサーバに成りすますことができる。

ということでした(実際にはもうちょっと条件なり操作が必要です)。

悪意のある第三者Googleのサービスに成りすますことができれば、クッキーなり重要な情報を抜けますし、フィッシングで誘い込むことが可能になります。

ここでこの脆弱性の鍵となる2つの技術項目、SPDYコネクション集約とPublic Key Pinningについて説明します。

3. SPDYコネクション集約機能

現在GoogleFacebookなど大規模にSPDYをサービスに展開しているサイトは、SSLの証明書にワイルドカード証明書(*.google.comとか)を利用しています。SPDYではワイルドカード証明書を使うと、複数ドメイン宛てのHTTPリクエスト・レスポンスを1つのSPDY接続にまとめて集約し、多重化をより高めて通信の効率化を図ることができます。
ただし無条件にどんなドメイン宛てのリクエスト・レスポンスが一緒になるわけではありません。

  • クライアントからサーバへの接続アドレス(DNSで解決した時のIP)が同一である。
  • 接続先のホスト名がSSL証明書で認証できる。

といった条件が必要です。

例えばProxy接続しているクライアントは、ソケットの接続アドレスがDNSで解決したサーバのアドレスと異なるのでSPDYコネクションの集約されません。また同一のIPであれば、SSL証明書(X509書式)での Common Name や subjectAltName フィールドを見て、同一の証明書で認証できるかどうか調べます。Chromeはこのホスト名のチェックにRFC6125で規定されている方法を使っています。

実際にSPDYのコネクション集約がされているかどうかは、 chrome://net-internals/#spdy を見るとわかります。赤線で囲った部分を見ると複数ドメインが1つのSPDY接続に集約されているセッションです。

今回の脆弱性は、このSPDYのコネクション集約機能を利用してGoogleのサーバに成りすましを行うものでした。

4. Public Key Pinning機能

この機能の詳細は別記事で紹介する予定です。簡単に書くと、不正に発行された証明書かどうかブラウザがチェックできる機能です。
近年ブラウザに登録されている正式な認証局(CA)が外部から侵入を受け、不正なgoogle.comドメインなどの証明書が発行されてしまう事件が起きています。普通は正式な認証局から発行された不正なSSL証明書の見分けがつかないのですが、Public Key Pinning機能を使うと見破ることができます。

現在 IETF仕様が検討され、IESGレビュー中でRFC化直前になっています。
今回の脆弱性は不正なSSL証明書とSPDYコネクション集約を使い、この Public Key Pinning のチェックのバイパスを狙うものでした。

5. CVE-2014-3166の脆弱性の原因と対策

やっと脆弱性の解説の本丸です。脆弱性の原因は、ChromeのSPDY実装でコネクションの集約を行う際に以下の2項目のチェックが抜けていたことです。

  1. 集約する既存のTLS接続がエラー(expireや証明書チェーンの検証に失敗等)になっていないか?
  2. 集約する際に対象のドメインの Public Key Pinning で登録されたものと既存のTLS接続の証明書が一致するか?

この抜けを利用すると、Googleサーバに成りすますには以下の手順になります。

  1. Googleドメインと攻撃者ドメインの両者で有効な証明書とSPDYサーバを用意する。
  2. キャッシュポイズニングなどによってクライアント側のDNS名前解決を操作して、攻撃者サーバ側にGoogleサービスの偽IPアドレスを向ける。
  3. クライアントから攻撃者ドメインのサーバ向けに最初のSPDYセッションを張らせる。
  4. IPアドレスGoogleサービスにアクセスするように誘導する。
  5. クライアントは不正なSSL証明書だとは気付かずに、攻撃者が成りすましたGoogleのサーバへのアクセスする。

という手順になります。

この手順を実際に試してみましょう。subjectAltNameで hoge.example.jp と *.iijplus.jp の二つのドメインを登録した証明書を作成します。ただしCAに侵入して不正発行するのはもっと難しいので、自己署名証明書を使います。

DNS情報を書き換える替わりに、hostsファイルで両ドメインを同じアドレスに向けます。 *.iijplus.jpドメインのPublic Key Pinningの設定をして、正式な証明書の公開鍵のハッシュ値をあらかじめブラウザに登録しておきます。このやり方は別記事で書きます。DNS情報を書き換えるのではなく、hostsファイルで同じアドレスに向けます。

脆弱性対策前のChrome Stable(36.0.1985.125)で接続してみます。

最初に hoge.example.jpのページに接続し、その後 demo-int.iijplus.jp に接続すると、SPDYセッションが集約されていることがわかります。自己署名証明書なので、SSLエラー通知の画面が出ているのですが、最初 hoge.example.jp を許可後 demo-int.iijplus.jp の接続には警告ページが出てきません。ブラウザのアドレスバーには警告マークが出てきます。

現状の Chrome Stable は、緊急避難的に一律にSPDYのコネクション集約を無効にした対策が施されていますが、昨日のChrome Canary(39.0.2129)より、SPDYコネクション集約時にSSLエラーとPublic Key Pinningチェックを行う処理が追加されました。脆弱性対策の有効性を確認するためCanaryで同様の試験をしてみます(ただしSPDY接続の分離を見るため Public Key Pinningの設定は外します)。SPDYセッションは次のようになりました。

不正なSSL証明書を使った hoge.example.jp と demo-int.iijplus.jp 宛ての接続が集約されておらず、別々のSPDY接続になっています。SSLエラーチェックが効いたことによるものです。その他の正式なGoogleサービスへの接続は問題なく集約され、Public Key Pinningの設定をしてみるとdemo-int.iijplus.jp への接続は遮断されてちゃんと証明書チェックが働いているのがわかりました。

この攻撃を本当にステルス的にするには、CAに侵入して不正なSSL証明書を入手し、クライアントネットワークにMITMの環境を仕込むというなかなか高いハードルが必要ですが、昨今のNSAによるネットワーク盗聴や改ざんの手法を聞くと絶対実現不可能とも言えないかと思います。

6. TLSとSPDYの間にある隙間

この脆弱性の発見者 Antoine Delignat-Lavaud 氏は、IETFTLS WGのメーリングリストで、現状のTLSとSPDYの仕様の間で考慮されていない部分があるのが問題だと指摘しています(Re: Inter-protocol attacks)。

TLSを使ったSPDYの初期接続時は、クライアントはTLS上でサーバ証明書で接続サーバが正式なものか認証(Authentication)し、サーバはTLSの認証情報やSPDYの:hostヘッダなど使いどのページにアクセスできるのか認可(Authorization)が行われます。しかし、再接続時(resumption)は、TLSは session ID, ticket, channel ID 等の仕組みを使って再接続しますが、SPDYの再接続はTLS接続の再利用を行うのでTLSの再接続の仕組みが使えません。

初期接続時は TLSとSPDYの認証・認可の機能はある程度連携できているのに、再接続時は全く違うものになってしまっている。 Antoine Delignat-Lavaud 氏は、ここを明確にしてちゃんと仕様で定義しておく必要があるのではとTLS WGのMLに投げかけています。

今回の脆弱性は、まさにこのTLSとSPDYの間にハマった事例だと言えるでしょう。

現在LastCall終了目前の HTTP/2 の仕様ではこの部分について9.1.1 Connection Reuseの節で、subjectAltName やSNIの利用についての注意が記載されています。この記載で本当に十分かどうかは議論が分かれるところです。

彼の発表では、他にもTLSのハンドシェイクのSNI(Server Name Indicator)で指定したサーバ名とHTTP中のホストヘッダが異なる場合に例外処理がちゃんとされてないために脆弱性が存在していること(VirtualHost Confusion)を指摘しています。

彼の発表では、集約型のSSLホスティングCDNでの exploit 動画を公開していますが、特に OpenProxyを突いたAkamai脆弱性にはびっくり、nsa.govのサイトを成りすましています。

他にも、分割されたTLSセグメントを切り捨てたことによる不完全なデータの受信を突くCookie Cutter脆弱性や、 2つのTLSセッションの master secret を同期させ、証明書を変更した renegotiation によって、不正データを入れ込む Triple Handshake などTLSネタが盛りだくさんです。

TLSセキュリティの最前線に興味のある方は、exploit 動画だけでも見ておいても損はないと思います。

OpenSSLの脆弱性(CVE-2014-3511)でTLSプロトコルの基礎を学ぶ

1. はじめに、

昨日 OpenSSLのバージョンアップがアナウンスされ、9つの脆弱性が公開されました。バージョンアップの数日前にOpenSSLの次期リリース予告がアナウンスされていましたが、ちょうど BlackHat 開催初日にあたることもあり、なんかまた重大な脆弱性の修正が入るんじゃないかとドキドキしていました。蓋を開けてみるとHeatBleed程の大事ではなくホットひと安心です。
昨日公開されたOpenSSLの9つの脆弱性のうち、TLS プロトコルダウングレード攻撃 (CVE-2014-3511)の修正を見ていたところ、これはTLSプロトコルを学ぶいい題材になるなぁとふと思いつき、試しにこのOpensslの脆弱性の詳細をTLSプロトコルの基礎に合わせて書いてみました。
ちょっと長いですが、TLSプロトコルの仕組み(の一部)を知りたい方はお読みください。

2. OpenSSLの脆弱性 CVE-2014-3511とは、

この脆弱性(CVE-2014-3511)はどんなものでしょうか?
本家 OpenSSLのアナウンスから該当部分を翻訳すると、

OpenSSL TLS プロトコルダウングレード攻撃 (CVE-2014-3511)
=====================================================
OpenSSLのSSL/TLSサーバコードの不具合によって、ClientHelloメッセージが不正に分割されるとTLS1.0より高いバージョンが使えるのにTLS1.0でネゴシエーションしてしまいます。
これによってMan In the Middle の攻撃者がクライアントのTLSレコードを変更するとサーバとクライアントの両者がTLS1.0以上が利用できるにも関わらず強制的にTLS1.0にダウングレードさせることができます。
OpenSSL 1.0.1 SSL/TLSサーバ利用者は 1.0.1i へアップグレードすべきです。

と記載されています。ざっと絵で描くと下図な感じでしょう。

一般的にダウングレード攻撃が可能になると、悪意のある第三者によって意図せず脆弱性を持つプロトコルバージョンへ強制的に接続されることになり、それをとっかかりにして様々な攻撃を通信上で受けることになります。 TLSv1.0に関しては、CBCモードの利用に脆弱性が存在するため気を付けることが必要です。

3. TLSプロトコルの基礎

この脆弱性をちゃんと理解するには、TLSプロトコルデータのフォーマットや初期ハンドシェイクの仕組みを理解することが必要です。

3.1 TLSプロトコルデータフォーマット

TLSプロトコルデータフォーマットの概要を下図に示します。

TLSのデータは必ず先頭に TLS Record Layer という5バイトのヘッダが付与されます。その後に4種類のメッセージが続きます。今回の主役は、Handshakeメッセージです。Handshakeメッセージは、暗号や証明書などのTLS通信に必要な情報をサーバ・クライアント間で共有するために用います。Handshakeは10種類規定され、今回問題となる ClientHello は 0x01 番で登録されています。
Record Layer は最大14bit長(16kバイト)のメッセージデータを扱えます。しかも複数のハンドシェイクメッセージを1つのRecordにまとめたり、1つのハンドシェイクメッセージを複数のRecordに分割することができます。今回の脆弱性は、分割を行うことによって発生するものでした。

3.2 TLS初期ハンドシェイク

一番最初にサーバクライアント間でTLS接続を開始する際に、下図の通り複数の Handshake のメッセージをサーバ・クライアント間で交換し合います。

それぞれの Handshake メッセージには役割がありますが、一番最初の ClientHello と ServerHello のやり取りでは双方が利用するTLSのバージョンや暗号アルゴリズムなどを決定します。今回のダウングレード攻撃は、最初のClientHelloを操作する手法です。

3.3 ClientHelloのデータフォーマット

今回の注目データ、ClientHelloのデータフォーマットを見てみます。

1バイトのClientHelloのメッセージタイプ(0x01)と3バイト分のClientHelloのメッセージ長さフィールドの次にクライアントのプロトコルバージョンを表す2バイトのフィールド(メジャーとマイナー)が現れます。ここにクライアントが利用できるTLSの最高バージョンが指定されます。
サーバがClientHello見てどのバージョンを使うか選択し、ServerHelloを使ってクライアントに返します。通常はクライアントが対応しているTLSバージョンの中でサーバが最も優先度が高いプロトコルバージョンを選択します。

OpenSSLで SSLv23互換メソッドでサーバを利用する場合、先頭の11バイトを見てClientHelloの処理を行います。今回この部分に問題がありました。

なお、TLSプロトコルバージョンは、SSL時代から続いているもので TLS1.0 は SSL3.1換算となります。過去、標準化の過程において諸所の事情からSSLからTLSへの名称変更が行われましたが、プロトコル中ではまだSSLが続いていることになります。

以上でTLSプロトコルの基礎編は終わりです。

4. CVE-2014-3511脆弱性の中身

ClientHelloがどういうもので、どういう役割を持つのか理解したところで、今回の脆弱性を見てみます。
修正個所の diff は以下のようになっています。Fix protocol downgrade bug in case of fragmented packets

                         * Client Hello message, this would be difficult, and we'd have
                         * to read more records to find out.
                         * No known SSL 3.0 client fragments ClientHello like this,
-                    * so we simply assume TLS 1.0 to avoid protocol version downgrade
-                    * attacks. */
+                  * so we simply reject such connections to avoid
+                  * protocol version downgrade attacks. */
                        if (p[3] == 0 && p[4] < 6)
                                {
-#if 0
                                SSLerr(SSL_F_SSL23_GET_CLIENT_HELLO,SSL_R_RECORD_TOO_SMALL);
                                goto err;
-#else
-                           v[1] = TLS1_VERSION_MINOR;
-#endif
                                }

p[3],p[4]は、TLS Record Layer の長さフィールドです、修正前では ClientHello の TLS Record Layer の長さが6バイトより小さければ、TLSのマイナーバージョンを1(=TLS1.0)にするということです。(これ以前にメジャーバージョンが3であることのチェックは済んでいます)

ということは、クライアントが最高でTLS.1.2が利用可能で ClientHello を送ろうと、ClientHello が6バイトより小さく分割されていれば問答無用で強制的にTLS1.0 になってしまいます。TLSレコードの分割は、経路の途中で容易に行えますので Man-in-the-Middle によるダウングレード攻撃は可能です。

でもどうしてこうなったのでしょうか? この箇所処理が入った過去のコミットを追っかけてみます。ChangeLogにはこう記載されています。
Assume TLS 1.0 when ClientHello fragment is too short

Instead of aborting with an error,simply choose the highest available protocol version (i.e.,TLS 1.0 unless it is disabled).
エラーで終了する代わりに、利用できる一番高いプロトコルバージョンを単に選ぶようにする。(特に TLS1.0が無効されてなければ)

あぁ、もともと6バイトより小さいClientHelloはエラーにしていたのですが、2001年当時最もバージョンの高いTLS1.0を決め打ちで定義してしまったようです。TLS1.1や1.2ができることを想定してなかったのでしょう。

SSLv3へのダウングレード対策で修正したことが、今度は13年後にダウングレード脆弱性を引き起こすことになるとは… なんとも皮肉なことです。

5. CVE-2014-3511の脆弱性を試す

脆弱性の詳細が分かったところで、実際に試してみます。ClientHelloを5バイト以下に分割して送ればいいだけです。まずはテスト用の最小ClientHelloを作ってみましょう。

わずか47バイトで出来ます。このHex文字列をそれぞれ 下記のNodeのコードを使って OpenSSL(1.0.1h)のサーバに送ってみます。

var net = require('net');
// TLSレコード分割のフラグ
var frag = process.argv[2] === 'frag' ? true : false;

// Hex文字列をBufferに変換する関数
function HexStrToBuf(str) {
  var buf = new Buffer(str.length/2);
  for(var i = 0; i < str.length; i += 2) {
    buf.writeUInt8(parseInt(str.substr(i,2), 16), i/2);
  }
  return buf;
}

var client_hello = "160301002f0100002b03030000000000000000000000000000000000000000000000000000000000000000000004009c00350100";
var client_hello_frag1 = "16030100050100002b03";
var client_hello_frag2 = "160301002a030000000000000000000000000000000000000000000000000000000000000000000004009c00350100";

var handshake = HexStrToBuf(client_hello);
var handshake_frag1 = HexStrToBuf(client_hello_frag1);
var handshake_frag2 = HexStrToBuf(client_hello_frag2);

var s = net.connect({port: 443}, function() {
  if (frag) {
    s.write(handshake_frag1);
    s.write(handshake_frag2);
  } else {
    s.write(handshake);
  }
});
s.on('data', function(b) {
  console.log(b);
});

まずは分割せずにHandshakeを見てみます。受信パーサを書いてもいいのですが、客観性を保つためWiresharkの結果を載せます。

ClientHelloはちゃんと意図したフィールドで送信できています。OpenSSLのサーバからは、無事 ServerHelloが返ってきました。

フィールドを確認するとバージョンは TLS1.2、暗号アルゴリズムも GCM が選択されています。よしよし。

次にいよいよClientHelloを分割して送ります。Wiresharkでは分割を正しくデコードできないので ServerHello だけを載せます。
結果

おっと、脆弱性発生! ServerHello からサーバは TLS1.0 を選択し、暗号アルゴリズムCBCの方になっています。ダウングレード攻撃の成功です。

今度は、脆弱性を修正した OpenSSL 1.0.1i のサーバで試します。以下の通り接続が切断され、エラーメッセージがサーバ側に無事出力されました。パチパチ。

$ ./apps/openssl version
OpenSSL 1.0.1i 6 Aug 2014
$ sudo ./apps/openssl s_server -cert ~/tmp/cert/bundle.crt -key ~/tmp/cert/server.key -port 443Using default temp DH parameters
Using default temp ECDH parameters
ACCEPT
ERROR
140379538994848:error:1407612A:SSL routines:SSL23_GET_CLIENT_HELLO:record too small:s23_srvr.c:355:
shutting down SSL
CONNECTION CLOSED
ACCEPT

セキュリティに関連する技術は厳密に定義されるので一般的に理解が難しいことが多いですが、こういう脆弱性の題材を見つけて少しでも理解が進むと楽しくなるんじゃないかと思います。

GoogleによるOpenSSLのfork、BoringSSLを試す。

1. はじめに、

先日、Chrome「Issue 401153002: Switch to BoringSSL. (Closed)」 という変更が行われました。これは、従来の Android向け Chrome では OpenSSL を利用していたのですが、今回これをGoogleがOpenSSLをforkしたBoringSSLに切り替えたことになります。 BoringSSLの発表からわずか1か月ぐらいですが、何回か Revert された末ようやく切り替えが成功したようです。

今回 BoringSSL を試しに少し使ってみましたので、そのレポートしてみたいと思います。

2. BoringSSLとは何か

BoreingSSLがどういうもので、なぜOpenSSLをforkしたかは、GoogleのセキュリティExpert agl さんのブログ「ImperialViolet - BoringSSL」で詳しく記載されています。

これまでGoogleは、Android向け Chrome に OpenSSL、それ以外のプラットフォームでは Mozilla の開発した「NSS(Network Security Services)」をベースにGoogle独自のパッチをあてて利用してきました。 ChromeがNSSに当てているパッチ集 を見ると、現在20以上のパッチを当てているのがわかります。なので、簡単に言うとメンテするのが大変になったのでforkしたということらしいです。(注:Android以外のプラットフォームではまだNSSを継続利用中です。過去OpenSSLに切り替えるかどうかの議論がされていました。)

openssl-1.0.2beta ベースを fork した BoringSSLは、
https://boringssl.googlesource.com/boringssl/
でソースが公開されています。ソースの変更履歴をざっと見て、現時点でどう変わったのかを*個人的に*まとめると、

な感じです。(注:細かく見ていないので*見落としがある*かもしれません。)

なぜBoringSSLと命名したのか、aglさんのブログでは明確に書いてなかったのですが、上記の変更作業を見ているとSSL処理をrobustにするためのものがほとんどで、地味で忍耐が必要な作業が中心(=退屈)ということじゃないな、と個人的に想像します。

3. BoringSSLを試す

3.1 BoringSSLのビルド

早速ビルドしましょう。ソースに付随のBUILDINGファイルに詳細に記載されていますが、cmakeとninjaがあればさくっと作れました(以下、Ubuntu14上です)。

$ cmake -GNinja
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ohtsu/tmp/boringssl
$ ninja
[392/392] Linking CXX executable tool/bssl
$ l -l ssl/libssl.a crypto/libcrypto.a
-rw-rw-r-- 1 ohtsu ohtsu 5482204 Jul 29 16:15 crypto/libcrypto.a
-rw-rw-r-- 1 ohtsu ohtsu 2762062 Jul 29 16:16 ssl/libssl.a

OpenSSLと同じく libssl.a, libcrypto.a ができれば完了です。

3.2 BoringSSLによるTLSクライアント接続

OpenSSLと違ってコマンドライン用の実行バイナリ apps/openssl は作られません。
替わりに tool/bssl が作られますが、今のところ機能としては encyption のベンチ
bssl speed とTLSクライアント bssl client の2つのみです。
Googleのサーバに接続して、早速TLSクライアント接続を試してみます。

$ ./tool/bssl client -connect www.google.co.jp:443
Connecting to 74.125.235.95:443
Connected.
  Version: TLSv1.2
  Cipher: ECDHE-RSA-CHACHA20-POLY1305
  Secure renegotiation: yes

おぉ! 現在 Google 一押しのTLS未公認CipherSuite、 ChaCha20+POLY1305で接続できています。

3.3 BoringSSLサーバによるChannel IDを試す。

折角ですから、 BoringSSL固有の機能の TLS Channel IDを試してみましょう。
Transport Layer Security (TLS) Channel IDsとは、GoogleIETF TLS WGで提唱しているTLSの新機能で、接続クライアントを特定・トラッキングを実現できるものです(ドラフトは昨年末にExpireしちゃったみたい)。
これはTLSハンドシェイクを拡張し、サーバ・クライアント間で Channel IDの利用を合意すると、クライアントがキーペアを作成し、公開鍵を含むChannel IDをサーバ側に渡す仕組みです。この Channel IDにバインドした Cookie や OAuth Token を利用すると、TLS再接続時に同一クライアントからの接続をサーバ側で検証してセッションハイジャックやMITMの対策が可能になります。

OpenSSLとAPIはほとんど変わっていないので、BoringSSLを使ってChannel IDを有効化したTLSサーバを作ってみます。ソースはこちら(簡単のためエラー処理は省いています)。
https://gist.github.com/shigeki/a0904e116def85d7e5ff
Channel ID対応クライアントは、既に Stable版 Chrome でサポートされているので、それを使います。ただHTTPSサーバを作るのは面倒なのでレスポンスは返さず、TLS接続するだけの用途で利用します。

以下はChromeで接続した時にサーバ側のログです。64byteの Channel ID をクライアントから受け取っていることがわかります。

$ ./server
channel_id=D8C277B1837B3CA1296C204124C342E06F8C968E14801A575BC1EFAB8103296B6BD51673944961DEED08B06245C3989AE8D8A1FCA293235C7BBB1D39E2F7
GET / HTTP/1.1
Host: demo-int.iijplus.jp:8443
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8

次にクライアント側の Chrome の Channel ID の確認です。 chrome://settings/cookies を見てみます。

おぉ、ちゃんと Channel IDが生成されて保存されていることがわかります。

4. 今後の見込み

先週カナダのトロントIETF の総会が開催されました。TLS WG も interim 等開催して、次期TLSのバージョン 1.3 の議論が進んでいます。ラストコール目前の HTTP/2 仕様もこのTLS1.3で議論されている内容(renegotiationの禁止、PFS/AEADの必須化等)を先取りした仕様項目を取り込んでいます。

今回 Google が OpenSSLをforkして BoringSSL を作ったことによって Chrome と GFE(Google Front End)間でこれまで以上に自由にTLSの実験的機能の試験ができるでしょう。 近い将来 ChromeGoogleのサービスを使っていると、いつの間にか皆が気づかないうちに TLS1.3 +α になっていたということになるんじゃないかと想像しちゃいます。