ぼちぼち日記

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

io.jsのTechnical Committeeに推薦されました

1. はじめに

「こんな私がXXXに!?」の宣伝文句ではありませんが、こんな私がio.jsプロジェクトのTechnical Commitee(TC)に推薦されました。
Nominating Shigeki Ohtsu @shigeki to the TC

まずは見習いとして数週間オブザーバーとしてTC meetingに参加、その後TCメンバーの投票を経て晴れてTCメンバーです。favや応援メッセージをいただいた方、ありがとうございました。

TC meetingは毎週木曜の早朝朝5時、Googleハングアウトで行います。会議の様子はライブ配信され、youtubeで録画公開されてます。議事録も随時公開されています。https://github.com/iojs/io.js/tree/master/doc/tc-meetings
コミュニケーションは当然全部英語。大変です。昨日の早朝に初めて参加しましたが、やっぱり議論に全然ついていけません(涙)。まだまだ修行が足りません。

私は、2月頭から io.jsの Collaboratorとしてプロジェクトに参加してきました。もっぱら開発に集中するためガバナンスやポリシー等の議論などほとんど追っていなかったので、今回慌てて io.js プロジェクト運用などのドキュメントを見直すはめに…

そこで今回、自分自身の復習を兼ねて、現状の io.js プロジェクトがどう運営されているか、この2か月半を振り返りながら書いてみたいと思います。

このエントリーを通じて、これまであまり io.js についてなじみがなかった方も io.js がどういうプロジェクトか知っていただければ幸いです。ちなみに現在交渉中である Joyent Node との統合話は今回のエントリーでは触れないことにします。

2. io.jsの プロジェクト運営

本日時点での io.jsプロジェクトの概要を図にしてみました。github organizationに280アカウント登録されています。

次に、それぞれの役割について記述します。

2.1 Technical Committee

io.js プロジェクトのハイレベルの意思決定を行う機関です。現在9名で構成されています。そのうち6〜7人がJoyent Nodeの現/旧コアメンバー経験者です。他にもV8やJS関連でGoogleの Domenic Denicola さんもオブザーバーとしてTC meetingに参加されています。

現在 Rod と mscdex と私がオブザーバー身分で、最終的には12名になる予定です。TCの人数は、9〜12名程度を想定してますが、明確な定員や任期は決められていません。ですが、これ以上になると集まって合意を取るのが大変だなと話も出てました。またTCは特定企業の色をなくすため、同一組織(正確には同一雇用者)のメンバーの割合を1/3以下にするという規定を設けています。超えちゃった場合は誰かが辞めることになるので、これからは転職先には気を付けないといけませんw。

TCの役割は、以下の通りです。

技術的な方針決定
影響度の大きい変更や意見が分かれるような修正等はTCで議論され、最終的に方針決定されます。
プロジェクトの統制やプロセス管理
今回改めて読み返したところです。https://github.com/iojs/io.js/blob/master/GOVERNANCE.md プロジェクトメンバーの役割や手続き等を決めます。
プロジェクトへの貢献ポリシーの策定
このポリシーは、プロジェクトメンバーだけでなく一般の方々からの io.jsの開発に協力していただくための規定です。 https://github.com/iojs/io.js/blob/master/CONTRIBUTING.md Nodeの頃からPRを出していたので、これまでほとんど目を通していませんでした。 コミットログの書き方やPRの出し方だけでなく、DCO (Developer’s Certificate of Origin:原作者証明書)やCode of Conduct(行動規範)も記載されていたとは、知りませんでした。
Githubリポジトリの管理
これは organization やレポジトリの管理の事じゃないかと思います。実際TCメンバーになるとどういう権限が与えられるかまで知りません。当然事前にTC meetingでの合意が必要でしょうが。
Collaboratorの人選、管理
Collaborator(後述)を推薦して、承認します。Joyent Nodeでは少数精鋭のコアチームでしたが、io.js は、比較的緩いというか幅広く多くの開発者を Collaborator として協力してもらう方針です。自薦・他薦からTCのメンバーがCollaboratorを推薦し、TC meetingで承認します。状況を見てある程度バッチ的に行っています。資格喪失の条件は今のところ見当たらないですね。人数が多くなって活動してない人が増えたりするとその辺整備されるのかもしれません。
2.2 Collaborator

日常的に github で作業を行います。TCメンバーも含み現在30名が登録されており、日本からは古川さんと私が参加しています。

