ぼちぼち日記

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

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