証明書ピンニング

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

モバイルTLS

証明書ピンニングとは

証明書ピンニング (Certificate Pinning / SSL Pinning) は、アプリケーションが特定の証明書または公開鍵のみを信頼し、OS の証明書ストアに依存しない検証を行うセキュリティ手法である。中間者攻撃 (MITM) で偽の証明書が提示されても、ピンニングされた証明書と一致しなければ接続を拒否する。

なぜ必要か

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

  • 企業のプロキシが独自の CA 証明書をインストールし、通信を傍受する
  • 攻撃者がユーザーのデバイスに不正な CA 証明書をインストールする
  • CA 自体が侵害される (2011 年の DigiNotar 事件)

証明書ピンニングは、これらのシナリオでも安全な通信を保証する。

ピンニングの種類

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

公開鍵ピンニングが最も実用的だ。証明書を更新しても、同じ鍵ペアで再発行すれば公開鍵は変わらない。

モバイルアプリでの実装

// iOS: URLSession でピンニング
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,
                completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    guard let serverTrust = challenge.protectionSpace.serverTrust,
          let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }
    let serverKey = SecCertificateCopyKey(certificate)
    let pinnedKey = loadPinnedPublicKey()
    if serverKey == pinnedKey {
        completionHandler(.useCredential, URLCredential(trust: serverTrust))
    } else {
        completionHandler(.cancelAuthenticationChallenge, nil) // 拒否
    }
}

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

アプリのロックアウト

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

対策:

  • バックアップピンを設定する (現在の鍵 + 次回更新用の鍵)
  • 公開鍵ピンニングを使い、証明書更新時に同じ鍵で再発行する
  • ピンニングの無効化メカニズムを用意する

デバッグの困難さ

開発中にプロキシ (Charles, mitmproxy) で通信を傍受できなくなる。デバッグビルドではピンニングを無効化する。

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

HTTP の Public-Key-Pins ヘッダーは、設定ミスでサイトがアクセス不能になるリスクが高く、Chrome が 2018 年にサポートを廃止した。Web では Certificate Transparency (CT) が代替手段として推奨されている。

// Node.js での証明書ピンニング
import https from 'https';
const options = {
  hostname: 'api.example.com',
  checkServerIdentity: (host, cert) => {
    const pin = crypto.createHash('sha256').update(cert.pubkey).digest('base64');
    if (pin !== expectedPin) throw new Error('Pin mismatch');
  },
};

詳しくは関連書籍を参照。

関連用語