Collaboratorになると github レポジトリのアクセス権や issue/pull requestの管理権限がもらえます。TC meetingで承認されると最初にGoogleハングアウトでガイダンスを受けます。もっともガイダンスと言っても1時間ほどCONTRIBUTINGのやり方を聞くぐらいで、あれこれ細かく言われるわけではありません。
https://github.com/iojs/io.js/blob/master/COLLABORATOR_GUIDE.md
Collaboratorでも自分のPRをマージするには、必ず他のCollaboratorのレビューを受けることが必須です。またCollaborator間で意見の相違がありまとまらなかったら、TCに最終的な判断を仰ぎます。

通常のエンジニアとして技術的な良心?を持って対応をしていけばほとんど大丈夫です。ただ将来規模が大きくなりすぎて収集つかなくなったらどうなるのかなとちょっと心配になったりもします。そうなる前にまた別のプロセスができるんじゃないかと思いますが。

繰り返しますが、io.js のCollaboratorは比較的広く門戸が開かれています。英語でのやり取りは必須ですが、やる気のある方はどしどしgithubでio.jsの開発に参加してください。

2.3 Working Group

io.jsでは、特定の活動を行うためのワーキンググループ(WG)を設けています。現在9つのWGが設立され、活動を開始しています。
https://github.com/iojs/io.js/blob/master/WORKING_GROUPS.md
私はどこにも所属しておらず各WGの詳細を把握していないのですが、機会があれば参加してみたいと思います。
驚くのは、i18nのWGに34もの言語コミュニティが設立されていることです。各国語への io.js 公開情報の翻訳やtwitterアカウントでの活動など、各国のコミュニティの中心としての活動が期待されています。

3. 中から見たio.js

2月の頭から io.jsの Collaboratorとして主にTLS/crypt周り(OpenSSLのバージョンアップ等)の開発を行いました。ここではこれまでを少し振り返って気づいたことを書いてみます。

3.1 本当に速い開発速度

現在の最新リリースは、 iojs-v1.8.1 です。 レポジトリには24のリリースタグが付いていますが、いくつか重複やスキップなどあり io.js-1.0.0 リリース以降わずか約3か月で約20回ほどのリリースが行われた換算です。

ほぼ毎日コミットがマージされタイムゾーン関係なく24時間開発が回っているような印象を受けます。自分がプルリクエストを出すと即レビューされ、レビューコメントを書くそばから修正や返答が返ってきます。まるで世界の超一流エンジニアとgithub経由してペアプロしている感覚。まぁほんと普通のエンジニアからスーパーサイヤ人に変身しないとそのスピードに付いていけない気分ですw

近々では V8 Ver42の導入に伴う iojs-2.0 の開発・リリースが予定されています。io.js 初のメジャーバージョンアップです。

3.2 Semver重視、安定性大事、性能も大切

以前の Node は、安定版リリースに伴い大幅な変更が入り、ユーザのマイグレーションコストが高いと批判を受けていました。それ受け io.js は、互換性の客観的な指標として Semver を非常に重視して短サイクルのリリースを行っています。ソフトウェアが進化する以上変更は避けられませんが、変更指標を厳格に適応して、できるだけユーザがバージョンアップの判断を明確にできるようにするのが狙いです。

先週私が openssl-1.0.2aへバージョンアップを行いましたが、iojs-1.8.0をリリースした直後、opensslのABI非互換であることを前提としてネィティブアドオンの動作を非互換にする修正をリリース時に入れたことが判明しました。本来なら iojs-2.0 にメジャーバージョンを上げる修正となったので、週末の土曜日にどう対応するかかんかんがくがくの大議論が発生しました。結局私がopensslのABI互換性を確認し、なんとか事なき得ましたが io.js が Semver を重視する一つのエピソードでした。

安定性や性能についても重視されています。PRに合わせてCIでビルド・テストを行うようにし、毎回800を超えるテストスクリプトのジョブの完了を確認しています。サーバの過負荷やタイミングで失敗するテストもまだありますが、この数週間でCIとテスト環境は大きく改善されています。まだまだ十分ではありませんが、確実に安定性は向上しつつあると感じています。

3.3 幅広いアーキテクチャサポート

現在 io.jsがサポートする cpu は arm, arm64, ia32, mips, mipsel, x32, x64 の7種類、OSは win, mac, solaris, freebsd, openbsd, linux, androidの7種です。全ての cpu x OS の組み合わせ、49通りが全部動作するわけではありませんが、V8がサポートするアーキテクチャ上はできるだけサポートする方向になっています。

