当社のAIエンジンNeutronが、UCバークレーのCyberGymベンチマークで96.75%のスコアを記録しました。 詳細を見る

セキュリティ

セキュリティ

SSLピンニングとは:OkHttpを使ったAndroidガイド

Network Security Config、OkHttp、Retrofitを使ってAndroidにSSLピンニングを実装し、その動作をテストして、ピンを安全にローテーションする方法を解説します。レガシーライブラリの例も掲載しています。

AndroidにおけるSSLピンニング(証明書ピンニングと公開鍵ピンニング)

Androidアプリとそのバックエンド間の通信を保護する出発点はTLSです。TLSは、接続を暗号化し、サーバーを認証し、機密性の高い通信が平文で送信されるのを防ぐことで、転送中のデータを保護します。標準的なAndroidの構成では、アプリはプラットフォームのトラストストアと通常の証明書検証に基づいて、サーバー証明書を信頼すべきかどうかを判断します。

ピンニングは、このベースラインの上に信頼の制限をもう一つ加えるものです。通常の信頼モデルで検証できる証明書チェーンをすべて受け入れるのではなく、特定の想定された鍵情報を含むチェーンだけを受け入れます。これにより、一部のシナリオでは中間者攻撃(MitM)を難しくできますが、運用上のリスクも生じます。ピンニングが厳格すぎたり、適切に管理されていなかったりすると、証明書の変更、認証局(CA)の移行、鍵のローテーションによって正当な接続が切断される可能性があります。証明書ピンニングに関するAndroidのガイダンスはこの点に慎重で、サーバーの構成を変更するとクライアントを更新しない限り接続できなくなる可能性があるため、ほとんどのアプリではピンニングは一般に推奨されないと述べています。

本ガイドでは、AndroidにおけるSSLピンニングの意味、それが役立つ場面、防げないもの、主な実装方法、安全に検証する方法、そして証明書のローテーションを障害につなげずに本番環境で運用する方法を解説します。また、モダンなAndroidネットワークスタックとレガシーなスタックの両方を扱うチームに向けて、HttpsURLConnection、OkHttp、Retrofit、Volley、Picassoの例も掲載しています。

Androidにおける信頼の比較:デフォルトのCAでは複数の証明書チェーンが受け入れられるのに対し、ピンニングされた信頼では、ピンニングされた鍵を含むチェーンだけが受け入れられ、それ以外は拒否される。

目次

  1. AndroidにおけるSSLピンニングの意味
  2. SSLピンニング、TLSピンニング、証明書ピンニングの違い
  3. ピンニングで防げないもの
  4. 脅威モデル:ピンニングが役立つ場面
  5. Androidにおけるピンニングの選択肢
  6. 実装例
  7. SSLピンニングが機能していることを検証する方法
  8. 本番環境でピンニングを安全に運用する
  9. 関連記事
  10. よくある質問

AndroidにおけるSSLピンニングの意味

標準的なAndroidの構成では、アプリはプラットフォームのトラストストアで検証できるサーバー証明書を信頼します。SSLピンニングは、このモデルの上にさらに制限を加えます。証明書チェーンに、対象ドメインについて想定された特定の鍵情報が含まれている場合にのみ、接続が受け入れられます。

Androidでは通常、証明書の公開鍵のハッシュをピンニングする形で実装されます。これはSPKIピンニングとも呼ばれます。一般に信頼された認証局をルートとする有効なチェーンであれば何でも信頼するのではなく、ピンニングされた公開鍵のいずれかを含むチェーンにまで信頼を絞り込みます。

実際には、アプリは証明書が有効かどうかを確認するだけでなく、アプリケーション自体が定義した、より具体的な信頼の条件に合致するかどうかも確認することになります。OkHttpのCertificatePinnerも同じ考え方に基づき、ピンニングされた公開鍵のハッシュに照らしてサーバーの証明書チェーンを検証します。

SSLピンニング、TLSピンニング、証明書ピンニングの違い

「SSLピンニング」はいまでも最も広く使われている呼び方ですが、実際には通常、TLSの証明書ピンニングまたは公開鍵ピンニングを指します。Androidの現在のセキュリティガイダンスでは、ピンニングは公開鍵のハッシュという観点で説明されています。本ガイドでは、SSLピンニング、TLSピンニング、証明書ピンニング、公開鍵ピンニングという用語を、実務者がこのテーマを検索したり議論したりする際の一般的な使い方に合わせて用いつつ、技術的な意味は正確に保ちます。

ピンニングで防げないもの

