証明書ピンニング

特定の証明書や公開鍵のみを信頼し、中間者攻撃を防ぐセキュリティ手法

モバイルTLS

証明書ピンニングとは

証明書ピンニング (Certificate Pinning / SSL Pinning) は、通信相手の証明書または公開鍵を、アプリケーション側であらかじめ決めたものに限定する仕組みである。OS の証明書ストアによる検証を置き換えるのではなく、その検証を通ったうえでさらに「埋め込んだ鍵と一致するか」を要求する。信頼された CA から発行された偽の証明書を中間者攻撃 (MITM) で提示されても、ピンと一致しなければ接続を拒否できる。

なぜ必要か

通常の TLS 検証は、OS にインストールされた CA (認証局) の証明書を信頼する。しかし:

  • 企業のプロキシが独自の CA 証明書をインストールし、通信を傍受する
  • 攻撃者がユーザーのデバイスに不正な CA 証明書をインストールする
  • CA 自体が侵害される (2011 年 8 月、DigiNotar が発行した *.google.com の不正な証明書が発覚し、ブラウザ側は同社のルート証明書への信頼を取り消した)

証明書ピンニングは、これらの経路で差し込まれた証明書を「信頼された CA が発行してはいるが、自分が想定した鍵ではない」として弾ける。逆に言えば、防げるのは通信相手を取り違える種類の攻撃だけである。Apple は「ピンニングはアプリの信頼要件を緩めることはできず、厳しくすることしかできない」と明記している。ピンを打ってもなお、OS の既定の信頼要件 (チェーン・有効期限・ホスト名) は満たす必要がある。

ピンニングの種類

ピンニングは「チェーンのどこの、何のハッシュを固定するか」で 3 通りに分かれる。強度の差というより、サーバー側の証明書更新でアプリが壊れる頻度の差である。

種類ピン対象証明書更新時の影響
証明書ピンニング証明書全体のハッシュ証明書更新でアプリ更新が必要
公開鍵ピンニング公開鍵のハッシュ同じ鍵で再発行すれば影響なし
CA ピンニング中間 CA の証明書同じ CA で発行すれば影響なし

公開鍵ピンニングが最も実用的だ。証明書を更新しても、同じ鍵ペアで再発行すれば公開鍵は変わらない。ピン値の表し方も公開鍵ハッシュが標準になっている。iOS の設定キー SPKI-SHA256-BASE64 は「X.509 証明書の DER エンコードされた ASN.1 Subject Public Key Info 構造の SHA-256 ダイジェストを Base64 で表したもの」と定義され、Android のネットワークセキュリティ設定も同じく X.509 証明書の SubjectPublicKeyInfo の SHA-256 を指定する (指定できるダイジェストは SHA-256 のみ)。

チェーンのどの段を固定するかは環境で変わる。Node.js の公式ドキュメントは、公衆インターネット向けでは発行者 (中間 CA) の公開鍵を、機密性の高い環境ではサービス自身の公開鍵を固定するのが一般的だと書き添えている。Apple も、サーバーの鍵ではなく CA の公開鍵を固定すればサーバー証明書を入れ替えてもピン設定を変えずに済むとして、CA 側の鍵を勧めている。

モバイルアプリでの実装

iOS も Android も、検証コードを書かずに設定ファイルでピンを宣言する仕組みを持つ。2026 年 8 月時点では、自前の証明書検証を実装するよりこちらが先に来る選択肢である。

iOS は Info.plist の App Transport Security にピン設定を書く。

<!-- iOS: Info.plist (App Transport Security) -->
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSPinnedDomains</key>
  <dict>
    <key>example.org</key>
    <dict>
      <key>NSIncludesSubdomains</key><true/>
      <!-- CA の公開鍵を固定する (サーバー証明書を入れ替えても設定変更が不要) -->
      <key>NSPinnedCAIdentities</key>
      <array>
        <dict><key>SPKI-SHA256-BASE64</key><string>base64PinValueCA=</string></dict>
      </array>
    </dict>
  </dict>
</dict>

NSPinnedCAIdentities の代わりに NSPinnedLeafIdentities を書けば、サーバー証明書自身の鍵を固定できる。NSIncludesSubdomains が届く範囲は 1 段だけで、example.org の設定は math.example.org を含むが advanced.math.example.org には及ばない。ピン値は証明書から次の手順で取り出す。

openssl x509 -in example.crt -pubkey -noout |
  openssl pkey -pubin -outform der |
  openssl dgst -sha256 -binary |
  openssl enc -base64

Android は res/xml/network_security_config.xml<pin-set> を書く。チェーンの中に列挙した鍵のいずれかが含まれていれば検証を通す仕組みなので、バックアップ用の鍵を必ず併記する (無いと鍵の入れ替えのたびにアプリ更新が必要になる)。

<network-security-config>
  <domain-config>
    <domain includeSubdomains="true">example.com</domain>
    <pin-set expiration="2027-01-01">
      <pin digest="SHA-256">base64PinValue1=</pin>
      <pin digest="SHA-256">base64PinValue2=</pin>
    </pin-set>
  </domain-config>
</network-security-config>