CIでは、このうち22種類のアーキテクチャでビルドとテストを行っています。
https://jenkins-iojs.nodesource.com/job/iojs+any-pr+multi/

特に arm 関連は、ビルドもテストも時間がかかって大変ですが、リリース時は armv7l用のビルドバイナリーを配布して、非力なマシンでユーザがわざわざビルドしなくても使えるよう配慮しています。armv8もCI環境用のサーバの寄付を受け、先日全てのテストが通るようになりました。これだけ幅広くアーキテクチャをサポートするのは開発者として本当に大変なことですが、できるだけいろんな環境で io.js を使ってもらいたいの一心で作業しています。こんなところで io.js が使われるようになったとの報告も待っていますのでお願いします。

以上いろいろ書きましたが、これまで以上に io.js が身近になるよう取り組みたいと思っています。できるだけ皆さんのフィードバックやご協力をお願いします。

華麗なる因数分解:FREAK攻撃の仕組み

1. はじめに

ちょうど今朝 OpenSSLをはじめとした様々なTLS実装の脆弱性の詳細が公表されました。

この InriaとMSRのグループは以前からTLSのセキュリティに関して非常にアクティブに調査・検証をしているグループで、今回も驚きの内容でした。
このグループは、TLSのハンドシェイク時の状態遷移を厳密にチェックするツールを開発し、様々なTLS実装の脆弱性を発見・報告を行っていたようです。
特にFREAKと呼ばれるOpenSSLの脆弱性(CVE-2015-0204)に関しては、ちょうど修正直後の1月初めに
Only allow ephemeral RSA keys in export ciphersuites
で見ていましたが、具体的にどのように攻撃するのかさっぱりイメージできず、あのグループだからまた超絶変態な手法だろうが、まぁそれほど深刻じゃないだろうと見込んでいました。
今回、その詳細が論文で発表されました。いろいろ報告されていますが、自分で攻撃手法を見つけられなかった反省を踏まえ、FREAKについて少し解説してみたいと思います。
まずは脆弱性の背景から。

2. 昔々の米国暗号輸出規制とSSL

その昔、国家安全保障上の理由で昔から暗号技術や暗号を使った製品などの利用や持ち出しは各国で規制されてきました。 Phil ZimmermannがPGPのコードを書籍化して合法的に米国外に持ち出したことは有名な話です。SSLも規制の対象で90年代後半は、日本からは低強度の暗号(40bitや56bit)しか使えない米国輸出版のブラウザーをダウンロードするよう制限されていたり、商用OSやアプリ等は、高強度対応の暗号ライブラリが削除されたものしか使えない状況でした。(たまにftp サイトで米国内専用のが置いてあったりしましたが)
その後2000年ぐらいに規制が緩和され、米国内外に関わらず最高暗号強度の通信が行えるようになりました。

米国外用に使える暗号にはEXPの記号が付いていて、今でも以下のように OpenSSL 実装されたままになっています。

$ openssl ciphers -v |grep EXP
EXP-EDH-RSA-DES-CBC-SHA SSLv3 Kx=DH(512)  Au=RSA  Enc=DES(40)   Mac=SHA1 export
EXP-EDH-DSS-DES-CBC-SHA SSLv3 Kx=DH(512)  Au=DSS  Enc=DES(40)   Mac=SHA1 export
EXP-DES-CBC-SHA         SSLv3 Kx=RSA(512) Au=RSA  Enc=DES(40)   Mac=SHA1 export
EXP-RC2-CBC-MD5         SSLv3 Kx=RSA(512) Au=RSA  Enc=RC2(40)   Mac=MD5  export
EXP-RC4-MD5             SSLv3 Kx=RSA(512) Au=RSA  Enc=RC4(40)   Mac=MD5  export

なかなか古い仕様の機能を廃止できない後方互換性重視が今回の脆弱性の要因の一つでした。

3. ephemeral RSA(一時的RSA)

時はまだ米国暗号輸出規制が厳しかった時代、DESやRC4等の共通鍵暗号方式はSSLのハンドシェイクで動的に決定するので使える強度の暗号をクライアントで利用制限していました。一方、RSA等の公開鍵暗号の制限(512bit)はやっかいなものでした。

そのままでは、サーバ側で米国内からのクライアント向けには1024bit RSA、米国外からは512bit RSAと2種類のサーバ証明書を用意して使い分けないといけなくなります。まぁ面倒です。そこで使われたのが ephemeral RSA(一時的RSA)という方式です。

