公開鍵ピンニングについて

ついに、インターネット技術タスクフォース(IETF)がRFC7469 HTTP公開鍵ピンニング拡張(HPKP)を発表しました。このアイデアを出してくれた同僚のRyan Sleevi、Adam Langley、Chris Evansに感謝します。また、RyanとChris EはRFCの最終稿に先立つ大量のドラフトの執筆を助けてくれました。そして、ドラフトにコメントし、RFCとして公開できるまでにしてくれたIETFの多くの参加者にも感謝します。

ピンニングとは何か? 何を解決できるのか?

HPKPはWeb PKIの大きな問題の1つを解決する試みです。その問題とは、基本的に認証局(CA)や中間認証局は、どのWebサイトにもエンドエンティティ(EEまたは”リーフ”)証明書を発行することができてしまうことです。例えば、mail.google.comの証明書が”Google Internet Authority G2″によって発行されたとしても、その後にルート認証局の”Geo Trust Global CA”が証明書を発行することもできます。同様に、あまり知られていないオランダのCAもmail.google.comの証明書の発行が可能です。そこで私たちは、何らかの方法でクライアントに不正な証明書を信頼させないようにしたいのです。

The Chrome Certificate Viewer
showing the mail.google.com issuance chain.
Chrome証明書ビューアの、mail.google.comの発行チェーン

度々、この問題を解決するために、Webを区域ごとに分ける方法を提案する人がいます。クライアントが自分たちの国のCAだけを信頼するように設定するか、X国のCAがY国やZ国の組織へ証明書を発行することを禁止するか、または、その両方をするということです。これには、いくつかの問題点があります。極めて重要なのは、そもそもWebというのは世界中に広がっているので、そのおかげで様々なすばらしい恩恵を受けられる、ということです。さらに、ある組織が”どの”国のものかというのは常に明白ではないので、どのCAが組織の証明書を発行”しなければいけなかった”のかは、常にはっきり分かるわけではありません。

これには、完璧な”ゴールデンルート(薬)”はありません。証明書発行の組合せを、最も確実に不正(意図的であれ偶発的であれ)を防ぎ、かつ最小限の組合せで手に入れることは不可能です。Webを区分してしまえば、実際に誤って発行する恐れを減らすのではなく、その価値を減らしてしまうことになります。そこで、何か他の解決策が必要なのです。

HPKPは、その解決策の1つです。Webサーバが、(ブラウザなどの)クライアントにX.509の証明書チェーンの中に少なくとも1つの公開鍵のセットが常にあることを想定するように伝えることができます。そうでない場合は、その証明書チェーンを拒否します。このようにして、WebサイトのオペレータはWebを区分せずに、自分たちのサイトの証明書を発行する認証局を、効果的に減らすことができるのです。

ピンニングはどのように機能するのか?

鍵ピンニングを理解するために、まずは典型的でシンプルなケースを考えてみましょう。SSHホスト鍵のマネジメントです。クライアントが一度も認識したことのないSSHサーバに初めて接続すると、以下のように表示されます。

chris@goatbeast:~ $ ssh freebsd
The authenticity of host 'freebsd (10.0.0.4)' can't be established.
ECDSA key fingerprint is b0:79:74:0f:58:20:80:fd:c7:47:33:d6:9c:40:df:20.
Are you sure you want to continue connecting (yes/no)? 

私のFreeBSDサーバは、身元を証明するためにサーバの公開鍵をSSHクライアントに提示します。問題は、私のクライアントがこのペア(サーバの名前と公開鍵)について何も知らないことです。そこで、この問題を解決するかどうかを質問されます。これは鍵の指紋が正しいかどうかの帯域外のチェックで、「はい」か「いいえ」で答えます。

「はい」と答えたと仮定します。今後、私のクライントはこのサーバがこの鍵だけを提示すると想定するでしょう。仮に私のサーバが異なる鍵を提示するとします。それが正当な鍵の交換やサーバの名前の変更、または実際のネットワーク攻撃が原因であったとしても、私のクライアントは接続を拒否し、次のようなメッセージを表示します。

