ぼちぼち日記

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

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のリリースは一体全体どうなるのか、先行きはますます見えなくなりました。