Fixing SSLHandshakeException: Trust Anchor for Certification Path Not Found on Android

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

What's Happening

Your Android app throws this during an HTTPS request:

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

Android's SSL stack couldn't verify the server's certificate chain against any trusted Certificate Authority (CA) in its trust store. There are three usual suspects:

  • The server uses a self-signed certificate โ€” common on dev and staging environments
  • The certificate is signed by a private or internal CA that Android has never heard of
  • The certificate chain is incomplete โ€” the server is only sending the leaf cert, not the intermediates

Debug First

Don't reach for code yet. Figure out which of those three cases you're actually dealing with.

Check the certificate chain

Run this from your terminal โ€” swap in your real hostname:

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

Scan the output for these lines:

  • verify error:num=19:self signed certificate in certificate chain โ†’ self-signed cert
  • verify error:num=20:unable to get local issuer certificate โ†’ missing intermediate cert
  • Chain verifies fine on desktop but fails on Android โ†’ that root CA was added to Android's trust store after the device's OS version shipped

Test on different Android versions

Android's system CA store is not static โ€” it gets updated with each OS release. A cert that works on Android 9 (API 28) can fail silently on Android 7 (API 24). If the error only appears on older devices, you're almost certainly hitting a root CA that post-dates that version. Check Android's CA certificate repository to confirm when a specific root was added.

Solution 1: Bundle the Certificate (Self-Signed or Private CA)

For dev/staging servers or internal APIs backed by a private CA, this is the right fix โ€” not a workaround.

Step 1: Export the certificate

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

Drop mycert.pem into app/src/main/res/raw/mycert.pem.

Step 2: Create a Network Security Config

Create 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>

The <certificates src="system"/> line is important โ€” it keeps normal HTTPS working for every other domain. Without it, your app would reject all system-trusted certs.

Step 3: Reference it in AndroidManifest.xml

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

That's it. No OkHttp changes needed. Android handles trust validation automatically, and the config is auditable in one XML file.

Solution 2: Fix a Missing Intermediate Certificate (Production Servers)

Production suddenly started throwing this? The server probably isn't sending the full certificate chain. Nine times out of ten, this is a server-side misconfiguration โ€” not an Android problem.

Confirm it:

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

Then add the intermediate cert to your web server config:

Nginx:

# Concatenate leaf cert + intermediate CA into a single file
cat your_cert.pem intermediate.pem > fullchain.pem

# In 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

After reloading the server, rerun openssl s_client and confirm the chain depth is 2 or 3 โ€” not 1.

Solution 3: OkHttp Custom Trust Manager (Code-Level Control)

Network Security Config covers most cases. But if you're loading certificates dynamically at runtime โ€” say, fetched from a backend โ€” you'll need to wire this up in OkHttp directly:

// 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()
}

Wire it into Retrofit:

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

What NOT to Do

Stack Overflow is full of this pattern. Avoid it completely:

// DON'T DO THIS โ€” disables all certificate validation
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()
})

This kills all certificate validation. Any attacker on the same network can intercept your HTTPS traffic. Google's Play Store automated scanner flags this pattern โ€” and apps that slip through still risk getting pulled post-publication when the security review catches up.

Verify the Fix

  • Clean build: ./gradlew clean assembleDebug
  • Install on device and trigger the HTTPS call
  • Check Logcat โ€” SSLHandshakeException should be gone
  • Rerun openssl s_client โ€” look for Verify return code: 0 (ok)
  • Test on at least two devices: a recent emulator (Android 10+) and an older physical device (Android 6 or 7) to catch CA trust store gaps

Tip: Confirm You Exported the Right Certificate

When bundling a cert from a staging server, it's worth double-checking you grabbed the right one. Generate a SHA-256 fingerprint and compare it against what the server actually presents:

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

No OpenSSL handy? ToolCraft's Hash Generator lets you paste certificate content and get SHA-256 or MD5 hashes in the browser โ€” handy when you're on a locked-down machine.

Key Takeaways

  • Never bypass SSL validation โ€” even "just for testing." Temporary hacks have a habit of reaching production.
  • Network Security Config is your default tool. It's declarative, scoped to specific domains, and doesn't scatter trust logic across your networking code.
  • Missing intermediates cause more production fires than self-signed certs do. Always run openssl s_client before assuming the problem is on the Android side.
  • Scope custom trust anchors to specific domains โ€” a <domain-config> block, not a global override.

Related Error Notes