SSLHandshakeException修正: AndroidでTrust Anchor for Certification Path Not Foundが発生する場合

intermediate📱 Android2026-05-22| Android 5.0以上 (API 21+)、OkHttp 3.x/4.x、Retrofit 2.x、Java/Kotlin

Error Message

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found
#ssl#https#okhttp#retrofit#certificate

何が起きているのか

AndroidアプリがHTTPSリクエスト中に以下のエラーをスローします:

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found

AndroidのSSLスタックが、サーバーの証明書チェーンをトラストストア内の信頼された認証局(CA)に対して検証できませんでした。主な原因は3つあります:

  • サーバーが自己署名証明書を使用している — 開発環境やステージング環境でよく見られます
  • 証明書がプライベートまたは内部CAによって署名されており、Androidが認識していない
  • 証明書チェーンが不完全 — サーバーがリーフ証明書のみ送信し、中間証明書を送っていない

まずデバッグする

コードをいじる前に、実際にどのケースに当たっているか確認しましょう。

証明書チェーンを確認する

ターミナルから実行します — 実際のホスト名に置き換えてください:

openssl s_client -connect your-api.example.com:443 -showcerts

以下の行を出力から探してください:

  • verify error:num=19:self signed certificate in certificate chain → 自己署名証明書
  • verify error:num=20:unable to get local issuer certificate → 中間証明書の欠落
  • デスクトップでは検証が通るがAndroidで失敗する → そのルートCAがデバイスのOSバージョン以降にAndroidのトラストストアへ追加された

異なるAndroidバージョンでテストする

AndroidのシステムCAストアは静的ではなく、OSリリースごとに更新されます。Android 9(API 28)で動作する証明書がAndroid 7(API 24)でサイレントに失敗することがあります。古いデバイスでのみエラーが発生する場合は、そのバージョン以降に追加されたルートCAが原因である可能性が高いです。AndroidのCA証明書リポジトリで特定のルートが追加された時期を確認してください。

解決策1:証明書をバンドルする(自己署名またはプライベートCA)

開発・ステージングサーバーやプライベートCAを使う内部APIの場合、これが正しい修正方法です — 単なる回避策ではありません。

手順1:証明書をエクスポートする

openssl s_client -connect your-api.example.com:443 -showcerts 2>/dev/null | \
  openssl x509 -outform PEM > mycert.pem

mycert.pemapp/src/main/res/raw/mycert.pemに配置します。

手順2:ネットワークセキュリティ設定を作成する

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

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">your-api.example.com</domain>
        <trust-anchors>
            <certificates src="@raw/mycert"/>
            <certificates src="system"/>
        </trust-anchors>
    </domain-config>
</network-security-config>

<certificates src="system"/>の行は重要です — これにより他のすべてのドメインで通常のHTTPSが機能し続けます。これがないと、アプリはシステムが信頼するすべての証明書を拒否してしまいます。

手順3:AndroidManifest.xmlで参照する

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

以上です。OkHttpの変更は不要です。Androidが自動的に信頼検証を処理し、設定は1つのXMLファイルで確認できます。

解決策2:中間証明書の欠落を修正する(本番サーバー)

本番環境で突然このエラーが発生した場合、サーバーが完全な証明書チェーンを送信していない可能性があります。十中八九、これはAndroidの問題ではなくサーバー側の設定ミスです。

確認方法:

openssl s_client -connect your-api.example.com:443 -verify_return_error

その後、Webサーバーの設定に中間証明書を追加します:

Nginx:

# リーフ証明書と中間CAを1つのファイルに結合する
cat your_cert.pem intermediate.pem > fullchain.pem

# nginx.conf内:
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/private.key;

Apache:

SSLCertificateFile /path/to/your_cert.pem
SSLCertificateChainFile /path/to/intermediate.pem

サーバーをリロードした後、openssl s_clientを再実行し、チェーンの深さが1ではなく2または3であることを確認します。

解決策3:OkHttpカスタムトラストマネージャー(コードレベルの制御)

ネットワークセキュリティ設定でほとんどのケースに対応できます。ただし、実行時に証明書を動的に読み込む場合(バックエンドから取得するなど)、OkHttpに直接設定する必要があります:

// Kotlin
fun buildOkHttpClient(context: Context): OkHttpClient {
    val cf = CertificateFactory.getInstance("X.509")
    val cert = context.resources.openRawResource(R.raw.mycert).use {
        cf.generateCertificate(it) as X509Certificate
    }

    val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
        load(null, null)
        setCertificateEntry("ca", cert)
    }

    val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply {
        init(keyStore)
    }

    val sslContext = SSLContext.getInstance("TLS").apply {
        init(null, tmf.trustManagers, null)
    }

    return OkHttpClient.Builder()
        .sslSocketFactory(sslContext.socketFactory, tmf.trustManagers[0] as X509TrustManager)
        .build()
}

Retrofitに組み込みます:

val retrofit = Retrofit.Builder()
    .baseUrl("https://your-api.example.com/")
    .client(buildOkHttpClient(context))
    .addConverterFactory(GsonConverterFactory.create())
    .build()

やってはいけないこと

Stack Overflowにはこのパターンが溢れています。絶対に避けてください:

// これをやってはいけない — すべての証明書検証を無効にする
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
    override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
    override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
    override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
})

これはすべての証明書検証を無効にします。同じネットワーク上の攻撃者がHTTPS通信を傍受できるようになります。GoogleのPlay Storeの自動スキャナーがこのパターンを検出します — 通り抜けたアプリも、セキュリティレビューで後から発覚して公開停止になるリスクがあります。

修正を確認する

  • クリーンビルド:./gradlew clean assembleDebug
  • デバイスにインストールしてHTTPS呼び出しをトリガーする
  • Logcatを確認 — SSLHandshakeExceptionが消えているはずです
  • openssl s_clientを再実行 — Verify return code: 0 (ok)を確認する
  • 少なくとも2台のデバイスでテストする:最新のエミュレーター(Android 10以上)と古い実機(Android 6または7)でCAトラストストアのギャップを検出する

ヒント:正しい証明書をエクスポートしたか確認する

ステージングサーバーから証明書をバンドルする際は、正しいものを取得したか確認する価値があります。SHA-256フィンガープリントを生成し、サーバーが実際に提示するものと比較してください:

openssl x509 -in mycert.pem -fingerprint -sha256 -noout

OpenSSLが手元にない場合は、ToolCraftのHash Generatorを使うと証明書の内容をブラウザで貼り付けてSHA-256やMD5ハッシュを取得できます — 制限されたマシンを使っているときに便利です。

まとめ

  • SSL検証を絶対にバイパスしない — 「テスト用だけ」でも同様です。一時的なハックが本番環境に届く習慣があります。
  • ネットワークセキュリティ設定がデフォルトのツールです。 宣言的で、特定のドメインにスコープされており、ネットワークコード全体に信頼ロジックが散らばりません。
  • 中間証明書の欠落は、自己署名証明書よりも本番環境での障害を多く引き起こします。 Android側の問題と決めつける前に必ずopenssl s_clientを実行してください。
  • カスタムトラストアンカーを特定のドメインにスコープする — グローバルな上書きではなく<domain-config>ブロックを使用します。

Related Error Notes