ピンニングは、適切なTLS検証の代わりにはなりません。アプリが安全でないHostnameVerifierやunsafe X509TrustManagerを使用している場合、なりすましや傍受に対して依然として脆弱になる可能性があります。Androidは、これらがいずれもアプリに悪意のある接続や無効な接続を受け入れさせる可能性があるとして、明確なセキュリティリスクとして文書化しています。

また、ピンニングは、サーバーサイドの脆弱性、漏えいした認証情報、不備のあるセッション処理、安全でないAPI、アプリの他の部分における機密データの露出を修正するものでもありません。ピンニングは通信のセキュリティ制御の一つにすぎず、モバイルアプリのセキュリティ戦略の全体ではありません。これが、ピンニングを単独の堅牢化チェック項目としてではなく、より広いAndroidのセキュリティモデルの一部として扱うべき理由の一つです。

脅威モデル:ピンニングが役立つ場面

Androidのドキュメントは、中心となるシナリオを明確に説明しています。デフォルトでは、アプリはプリインストールされた多数の認証局を信頼しており、そのうちの1つが不正な証明書を発行した場合、アプリは経路上の攻撃者にさらされる可能性があります。ピンニングは、証明書チェーンにピンニングされた公開鍵のいずれかが含まれていることを必須とすることで、この信頼を縮小します。OkHttpも同じ保護について、認証局に対する攻撃や、ユーザーが認識している、あるいは認識していない中間者の認証局に対する防御という観点で説明しています。

そのため、特定のバックエンドについて受け入れる信頼チェーンをより厳密に制御したいチームにとって、ピンニングはより意味を持ちます。一般に、バックエンドが安定しており、証明書のライフサイクルが厳密に管理され、ローテーションに伴う運用上の負担があらかじめ計画されている場合に、最も正当化しやすくなります。その場合でも、ピンニングは摩擦の大きい制御であり、安易に導入すべきではありません。Androidはこれを運用上壊れやすい対策として扱っており、OkHttp自身のドキュメントも、証明書の更新を制限し運用を複雑にするとして、「Certificate Pinning is Dangerous!」と明記しているほどです。

Androidにおけるピンニングの選択肢

新しいAndroidのコードベースでは、ピンニングはAndroid Network Security Configuration(次のセクションの例を参照)で扱われることが多くなっています。これは、宣言的な構成ファイルで、証明書ピンニング、バックアップピン、ピンの有効期限、カスタムのトラストアンカー、デバッグ用のオーバーライドをサポートします。これにより、TLSの処理を複数のライブラリに分散させるのではなく、信頼に関する判断を一元化できます。アプリがOkHttpを直接使用している場合は、CertificatePinnerが引き続きクライアントレベルの主な選択肢です。RetrofitはHTTPクライアントの上に位置するため、Retrofitを使った構成でのピンニングの挙動は、設定された基盤のクライアント(通常はOkHttp)に依存します。

古いAndroidのコードベースでは、ピンニングがいまだにHttpsURLConnection、Volley、Picassoとの連携における独自のTLS処理に依存していることがあります。これらの例は、歴史的な参考資料として、あるいはレガシーアプリの保守には今でも役立ちますが、新しいプロジェクトのデフォルトの選択肢ではなく、古い実装パターンとして扱うべきです。Androidのピンニングについて、OWASP MASTGはNetwork Security Configurationを推奨される望ましいアプローチとして説明する一方、TrustManagerをカスタマイズしたピンニングは、慎重に実装しなければ誤りが起きやすい低レベルの手法であると警告しています

アプローチ ピンニングのサポート 保守の負担 主な落とし穴 最適な用途
Android Network Security Configuration あり 低め ドメインとデバッグの慎重な設定が必要 ほとんどのモダンなAndroidアプリ
OkHttp CertificatePinner あり 中 ローテーションとホスト名のカバー範囲を計画する必要がある すでにOkHttpを直接使用しているアプリ
設定済みのOkHttpクライアントを使ったRetrofit あり 中 Retrofitはクライアントの挙動を引き継ぎ、ピンニングは別個に存在しない Retrofitベースのアプリ
HttpsURLConnectionの独自のトラスト設定 可能 高め TLSの処理を過剰にカスタマイズしやすい レガシーの保守のみ
Volleyの独自のTLS設定 可能 高め 一元化を維持しにくい レガシーの保守のみ
Picassoの独自のダウンローダー設定 可能 高め 古い連携パターン レガシーの保守のみ

この比較は、現在のAndroidのガイダンス、OkHttpのドキュメント、そして宣言的なプラットフォームレベルの構成と、古いライブラリ固有のトラストのカスタマイズとの実務上の違いを反映したものです。

実装例