No ECDSA host key is known for freebsd and you have requested strict checking.
Host key verification failed.

これがSSHで機能する理由は、SSHを使うほとんどのユーザがエキスパートだからです。システム管理者、DevOpsエンジニアまたはソフトウェアエンジニアです。彼らはこのエラーメッセージを理解しています。鍵の証明が失敗した時はどうすればいいかを知っているので、対応することができます。サーバを利用する人々のコミュニティは小さいので、「ねえ、サーバの鍵を交換した?」と会話すればいいだけです。

しかし、ワールドワイドWebでは、この方法はうまくいきません。日常的に鍵の交換がされるので、最初の接続と、暗号認証に関して特別な知識を持たないブラウザを使う人々のために、スムーズな導入が必要です。なので、この導入をするためには、まだ私たちはCAに頼らなければいけません。そして、私たちのWebサーバの暗号識別を柔軟に連続させるために、いまだに証明書のチェーンを使うのです。SSHの場合のように1つのエンドエンティティをピンニングするのではなく、潜在的に証明書チェーンのいくつかの場所において鍵のセットをピン留めします。次に紹介するように、これは信頼度をかなり高められる方法です。

HPKPによるピンの認証とは、基本的には集合の交わりです。署名済み証明書チェーン内の公開鍵一式のうちどれかが、サーバが不正ではないとアサートした(“ピン留めされた”)鍵のどれかと一致していますか? もしそうならピンの認証は成功しています。そうでない場合、クライアントはSSHクライアントのように動作し、無効な接続を中断する必要があります。Chromeでは、このように表示されます。

A screenshot of Chrome rejecting an
invalid pin set.
Chromeが無効なピンを拒否している。

自分のサイトでどうやってHPKPを設定すればいいの?

注意:Webサイト向けの公開鍵ピンニングは大きな危険を伴う可能性があります。クライアントがピン留めした鍵一式が現時点で認証を行っていたとしても、設定ミスがあれば、状況の変化などにより1週間後、または1年後に認証をストップしてしまうこともあり得るのです。その場合は自分自身のサイトのサービスを拒否してしまうことになり、接続できなくなります(これを”サイトの文鎮化”と呼びます)。Web PKIをよく理解していて、サイトの暗号化IDをきちんと管理できているという確固とした自信がない限り、公開鍵ピンニングは使うべきではありません。確信が持てるまでは、通常の、ピンニング機能を使わないWeb PKIにしておきましょう。

証明書チェーンの公開鍵をピンニングするには、いくつかステップを踏む必要があります。

  1. 供給されていて、かつクライアントによって認証されている証明書チェーンを算定する。
  2. チェーンのどこにピンニングするか決める。
  3. バックアップ用のピンを1個以上設定する。
  4. (テスト用)サーバを設定して短期間有効のHPKPヘッダを発行し、テストする。
  5. 慣れてくるにしたがって徐々にピンの有効期限を延ばす。

次の項ではそれぞれのステップについて説明します。

サイトの証明書チェーンを算定する

証明書ビューアのスクリーンショットで見ていただいたように、サイトの証明書はチェーンの最後にあり、そのチェーンには(通常は)最低でも3つの証明書が存在します。ルート証明書またはトラストアンカー、1つまたは複数の中間発行者による証明書、そしてエンドエンティティ(EE)証明書の3つです。通常WebサーバはTLSハンドシェイクの一環として、ルートまたはトラストアンカー以外のすべての証明書を供給する必要があります。クライアントはトラストアンカー一式を保持して、トップに署名された、最も中間にある1つを調べます。たまに、サーバがEEしか発行できず、クライアントが中間発行者を発見してしまうというケースがありますが、これがよく問題を起こします。一般的には、中間発行者を含むチェーンを供給しなくてはならないと思っていてください。

