証明書ピンニング(Certificate Pinning)
概要
証明書ピンニング(Certificate Pinning) は、クライアントが接続先サーバーの証明書または公開鍵を事前に記録(ピン留め)し、接続時に提示された証明書がピン留めされたものと一致するかを検証する技術です。一致しない場合、たとえ正規の CA から発行された証明書であっても接続を拒否します。
ピンニングが解決する問題は、PKI のトラストモデルの構造的な弱点です。ブラウザーのトラストストアには数百の CA が含まれており、どの CA でも任意のドメインに対して証明書を発行できます。2011 年の DigiNotar 事件では、侵害された CA が google.com の不正証明書を発行しました。ピンニングを使えば、ドメインオーナーが信頼する特定の CA または特定の公開鍵だけを受け入れるように制限できます。
ピンニングの実装方式には複数のバリエーションがあります。ブラウザー向けの HTTP Public Key Pinning(HPKP)は RFC 7469(2015 年)で標準化されましたが、運用リスクの高さから Chrome が 2018 年に廃止し、他のブラウザーも追随しました。現在ピンニングが使われるのは、主にモバイルアプリとデスクトップアプリケーションです。
仕組み
ピンニングには対象の粒度や配信方法が異なる複数の方式があり、用途に応じて使い分けます。
ピンニングの対象
ピンニングの対象は大きく 3 つに分類できます。
証明書ピンニングは、サーバー証明書全体をピン留めします。証明書が更新されるとピンが無効になるため、証明書の更新のたびにアプリのアップデートが必要です。
公開鍵ピンニングは、証明書内の公開鍵(SubjectPublicKeyInfo)をピン留めします。証明書を更新しても同じ鍵ペアを使えばピンは有効なままです。ただし鍵漏洩時には鍵の変更が必要になり、ピンの更新も必要です。
CA ピンニングは、中間 CA またはルート CA の公開鍵をピン留めします。その CA が発行した証明書であれば受け入れるため、柔軟性が高い反面、ピンニングの粒度は粗くなります。
HPKP(HTTP Public Key Pinning)の歴史
RFC 7469 で標準化された HPKP は、HTTP レスポンスヘッダーで公開鍵のハッシュを配信する方式でした。
Public-Key-Pins: pin-sha256="base64=="; pin-sha256="backup-base64=="; max-age=5184000
HPKP のヘッダーには最低 2 つのピンが必要でした。1 つは現在の鍵、もう 1 つはバックアップ用の鍵です。max-age でピンの有効期間を指定します。
HPKP が廃止された理由は次のとおりです。
設定ミスのリスクが極めて高いことが最大の理由です。誤ったピンを長い max-age で配信した場合、そのドメインへのアクセスが max-age の期間中完全に不能になります(自殺ピンニング)。バックアップ鍵を紛失した場合も同様です。
攻撃者が HPKP を悪用して、侵害したサイトに自分の鍵のピンを設定し、正規の管理者がドメインを取り戻しても接続をブロックし続ける「ランサムピンニング」攻撃も理論的に可能でした。
Chrome は 2018 年 5 月の Chrome 67 で HPKP のサポートを廃止しました。Firefox も 2019 年に廃止しています。RFC 7469 の後継は作成されていません。代替として Certificate Transparency が推奨されています。
モバイルアプリでのピンニング
HPKP の廃止後も、モバイルアプリでは証明書ピンニングが広く使われています。アプリのコード内に信頼する公開鍵のハッシュをハードコーディングし、TLS 接続時にサーバー証明書と照合します。
Android では network_security_config.xml でピンニングを宣言的に設定できます。
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2027-01-01">
<pin digest="SHA-256">base64EncodedPinHash==</pin>
<pin digest="SHA-256">backupBase64EncodedPinHash==</pin>
</pin-set>
</domain-config>
</network-security-config>
iOS では NSAppTransportSecurity の設定やコードレベルでの URLSessionDelegate 実装でピンニングを行います。
ブラウザーのビルトインピン
Chrome は HPKP を廃止しましたが、Google 自身のドメイン(google.com、youtube.com など)に対するビルトインピンは引き続き維持しています。これらのピンはブラウザーのソースコードにハードコーディングされており、HTTP ヘッダーではなくバイナリ更新で管理されます。
設定例
ピンニング用の公開鍵ハッシュ取得と、curl による接続テストの手順を示します。
OpenSSL で公開鍵のハッシュを取得
ピンニング用の公開鍵ハッシュを取得するには次のコマンドを使います。
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | openssl enc -base64
YZPgTZ+woNCCCIW3LH2CxQeLzB/1m42QcCTBSdgayjs=
この Base64 エンコードされたハッシュ値をピンとして使用します。
curl でピンニングを使った接続テスト
curl --pinnedpubkey "sha256//YZPgTZ+woNCCCIW3LH2CxQeLzB/1m42QcCTBSdgayjs=" https://example.com
--pinnedpubkey オプションにピンを指定すると、curl はサーバーの公開鍵がピンと一致しない場合に接続を拒否します。
確認方法
openssl でサーバー証明書の公開鍵ハッシュを取得して、アプリに設定されたピンと一致するか確認します。
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | openssl enc -base64
中間証明書のハッシュも同様に取得できます。
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | openssl enc -base64
外部の視点からも確認したい場合は、Labee Dev Toolbox の SSL Cert API を使うと、外部の視点から見た結果を取得できます。
curl "https://labee.dev/api/ssl-cert?hostname=example.com"
{
"success": true,
"data": {
"hostname": "example.com",
"port": 443,
"reachable": true,
"status": 200
},
"error": null,
"meta": { "responseTime": 123 }
}
data.reachable が true であれば HTTPS 接続が成立しています。ピンニングの検証はクライアント側で行われるため、この API ではピンニングの成否は判定できませんが、サーバーの証明書が有効であることの基本確認として使えます。
よくある問題
ピンニングは証明書や鍵の更新時にアプリの接続障害を引き起こすリスクが高く、運用設計が重要です。
証明書更新時にアプリが接続不能になる
証明書ピンニング(証明書全体のピン留め)を使っている場合、サーバー証明書を更新するとピンが一致しなくなり、アプリが接続を拒否します。公開鍵ピンニングを使い、証明書更新時に同じ鍵ペアを再利用することで回避できます。ただし、バックアップピンの設定が必須です。鍵漏洩で鍵を変更する事態に備えて、バックアップ鍵のピンをあらかじめ設定しておきます。
CA の中間証明書変更でアプリが接続不能になる
CA ピンニングを使っている場合、CA が中間証明書を変更するとピンが一致しなくなります。Let’s Encrypt は 2024 年に中間証明書を R3 から R10/R11 に切り替えました。中間 CA のピンを使っていたアプリはこの切り替えで影響を受けました。CA ピンニングを使う場合は、CA の中間証明書の更新予定を把握しておく必要があります。
ピンニングがデバッグやセキュリティ監査を妨げる
企業ネットワークでは、TLS インスペクション(中間者プロキシ)を使って通信内容を検査することがあります。ピンニングが設定されたアプリはプロキシの証明書を拒否するため、通信の検査ができません。Android 7.0 以降ではデバッグビルドでのみカスタム CA を信頼する設定が可能です。
アプリの強制アップデートが必要になるリスク
ピンの更新にはアプリのアップデートが必要です。ユーザーがアップデートしなければ、古いピンのまま接続を試み続けます。ピンの有効期限(expiration)を設定し、期限切れ後はピンニングを無効化するフォールバックを設計に含めておきます。