ピンニングは、使用しているAndroidのネットワークスタックに応じてさまざまな方法で実装できます。新規の開発では、Network Security ConfigurationまたはモダンなOkHttpの構成から始めてください。以下のライブラリの例は、古いスタックをいまだに保守しているチーム向けのレガシーな参考パターンとして理解するのが適切です。

レガシーの例の前に:モダンな推奨事項

現在新しいAndroidアプリを構築するのであれば、宣言的な信頼とピンニングのためにAndroid Network Security Configurationから始めるか、ネットワークスタックがすでにOkHttpを中心としている場合はOkHttp CertificatePinnerから始めてください。Androidはpin-setによる公開鍵ピンニングをサポートし、バックアップピンにも対応し、任意で有効期限を設定でき、デバッグ専用のオーバーライドも備えているため、チームはリリースビルドを弱めることなく安全にテストできます。

Android Network Security Configuration(NSC)によるピンニングの例(宣言的)

res/xml/network_security_config.xmlを作成します:

    <?xml version="1.0" encoding="utf-8"?>
<network-security-config>

    <!-- Apply rules to a specific domain (and optionally its subdomains) -->
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">example.com</domain>

        <!-- Public-key pins (SHA-256 of SPKI), with an expiration date -->
        <pin-set expiration="2027-12-31">
            <!-- Primary pin -->
            <pin digest="SHA-256">afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=</pin>

            <!-- Backup pin (rotate keys/certs safely) -->
            <pin digest="SHA-256">BASE64_SHA256_BACKUP_SPKI_PIN_HERE=</pin>
        </pin-set>

        <!-- Optional: customize which CAs you trust for this domain -->
        <trust-anchors>
            <certificates src="system" />
            <!-- If you also need a private CA bundled with the app: -->
            <!-- <certificates src="@raw/my_private_ca" /> -->
        </trust-anchors>
    </domain-config>

    <!-- Optional (debug builds): allow user-added / debug CAs without weakening release -->
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
            <!-- or a dedicated debug CA -->
            <!-- <certificates src="@raw/debug_ca" /> -->
        </trust-anchors>
    </debug-overrides>

</network-security-config>

要点:pin-setは複数のピン(プライマリ+バックアップ)と有効期限(形式はyyyy-MM-dd)をサポートしており、構成を更新しなければ、有効期限を過ぎた後はピンニングが無効になります。 AndroidManifest.xmlで設定を有効にする

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

HttpsURLConnectionを使ったAndroidの証明書ピンニング

    CertificateFactory cf = CertificateFactory.getInstance("X.509");

    // Generate the certificate using the certificate file under res/raw/cert.cer
    InputStream caInput = new BufferedInputStream(getResources().openRawResource(R.raw.cert));
    Certificate ca = cf.generateCertificate(caInput);
    caInput.close();

    // Create a KeyStore containing our trusted CAs
    String keyStoreType = KeyStore.getDefaultType();
    KeyStore keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    keyStore.setCertificateEntry("ca", ca);

    // Create a TrustManager that trusts the CAs in our KeyStore
    String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
    tmf.init(keyStore);

    // Create an SSLContext that uses our TrustManager
    SSLContext context = SSLContext.getInstance("TLS");
    context.init(null, tmf.getTrustManagers(), null);

    // Tell the URLConnection to use a SocketFactory from our SSLContext
    URL url = new URL("https://example.com/faq/");
    HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
    urlConnection.setSSLSocketFactory(context.getSocketFactory());
    InputStream in = urlConnection.getInputStream();
    theString = readInputStream(in);
    CertificateFactory cf = CertificateFactory.getInstance("X.509");

OkHTTPを使ったAndroidの証明書ピンニング

    //okhttp version 3.x

    public CertificatePinning()
    {
        // We add the public key of our trusted server hashed and encoded base64
        client = new OkHttpClient.Builder().certificatePinner(new CertificatePinner.Builder()
        .add("https://example.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=").build()).build();
    }

    public void run() throws Exception
    {
        Request request = new Request.Builder().url("https://example.com/faq").build();

        Response response = client.newCall(request).execute();
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    /**
    * response.handshake() contains the TLS handshake of the connection that carried this response, or null if the response
    * was received without TLS.
    */
        if(response.handshake())
        {
            for (Certificate certificate : response.handshake().peerCertificates())
            {
                //Pin returns SHA-256 hash of the public key
                String caPin = CertificatePinner.pin(certificate);

                // then you we to check the hash value and apply the corresponding action
            }
        }
    }

