[Android] 証明書ピンニングの実装パターン3種と、ドメイン移行時に必要な対応

「APIの接続先ドメインを変えたいのですが、アプリ側はURLを差し替えるだけで大丈夫ですよね?」 ―― こういう相談を受けたとき、真っ先に確認すべきなのが証明書ピンニング(Certificate Pinning)の有無です。ここを見落とすと、新ドメインの証明書が正規のものであっても、アプリからの全リクエストが失敗するという事故につながります。

この記事では、Android アプリで証明書ピンニングが実装される代表的な3パターンを取り上げ、「コードのどこを見れば判定できるか」「ドメイン移行時に何をすればいいか」を整理します。既存プロジェクトの調査やレビューで、ピンニングの有無を素早く見極めたい人向けの内容です。

スポンサーリンク

証明書ピンニングとは何か、なぜドメイン移行で問題になるのか

通常の HTTPS 通信では、サーバー証明書が「OS が信頼する認証局(CA)から発行されたものか」を検証します。証明書ピンニングは、これに加えて「特定の証明書(または公開鍵)であること」まで固定(pin)する仕組みです。中間者攻撃で偽の証明書を挟まれても、ピンと一致しなければ接続を拒否できます。

裏を返すと、ピンニングしているアプリは「事前に登録した鍵以外を受け付けない」ため、ドメインや証明書が変わって公開鍵ハッシュが変わると、正規の通信であっても弾いてしまうのです。これがドメイン移行時に「URL差し替えだけでは済まない」最大の理由になります。

では、実装パターンごとに見ていきましょう。

パターン1:OkHttp の CertificatePinner

OkHttp / Retrofit を使っているプロジェクトで最もよく見るのがこれです。CertificatePinner でホスト名と公開鍵ハッシュ(SHA-256)を結びつけます。

val certificatePinner = CertificatePinner.Builder()
    .add(
        "api.example.com",
        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
    )
    .build()
 
val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()

調査の際は、コードベース全体で CertificatePinner という文字列を検索すれば、ほぼ確実に見つかります。.add(...) に並んでいるホスト名が、ピンニング対象のドメインです。

ドメイン移行時の対応:新ドメイン(api-new.example.com 等)の公開鍵ハッシュを先方から入手し、.add() に追記します。移行期間は新旧両方のピンを残しておくと、切り替えタイミングのズレで事故りません。

パターン2:Network Security Config の pin-set

Android 7.0(API 24)以降では、XML の宣言だけでピンニングを設定できます。コードに現れないぶん、見落とされやすいパターンです。

<!-- res/xml/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">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
            <!-- バックアップピン(後述) -->
            <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

この XML は AndroidManifest.xml から参照されて初めて有効になります。マニフェスト側も合わせて確認しましょう。

<application
    android:networkSecurityConfig="@xml/network_security_config"
    ... >

調査手順としては、まず AndroidManifest.xmlnetworkSecurityConfig 属性の有無を確認し、参照先 XML に <pin-set> があるかを見ます。

ドメイン移行時の対応:新ドメイン用の <domain-config> を追加し、新しい公開鍵ハッシュを <pin> に記載します。expiration 属性が過去日になっていると、その時点でピンニングが無効化される点も合わせて確認しておくと安心です。

パターン3:自前の X509TrustManager / HostnameVerifier

一番厄介で、かつ見落としやすいのがこの自前実装パターンです。X509TrustManagerHostnameVerifier を独自に実装し、その中で証明書やホスト名をチェックしているケースです。

// 例:ホスト名をハードコードで検証している実装(要注意パターン)
val hostnameVerifier = HostnameVerifier { hostname, _ ->
    // ドメインが直書きされていると、移行時にここの修正が必要になる
    hostname == "api.example.com"
}
 
val client = OkHttpClient.Builder()
    .hostnameVerifier(hostnameVerifier)
    .build()

この種の実装は CertificatePinner のような分かりやすいキーワードがないため、grep の取りこぼしが起きがちです。X509TrustManagerHostnameVerifierTrustManagerFactory あたりを横断的に検索して、自前検証ロジックが隠れていないかを確認します。