しかし、あなたが供給するチェーンは、必ずしもクライアントがチェーンを認証する時に(再)構築するチェーンであるとは限らない、ということを認識しておく必要があります。これは相互認証によるもので、一般に、クライアントが認証チェーンを構築し認証する、驚くほど複雑な方法です。部分的にはこれを制御できます。つまり、よく知られている1つのトラストアンカーにつながっている、よく知られた中間発行者との正当なチェーンの供給を保証すればいいのです。そしてその場合でも、クライアントがどのチェーンを実際に構築し認証するかということを確認するため、様々なクライアントでテストする必要があります。

これは重要な点で、クライアントはピンの認証をチェーン認証の時に構築したチェーン上で行うのですが、必ずしもあなたが供給しているチェーンである必要はありません。ですから、残念ながら、ただ単にあなたが供給しているチェーンの鍵をピンニングしただけではピンの認証が成功したと単純には言えません(カバーされる範囲を向上させるには次の項を見てください)。
サイトによっては、クラスタの中の異なるサーバに対してそれぞれ異なるEE証明書を用いるものもあります。各EEは同じ発行者から発行されているかもしれませんが、そうでない場合もあります。違う場合、状況がかなり複雑になる可能性があり、鍵のピンニングはうまくいかないかもしれません(あるいは、非常に広範囲のピンだけ有効となります)。

どの鍵をピンニングするか決める

さて、証明書チェーンのクライアントが何を構築し認証するか把握できたところで、そのチェーンの中のどこにピンニングするか決定します。説明しやすくするため、シンプルなサーバのデプロイメントモデルを想定してみましょう。

  • とある会社はデータセンターを2つ保有しており、それぞれWebサーバのクラスタをホストしている
  • 1つのクラスタにはEE証明書を取得しており、もう1個のクラスタにはもう1つEE証明書を取得している
  • EEは両方とも同じCAから発行されている
  • CAからの発行は1つの中間局経由である

したがって、2つの証明書チェーンが稼働中ということになります。つまりCA->中間局->EE1と、CA->中間局->EE2です。2つのクラスタのサーバが正しく設定され、中間局->EE1(データセンタ1用)と、中間局->EE2(DC2用)のチェーンを提供します。

今度は、簡易化のため、クライアントが期待通りにCA証明書を介して実際にパスを設定すると仮定しましょう(繰り返しになりますが、現実的にはこのように単純に仮定することはできません)。

どの証明書の鍵をピン留めするか、次の4つの証明書から選択できます。CA証明書、中間証明書、EE1証明またはEE2証明書です。 ピンニングの意味合いはレベルによって異なります。

EE証明書へのピンニング

サイトオペレータに対して、SSHと同じくらいセキュリティが強化されていることを保証します。つまり、証明書が誤って発行された場合、クライアントは証明書チェーンを拒否します。マイナス面は、サーバオペレータが特定のEE証明書で自分たちを拘束してしまう点です。単純に、同じ発行者から発行された新しいEE証明書を取得するやり方ではうまくいきません(とは言いつつも、ピンのバックアップについては下記をご覧ください)。

中間証明書へのピンニング

ピン留めされた中間証明書に対してのみ、誤って発行される脅威が減少します。現状で著しい改善が見られますが、厳密にはEE証明書のみに対するピン留めほど安全性は高くありません。その証拠に、これらの発行者からサイトに新しく発行された証明書は、いずれもピンの認証を通過していきます。

ルート証明書へのピンニング

中間証明書へのピンニングと類似しています。同じ組織がルート証明書と中間証明書の秘密鍵を管理するのはよくあることです。それ以外の場合だと、”中間証明書へのピンニング”とは別物になります。サイトオペレータは信頼を深めるか、または誤った発行を防ぐために別の秘密鍵保持者を信頼するでしょう。

EE証明書、中間証明書、EE証明書とルート証明書、中間証明書とルート証明書、または3種類の証明書全てといった、証明書チェーンの多様なレベルにピン留めすることで、サイトオペレータはサイトが使い物にならなくなることを避けられるという大きな安心感を得て、より多くの発行者を信頼できるようになります。