Retrofitを使ったAndroidの証明書ピンニング

    // Use the steps above to create OkHttpClient and set the custom client when building adapter
    Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://example.com")
    .addConverterFactory(GsonConverterFactory.create())
    .client(client)
    .build();
  Volley:

    CertificateFactory cf = CertificateFactory.getInstance("X.509");

    // Generate the certificate using the certificate file under res/raw/cert.cer
    InputStream caInput = new BufferedInputStream(getResources().openRawResource(R.raw.cert));
    Certificate ca = cf.generateCertificate(caInput);
    caInput.close();

    // Create a KeyStore containing our trusted CAs
    String keyStoreType = KeyStore.getDefaultType();
    KeyStore trusted = KeyStore.getInstance(keyStoreType);
    trusted.load(null, null);
    trusted.setCertificateEntry("ca", ca);

    // Create a TrustManager that trusts the CAs in our KeyStore
    String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
    tmf.init(trusted);

    // Create an SSLContext that uses our TrustManager
    SSLContext context = SSLContext.getInstance("TLS");
    context.init(null, tmf.getTrustManagers(), null);

    SSLSocketFactory sf = context.getSocketFactory();
    mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext(), new HurlStack(null, sf));

Picassoを使ったAndroidの証明書ピンニング

    CertificateFactory cf = CertificateFactory.getInstance("X.509");

    // Certificate file is under res/raw/cert.cer
    InputStream caInput = new BufferedInputStream(getResources().openRawResource(R.raw.cert));
    Certificate ca = cf.generateCertificate(caInput);
    caInput.close();

    // Create a KeyStore containing our trusted CAs
    String keyStoreType = KeyStore.getDefaultType();
    KeyStore keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    keyStore.setCertificateEntry("ca", ca);

    // Create a TrustManager that trusts the CAs in our KeyStore
    String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
    tmf.init(keyStore);

    // Create an SSLContext that uses our TrustManager
    SSLContext context = SSLContext.getInstance("TLS");
    context.init(null, tmf.getTrustManagers(), null);

    OkHttpClient okHttpClient = new OkHttpClient();
    okHttpClient.setSslSocketFactory(sslContext.getSocketFactory());
    OkHttpDownloader okHttpDownloader = new OkHttpDownloader(okHttpClient);
    Picasso.Builder builder = new Picasso.Builder(context);
    builder.downloader(okHttpDownloader);
    Picasso sPicasso = builder.build();

SSLピンニングが機能していることを検証する方法(ユーザーに影響を与えずに)

AndroidアプリやiOSアプリでSSLピンニングを検証するには、Burp Suiteやmitmproxyなどのプロキシを設定し、プロキシのCA証明書を信頼している状態でも、アプリが通信の傍受をブロックすることを確認します。これにより、ピンニングが中間者攻撃を防いでいることが実証されます。

前提条件:

  1. mitmproxyをインストールする:pip install mitmproxy
  2. mitmdump -p 8080 を実行し(UIを使う場合はmitmproxy)、自分のIPアドレスを控える(例:ifconfig または ip addr)
  3. 完全な信頼のためにはroot化したAndroidまたはジェイルブレイクしたiOSを推奨。エミュレーターやシミュレーターでも動作する

Androidの手順

  1. 端末のWi-Fiプロキシを :8080 に設定し、ブラウザーでHTTPSを開いてテストする(プロキシを経由するはず)。
  2. ブラウザーで http://mitm.it/?mode=android にアクセスし、ユーザーCAをダウンロードしてインストールする。
  3. システムの信頼(ほとんどのアプリで必要):openssl x509 -inform PEM -subject_hash_old -in ~/.mitmproxy/mitmproxy-ca-cert.pem | head -1 → .0; adb push .0 /system/etc/security/cacerts/; adb shell chmod 644 /system/etc/security/cacerts/.0; reboot
  4. アプリのネットワーク呼び出しを発生させる。mitmproxy/mitmdumpにフローが表示されず、adb logcat | grep -i ssl でSSLエラーが出ていれば、ピンニングは有効です。

検証:

プロキシなしでアプリが正常に動作すれば、問題の原因が構成ではなくピンニングであることを確認できます。通信を傍受できた場合は、ピンニングは行われていません(確認にはobjectionなどのバイパスツールも使えます)。

これはアプリケーションの起動時だけでなく、その後にも試してください。アプリケーションによっては、特定の操作の際にしかチェックを行わず、それ以外ではチェックしないものもあるためです。

本番環境でピンニングを安全に運用する(ローテーション、バックアップピン、ロールアウト)

ピンニングに伴う本番環境での主なリスクは、証明書や鍵のローテーションです。Androidは、鍵を切り替えたりCAを変更したりする必要が生じた場合にもアプリの接続が失われないよう、常にバックアップの鍵を含めることを明確に推奨しています。また、ピンの有効期限もサポートしており、古いバージョンのアプリが無期限に接続できなくなるリスクを減らせます。ただしその代わりに、有効期限が切れたピンは接続を保護しなくなります。