ドメイン移行時の対応:ロジックそのものを読み解いて修正が必要です。ハードコードされたホスト名や証明書比較があれば、新ドメイン向けに書き換えます。自前実装は検証を誤るとセキュリティを損なうため、可能なら標準の Network Security Config か CertificatePinner への移行も検討する価値があります。

調査の進め方:どこを、どの順で見るか

既存プロジェクトでピンニングの有無を判定するときは、見落としを防ぐために次の順で検索すると効率的です。

  1. AndroidManifest.xmlnetworkSecurityConfig 参照を確認 → あれば参照先 XML の <pin-set> を見る
  2. CertificatePinner を全文検索
  3. X509TrustManager / HostnameVerifier / TrustManagerFactory を検索(自前実装の取りこぼし防止)

この3つすべてがヒットしなければ「ピンニングなし」と判断できます。1か2でヒットすれば「新ドメインの公開鍵ハッシュを入手して追記が必要」、3でヒットすれば「ロジックを読んで個別対応」となります。

公開鍵ハッシュ(pin)の取得方法

新ドメインのピンを追加するには、SHA-256 の公開鍵ハッシュが必要です。先方からもらうのが基本ですが、自分で確認したい場合は openssl で取得できます。

openssl s_client -connect api.example.com:443 -servername api.example.com < /dev/null 2>/dev/null \
  | openssl x509 -pubkey -noout \
  | openssl pkey -pubin -outform der \
  | openssl dgst -sha256 -binary \
  | openssl enc -base64

出力された Base64 文字列を sha256/ に続けて記載すれば OkHttp のピンになります。Network Security Config の <pin> には sha256/ を付けず、Base64 部分だけを入れる点に注意してください。

ピンニングなしの場合に確認すべきこと

3パターンすべてに該当せず「ピンニングなし」と確定した場合、証明書まわりでアプリ側のコード修正は基本的に不要です。新ドメインの証明書が正規 CA 発行で信頼チェーンが通っていれば、OS 標準のトラストストアで素通り検証されます。

ただし、アプリの修正ではなく「新サーバーが正しく設定されているか」という観点で、先方に次の点を確認しておくと安全です。

  • 新証明書の SAN(Subject Alternative Name)に新ドメインが含まれているか(最近の Android は CN ではなく SAN を見る)
  • 中間証明書まで正しくチェーンを返しているか(中間証明書の欠落は一部端末で検証失敗の典型)
  • アプリの minSdkVersion がカバーする古い OS でも、新サーバーの TLS バージョン・暗号スイートで接続できるか

バックアップピンを必ず入れておく

最後に運用面の重要ポイントを一つ。ピンニングを使うなら、バックアップピンを必ず1つ以上入れておくことを強く推奨します。

証明書は更新(再発行)されることがあり、その際に公開鍵が変わるとピンも変わります。本番の鍵だけをピン留めしていると、証明書更新のたびにアプリも更新しないと全ユーザーが接続不能になります。次回更新時に使う予定の鍵(バックアップ用の鍵ペア)を事前にピンとして登録しておけば、サーバー側の証明書差し替えだけで乗り切れます。パターン2の XML 例で <pin> を2つ書いていたのは、このためです。

まとめ

API のドメイン移行で「URL差し替えだけ」で済むかどうかは、証明書ピンニングの有無で決まります。要点を振り返ります。

  • ピンニングの実装は主に3パターン:OkHttp の CertificatePinner、Network Security Config の <pin-set>、自前の X509TrustManager / HostnameVerifier
  • 調査は「マニフェストの networkSecurityConfig → CertificatePinner → 自前 TrustManager」の順で検索すると取りこぼしが少ない。
  • ピンニングありなら新ドメインの公開鍵ハッシュを追記。なしなら証明書面の対応は基本不要だが、SAN・中間証明書・TLS バージョンは先方に確認する。
  • ピンニングを使うならバックアップピンを必ず入れて、証明書更新で詰まないようにする。

ドメイン移行の相談を受けたら、まず「証明書ピンニングしていますか?」を起点に調査を始めると、対応の規模を早い段階で見積もれます。なお、ハードコードされた URL による未更新ユーザー対策(旧ドメインの並行稼働)も同じくらい重要な論点なので、こちらは別記事で扱う予定です。

コメント

タイトルとURLをコピーしました