証明書ピンニング
特定の証明書や公開鍵のみを信頼し、中間者攻撃を防ぐセキュリティ手法
モバイル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');
},
};
詳しくは関連書籍を参照。