OkHttpも運用上の負担について同様に率直で、ピンニングはサーバーチームが証明書を更新したり、認証局を移行したりする能力を制限すると述べています。そのため、ピンニングはコードのクリーンアップのように単にマージするのではなく、本番機能としてロールアウトすべきです。妥当なロールアウトの進め方は、まず社内ビルド、次にベータ、続いて本番環境の限られた割合に展開し、証明書の挙動、監視、フォールバック計画が検証されてから初めて全面展開するというものです。ここで述べた段階的なロールアウトの助言は、AndroidとOkHttpが文書化している制約に基づく運用上の推奨事項です。

本番環境でピンニングが失敗した場合、アプリは理解しやすく診断しやすい形で失敗すべきです。少なくとも、ユーザーに表示するメッセージ、ログのフィールド、アラートのしきい値、サポートのトリアージ経路を定義してください。ピンの検証失敗は、一般的なタイムアウトやオフライン状態とは異なるため、同じエラー分類に埋もれさせるべきではありません。

企業のTLSインスペクションやキャプティブポータルについても、ポリシーを明示的に決めておく必要があります。ピンニングは想定された鍵情報に信頼を限定するため、サーバー証明書を置き換える環境では、設計上ピンニングが失敗することがあります。チームは、デプロイ中にそれに気づくのではなく、ロールアウト前にその挙動を意図的に決めておくべきです。ピンニングを無効にせずに、ピンニングされた環境で通信を解析する技術的な例については、当社の記事「SSLピンニングのユニバーサルバイパス:理論からLLDBによる完全に動作するPoCまで」を参照してください。

よくある質問

AndroidでSSLピンニングを導入する価値はあるか

価値はあり得ますが、それはバックエンド、リリースプロセス、証明書のライフサイクルが、ピンニングを支えられるほど成熟している場合に限られます。Androidの公式ガイダンスは、バックエンドの証明書を変更するとクライアントを更新しない限り接続できなくなる可能性があるため、多くのアプリでは証明書ピンニングは一般に推奨されないと明確に注意を促しています。チームがピンニングを選択する場合、Androidはバックアップピンと短い有効期限を推奨しています。

証明書ピンニングと公開鍵ピンニングの違いは何か

Androidの現在のガイダンスでは、ピンニングは証明書ファイル全体をそのまま比較するのではなく、証明書の公開鍵のハッシュとして表現されます。OkHttpのCertificatePinnerも、SPKIベースのピンニングを文書化しています。 証明書をローテーションすると何が壊れるか。 新しい証明書チェーンにピンニングされた鍵が1つも含まれていない場合、ピンが更新されるか、バックアップピンがあらかじめ含まれていない限り、アプリは接続できなくなる可能性があります。そのため、Androidはバックアップの鍵を推奨し、ピンの有効期限を任意で設定できるようにしています。

Android Network Security Configurationはピンニングをサポートしているか

はい。Androidは、Network Security Configurationのpin-set entriesを通じて証明書ピンニングをサポートしており、複数のピン、バックアップピン、有効期限、カスタムのトラストアンカー、デバッグ専用のオーバーライドにも対応しています。

Retrofitは単体でピンニングを行うか

いいえ。RetrofitはHTTP APIをJavaまたはKotlinのインターフェースに変換するもので、ピンニングの挙動は、OkHttpなど設定された基盤のクライアントによって決まります。

本番環境のセキュリティを弱めずにピンニングをテストするには

管理されたステージング環境でのテストを行い、成功した場合と不一致の場合の両方の経路を検証し、リリース時の検証ロジックを弱めるのではなく、Androidのデバッグ構成のサポートを活用してください。

AndroidアプリはエンタープライズのTLSインスペクションにどう対処すべきか

チームはロールアウト前にそのポリシーを明示的に決めておくべきです。ピンニングは特定の鍵情報を検証するため、TLSインスペクションを行う環境では設計上失敗することがあります。したがって、想定される挙動は、デプロイ中にユーザーが気づくのではなく、あらかじめ文書化しておくべきです。

新しいAndroidアプリにとって最もモダンな出発点は何か

ほとんどの新しいアプリでは、一元的かつ宣言的なピンニングのためにNetwork Security Configurationから始めるか、ネットワークレイヤーがすでにOkHttpを中心としている場合はOkHttp CertificatePinnerを使用してください。古いライブラリ固有の独自のTLS設定は、レガシーなコードを保守する場合にのみ残してください。