ピンのバックアップを生成する

RFCの文章には、「ホストは必ずピンのバックアップを準備しなくてはならない」、と明記されています。そしてそのピンはクライアントが検証するチェーンに存在しないピンでなければなりません。これはあなた自身のためです。もし秘密鍵の操作を誤り、サイトの暗号鍵を変更して新しく証明書を取得しなくてはいけなくなった場合、サイトのダウンタイムは避けたいものです。ましてや、使い物にならないのは許しがたいことです。クライアントが鍵のバックアップを前々からピン留めしていない限り、サイトはmax-ageがタイムアウトするまで文鎮化してしまうでしょう。
今回の例では、バックアップ用のEE証明書をピンのバックアップとして使用します(または代替の中間証明書かルート証明書をバックアップに使うこともできますし、おそらくそうすべきでしょう。そのうえ、大惨事に見舞われる前に正式な発行者から署名されたバックアップを取っておくのが最も好ましいことです。そうすれば、すぐにまた使い始めることができます)。

下記のスクリプトは新しい鍵と、関連した”証明書署名要求”(CSRと呼ばれ、署名をCAに請求するために送るメッセージです)を生成します。サイトのプライマリおよび/またはバックアップ用のEE証明書の鍵とCSRは、この方法で生成されます。もう一度言いますが、必要な時にすぐに使うことができるように、正式な発行者が発行した証明書からバックアップの鍵を取得するのが最も安全な方法です。

#!/bin/sh

openssl genrsa -out "$1".key 2048
openssl req -new -key "$1".key -out "$1".csr

HPKPヘッダのテストを行う

下記のスクリプトを使うことで鍵がピン留めされます。スクリプトはX.509証明書(PEM形式)か証明書署名要求(同じくPEM形式)のどちらかを読み込み、サブジェクト公開鍵情報(SPKI)のフィールドの部分を抜き出します。そのSPKIのハッシュ値とSHA-256のハッシュ値を計算して、base64にエンコードします。

#!/bin/sh

type="x509"
case "$1" in
  x509)
    type="x509"
    ;;
  req)
    type="req"
    ;;
  *)
    echo "Usage: $0 x509 certificate-pathname"
    echo "       $0 req certificate-signing-request-pathname"
    exit 1
esac

openssl $type -noout -in "$2" -pubkey | \
  openssl asn1parse -noout -inform pem -out public.key
openssl dgst -sha256 -binary public.key | openssl enc -base64

このスクリプトのアウトプットをPKPヘッダに入力します。 私が現在nonfreesoftware.orgのために使っているApacheのヘッダのディレクティブを例にとってみましょう(行を調整しています)。

Header add Public-Key-Pins "max-age=500; includeSubDomains;
    pin-sha256=\"wBVXRiGdJMKG7vQhr9tZ9br9Md4l7cO69LF2a88Au/o=\";
    pin-sha256=\"fv1+PWVvrBGKldX8uRtODY3sDbBKlsJOa48mI9s+6Mk=\";
    pin-sha256=\"lT09gPUeQfbYrlxRtpsHrjDblj9Rpz+u7ajfCrg4qDM=\""

私はエンドエンティティ、発行者、そしてバックアップの鍵をピン留めしました。そしてサイトが長い時間使えなくなることを防ぐため、max-ageは500秒に設定しました。もちろん、正規名ではなく、そのサイトが唯一保持している代替の名前に対してピン留めをしました(noncombatant.orgという名前です)。

最後に、クライアントがその鍵のピンを読み込んで理解したか確認します。下記のスクリーンショットを見ると、Chromeが私の公開鍵のピンのヘッダを認識したことが分かります。

A view of chrome://net-internals/#hsts
showing key pins for nonfreesoftware.org
chrome://net-internals/#hstsはChrome上のHSTSとHPKPデータベースの状態にクエリを行うことを許可しています。