EXPが付いた輸出向けの暗号方式を利用する場合には、サーバの証明書(通常は1024bit)のRSA公開鍵を使わず、一時的に生成した512bit長の公開鍵をサーバからクライアントに送信して利用するというものです(送るときにはサーバの公開鍵で署名)。
クライアント側は、サーバから送られた512bit長の一時的RSA公開鍵を使って pre_master_secret を送り、両者で同じ master_secret を共 有することになります。
米国内のクライアントからはEXPでない暗号方式を利用するため通常と同じくサーバ証明書RSA公開鍵(1024bit)を使って pre_master_secretのやり取りを行います。これでサーバ証明書1枚だけで米国の暗号輸出規制に適合です。

本来この512bit長の一時的RSAによる鍵交換は、EXPの暗号方式だけに適応されるものでした。しかしOpenSSLでは、EXPでない暗号方式でも利用ができるよう独自に拡張を行っていました。今回のFREAK攻撃は、OpenSSLのこの独自拡張を突いたものでした。

この ephemeral な鍵交換方式は、現在 PFS(Perfect Forward SecurecySecrecy) としてTLSサーバの利用が推奨されています。もっともRSAではなくDH(DHE)やECDH(ECDHE)を利用する方式ですが、今回のFREAK攻撃ではPFSとして低強度RSAを使うことが、脆弱性につながってしまったという なんとも皮肉なものです。PFSの鍵長に関しては、DHEで512bitや1024bitを利用している場合もリスクが高く、2048bit以上が推奨されています。自分が使っているTLSサーバの暗号の鍵長は時代遅れになっていないか常に留意しておきましょう。

4. FREAK攻撃とは

FREAK攻撃は、Factoring attack on RSA-EXPORT Keys の略、輸出向けのRSA因数分解する攻撃です。
以下の4つの条件が必要です。

  1. MiTM(中間者攻撃) ができること
  2. サーバ側でEXPの暗号方式で ephemeral RSA(512bit)が使えること
  3. サーバ側で ephemeral RSAの鍵ペアが使いまわされていること
  4. クライアント側でEXP以外の暗号方式でも ephemeral RSA(512bit)が使えるよう拡張されていること(1.0.1kより前のOpenSSL)

3番目の条件ですが、RFC2246,D. 実装上の注意,D.1. 一時的 RSA 鍵には

512 ビットの RSA 鍵はそれほど安全ではないので、一時的 RSA 鍵はときどき変更するべきである。 典型的な電子商 取引アプリケーションにおいては、その鍵は一日ごと、または 500 トランザクションごと、できればそれ以上の頻度で変更することを提案する。

となっていますが、現状多くのサーバでは立ち上がるとずっと同じ一時的 RSA 鍵を使い続けるようです。nginxでは、
https://github.com/nginx/nginx/blob/master/src/event/ngx_event_openssl.c#L740-L742
にあるよう最初のコールバックで生成したものをずっと使い続けています。

また、TLS1.0の時代では一日ごとの更新が推奨でしたが、今回のFREAKでは EC2 を使って7時間で512bitのRSA鍵を解いてしまったようです。時代は変わってしまいました。

それでは FREAK攻撃をステップ毎に見てきましょう。

ステップ1 事前に一時的RSA因数分解

使いまわされることを前提に事前に一時的RSAの公開鍵を入手しておきます。後は力技、512bitの数字を因数分解して2つの素数を見つけます。

ステップ2 サーバ側を輸出用暗号方式でハンドシェイクするよう改ざん

クライアントとサーバの間に入り込み MiTM攻撃を仕掛けます。
ClientHelloで要求する CipherSuite を輸出用のものに置き換え、サーバ側が一時的RSAで鍵交換するよう仕向けます。
サーバ側は使いまわしの一時的RSA公開鍵を送ります。クライアントは、独自拡張してしまっているので輸出用暗号じゃなくても一時的RSAを使えるようになってます。

ステップ3 pre_master_secretの入手

クライアントから一時的RSAの公開鍵で暗号化した pre_master_secretが送られてきます。既に秘密鍵が事前に計算できているので中身が丸見えです。攻撃者は pre_master_secretからサーバ・クライアントと同じ master_secretを直ちに生成します。

ステップ4 Finishedのハッシュデータの改ざん