認証チャレンジのハンドラーで自前に検証する場合は、鍵の一致だけを見て接続を成立させてはいけない。サーバーの信頼オブジェクトをそのまま資格情報として返す実装は、チェーン・有効期限・ホスト名という既定の検証結果を確かめずに通してしまう。ピンニングは既定の信頼要件に条件を足すものなので、既定の検証が成功したことを確認した上でピン照合を重ねる順序になる。

証明書ピンニングのリスク

アプリのロックアウト

ピンニングした証明書が期限切れになり、新しい証明書の公開鍵が異なる場合、アプリが一切通信できなくなる。アプリのアップデートを配布するまでユーザーはアプリを使えない。

対策:

  • バックアップピンを設定する (現在の鍵 + 次回更新用の鍵)
  • 公開鍵ピンニングを使い、証明書更新時に同じ鍵で再発行する
  • 鍵が変わったときの復旧経路を先に決めておく。Apple は、ピン照合が失敗したときに見せるフォールバックの体験と、新しいピン設定をアプリ更新で届ける復旧の道筋を用意するよう勧めている
  • Android の <pin-set expiration="..."> は期限を過ぎるとピンニングを行わなくなる。公式ドキュメントは、更新していないアプリが接続不能になるのを防ぐ手段として説明する一方で、期限を設けると攻撃者にピンを回避する余地を与え得るとも明記している

デバッグの困難さ

開発中にプロキシ (Charles, mitmproxy) で通信を傍受できなくなる。Android にはデバッグ用の上書き設定 (debug-overrides) があり、そこに置いたトラストアンカーを使う場合はピンニングを迂回する扱いにできる。この上書きは android:debuggable が true のビルドでしか適用されないため、リリースビルドの検証を弱めずにデバッグ時だけ通せる。

Web での証明書ピンニング (非推奨)

HTTP の Public-Key-Pins ヘッダー (HPKP) は Chrome 72 で削除された (このバージョンの安定版リリースは 2019 年 1 月)。Chrome の機能ステータスに残された理由は、採用がきわめて少なかったこと、そして証明書の誤発行に対する保護にはなる一方で、サービス不能 (自分のサイトへ到達できなくなる) と敵対的ピンニングのリスクを生むことだった。

2026 年 8 月時点で、これに代わるヘッダーは用意されていない。Web での誤発行対策は Certificate Transparency (CT) の側にある。CT は、誤って発行された証明書や悪意で発行された証明書を検知できるようにする仕組みで、ドメイン所有者は監視サービスを使って自分のドメイン向けに証明書が発行されたときに通知を受け取れる。ヘッダー 1 行で全訪問者の接続を止めうるピンニングと違い、監視は可用性を犠牲にしない。

サーバー間通信での実装

ブラウザ向けのヘッダーが無くなっても、サーバー間通信のクライアント側には同じ考え方でピンを足せる。Node.js の checkServerIdentity オプションに渡した関数は、組み込みのホスト名検証関数の「代わりに」呼ばれる。したがって自前の関数を渡すときは、組み込みの検証を自分で呼び直す必要がある。この関数は、信頼された CA から発行されているかといった他の検査をすべて通った後にだけ呼ばれる。判定の失敗は例外ではなく Error を返して伝える。

// Node.js: 既定の検証を通した上でピンを照合する
import { checkServerIdentity } from 'node:tls';
import { Agent, request } from 'node:https';
import { createHash } from 'node:crypto';

const pubkey256 = 'base64PinValue1=';

const options = {
  hostname: 'api.example.com',
  port: 443,
  path: '/',
  checkServerIdentity: (host, cert) => {
    // 組み込みのホスト名検証を置き換えてしまうので、まずそれを呼ぶ
    const err = checkServerIdentity(host, cert);
    if (err) return err;
    // 公開鍵のピン (HPKP の pin-sha256 に相当)
    if (createHash('sha256').update(cert.pubkey).digest('base64') !== pubkey256) {
      return new Error('pinned public key mismatch');
    }
    // 証明書そのものを固定するなら cert.fingerprint256 (DER の SHA-256) と比べる
  },
};
options.agent = new Agent(options);
request(options, (res) => { res.resume(); }).end();

導入するかの判断

ピンニングは、可用性を証明書運用と引き換えにする仕組みである。Apple は「ほとんどの場合ピンニングは必要なく、避けるべきだ」「本当に必要な場合にだけ慎重に配備すること」と述べている。判断の材料は次の 3 点になる。

  • 通信相手が自分たちの管理下にあり、鍵の更新時期を自分で決められるか。第三者の API を相手にピンを打つと、相手の鍵交換でアプリが止まる
  • 鍵が変わったときにアプリを更新して全ユーザーへ行き渡らせるまでの時間を許容できるか。ストア審査と更新の浸透にかかる期間が、そのまま障害時間になる
  • バックアップピンと、ピンを外す判断の手順が用意されているか

守りたいものが「利用者のデバイスに入り込んだ CA による傍受」であれば、ピンニングは有効な手段である。目的が誤発行の検知であれば、CT ログの監視のほうが可用性を犠牲にせずに済む。

この記事は役に立ちましたか?

関連用語

関連する記事