TLSハンドシェイクの完了は、Finishedのメッセージに含まれるハンドシェイクデータと master_secretを合わせたハッシュ値を見て、ハン ドシェイクが改ざんされてないことを確認します。攻撃者は master_secretを持っているので自由にハッシュを改ざんして、クライアント・サーバの両者をだまします。

いやー、ほんと見事で感心してしまいます。この他、いろいろTLSハンドシェイクの状態遷移の実装バグをついた脆弱性がいろいろ報告され ています。 Inria と MSRチームのTLSの信頼性を向上させる取り組みはホントすごいなと思います。今後このような不備が根本的に解決されるような新しいプロトコルの取り組みでも始まらないかなと願ったりもします。

ES6時代のGoogle的Good Parts: V8のstrong modeを試す

1. 新しいGoogleのV8実験プロジェクト

巷ではIEの asm.js サポートのアナウンスが話題を集めていますが、実は先月末のTC39の会合でGoogleが今年新しくV8に2つのJavaScript機能の試験実装を進めていることがプレゼンされていました(すっかり見落としてた)。

Experimental New Directions for JavaScript, Andreas Rossberg, V8/Google

このV8実験プロジェクトは、資料によると

SaneScript (strong mode)
より安全なセマンティクスと性能向上が図れるよう一部機能を削減したJSサブセット。
SoundScript*1
TypeScriptをベースとしたより堅固で効率的な型システムの導入。

の2つです。ちょうど今朝知ったのでV8のソースを見てみると、今まさに strong mode の実装が進めれられている最中でした。そこで、本日(2/19)時点で実装されている strong mode の機能がどんなのか知るため、実際に試してみました(現状4つ実装されてます)。

2. V8の strong mode を試す

今日の V8(4.2.0 candidate)をビルドして早速ためしてみます。

2.1. var の禁止
$ ./out/x64.release/d8 --strong_mode
V8 version 4.2.0 (candidate) [console: dumb]
d8> "use strong"; var a;
(d8):1: SyntaxError: Please don't use 'var' in strong mode, use 'let' or 'const' instead
"use strong"; var a;

var の代わりに let や constを使えとのこと。

2.2. オブジェクトプロパティの delete 禁止
d8> "use strong"; const o = {a:1}; delete o.a;
(d8):1: SyntaxError: Please don't use 'delete' in strong mode, use maps or sets instead
"use strong"; const o = {a:1}; delete o.a;

プロパティを削除しちゃうぐらいならMap/Setを使えとのこと。

2.3. 型変換する比較演算子の禁止
d8> "use strong"; 1==1;
(d8):1: SyntaxError: Please don't use '==' or '!=' in strong mode, use '===' or '!==' instead
"use strong"; 1==1;

暗黙の型変換する比較演算子の問題については昔からよく言われてますね。ただtruthy/falthyへの変換(ToBoolean)は許される見込みのようです。

2.4. ステートメントを省いた条件式の禁止
d8> "use strong"; while(true);
(d8):1: SyntaxError: Please don't use empty sub-statements in strong mode, make them explicit with '{}' instead
"use strong"; while(true);

あまり自分で使った覚えがないけど、{}を付けない条件式はダメとのこと。

3. 今後は?

その他に資料には、

スコープ
declaration前に変数を利用することを禁止。ただし相互に参照している再帰関数などでの利用は許可
Class
オブジェクトのfreezeやインスタンスのseal化等々
Array
要素の歯抜け禁止、accesssorメソッドの禁止、lengthを超えた要素のアクセス禁止等々
Function
argumentsオブジェクトの禁止、呼び出し引数の厳密化等々

などの機能変更が挙げられています。まさにES6時代のGoogle的Good Parts集だと思います。

strong-mode を利用するにはフラグ(--strong_mode もしくは --use_strong)が必要ですが、Chromeのリリースに合わせてV8がブランチカットされれば、少し後に io.js のCanary版(予定)で利用できるようになります。
まだ実験初期段階で今後どこまで変わってどう標準化にフィードバックされるのか全く未知数です。でも今後ES6のコードを書く場合には、ここはMap/Setが使えるか、var使わなくてもいいのかなど、少し頭の隅にいれておくといいかもしれません。

SoundScript の方はまだ資料だけですが、Q3/Q4でどう実装されるのがこれから楽しみです(時間がないのでこの話題はまたの機会に)。

*1:soundは、「〔構造が〕堅固な、安定した, 〔考えなどが〕理にかなった、正当な 」の意味じゃないかと思います。日本人には音楽的な意味に思えてちょっと紛らわしいです。

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をベースとします