androidokhttplets-encrypt

CertPathValidatorException connecting to a Let's Encrypt host on Android M or earlier


[edit: If you are here for Let's Encrypt expiry event, this was extended to 2024 https://letsencrypt.org/2020/12/21/extending-android-compatibility.html], but is now ending this year https://letsencrypt.org/2023/07/10/cross-sign-expiration.html. This can be extended slightly by configuration until at least June 2024

Connecting to https://valid-isrgrootx1.letsencrypt.org/ via OkHttp on Android M or earlier fails, while the connection works on N or later.

    OkHttpClient client = new OkHttpClient();

    try {
      Request request = new Request.Builder()
              .url("https://valid-isrgrootx1.letsencrypt.org/robots.txt")
              .build();
      try (Response response = client.newCall(request).execute()) {
        assertTrue(response.code() == 200 || response.code() == 404);
        assertEquals(Protocol.HTTP_2, response.protocol());
      }
    } catch (SSLHandshakeException sslhe) {
      sslhe.printStackTrace();
    }
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:322)
at okhttp3.internal.connection.RealConnection.connectTls(RealConnection.java:320)
at okhttp3.internal.connection.RealConnection.establishProtocol(RealConnection.java:284)
at okhttp3.internal.connection.RealConnection.connect(RealConnection.java:169)
at okhttp3.internal.connection.StreamAllocation.findConnection(StreamAllocation.java:258)
at okhttp3.internal.connection.StreamAllocation.findHealthyConnection(StreamAllocation.java:135)
at okhttp3.internal.connection.StreamAllocation.newStream(StreamAllocation.java:114)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:42)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:127)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:257)
at okhttp3.RealCall.execute(RealCall.java:93)
at okhttp.regression.LetsEncryptTest.sendRequest(LetsEncryptTest.java:133)
at okhttp.regression.LetsEncryptTest.getFailsWithoutAdditionalCert(LetsEncryptTest.java:52)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at androidx.test.ext.junit.runners.AndroidJUnit4.run(AndroidJUnit4.java:154)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:395)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1853)
Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:318)
at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:219)
at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:114)
at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:550)
at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:318)
... 50 more
Caused by: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
... 56 more

Solution

  • [edit: If you are here for Let's Encrypt expiry event, this was extended to 2024 https://letsencrypt.org/2020/12/21/extending-android-compatibility.html], but is now ending this year https://letsencrypt.org/2023/07/10/cross-sign-expiration.html. This can be extended slightly by configuration until at least June 2024

    As generic advice, if you need to add an root CA to older Android devices, this example shows how this can work.

    The issue relates to the known expiry of the ISRG root certificate for Let's encrypt in 2021, delayed to 2024.

    Note: none of this advice applies in combination with CertificatePinner, if you choose to also pin certificates, then please discuss your strategy with your in-house security team.

        boolean androidNorEarlier = Build.VERSION.SDK_INT <= 25;
    
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
    
        if (androidNorEarlier) {
          // TODO: download fresh from https://letsencrypt.org/certs/isrgrootx1.pem
          String isgCert =
                  "-----BEGIN CERTIFICATE-----\n" +
                  "MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n" +
                  "TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n" +
                  "cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n" +
                  "WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n" +
                  "ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n" +
                  "MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n" +
                  "h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n" +
                  "0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n" +
                  "A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n" +
                  "T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n" +
                  "B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n" +
                  "B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n" +
                  "KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n" +
                  "OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n" +
                  "jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n" +
                  "qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n" +
                  "rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n" +
                  "HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n" +
                  "hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n" +
                  "ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n" +
                  "3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n" +
                  "NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n" +
                  "ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n" +
                  "TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n" +
                  "jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n" +
                  "oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n" +
                  "4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n" +
                  "mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n" +
                  "emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n" +
                  "-----END CERTIFICATE-----";
    
          CertificateFactory cf = CertificateFactory.getInstance("X.509");
          Certificate isgCertificate = cf.generateCertificate(new ByteArrayInputStream(isgCert.getBytes("UTF-8")));
    
          HandshakeCertificates certificates = new HandshakeCertificates.Builder()
                  .addTrustedCertificate((X509Certificate) isgCertificate)
                  // Uncomment to allow connection to any site generally, but could possibly cause
                  // noticeable memory pressure in Android apps.
    //              .addPlatformTrustedCertificates()
                  .build();
    
          builder.sslSocketFactory(certificates.sslSocketFactory(), certificates.trustManager());
        }
    
        OkHttpClient client = builder.build();
    
        Request request = new Request.Builder()
                .url("https://valid-isrgrootx1.letsencrypt.org/robots.txt")
                .build();
        try (Response response = client.newCall(request).execute()) {
          assertTrue(response.code() == 200 || response.code() == 404);
          assertEquals(Protocol.HTTP_2, response.protocol());
        }
    

    The new host certificates are root signed by the ISRG Root X1 CA.

     ./cft --host valid-isrgrootx1.letsencrypt.org           
    CN:     valid-isrgrootx1.letsencrypt.org
    Pin:    sha256/489aa1610850a89c720217b9d9dbdc7f80918119f32b88c2dd3bcfaf1de29079
    SAN:    valid-isrgrootx1.letsencrypt.org
    Key Usage: DigitalSignature, KeyEncipherment
    Ext Key Usage: serverAuth, clientAuth
    Authority Info Access:
        ocsp: http://ocsp.int-x3.letsencrypt.org
        caIssuers: http://cert.int-x3.letsencrypt.org/
    Valid:  2020-10-14T15:00:50Z..2021-01-12T15:00:50Z (1 months)
    CA: false
    
    CN:     Let's Encrypt Authority X3
    Pin:    sha256/60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
    SAN:    <N/A>
    Key Usage: DigitalSignature, KeyCertSign, CRLSign
    Authority Info Access:
        ocsp: http://ocsp.root-x1.letsencrypt.org/
        caIssuers: http://cert.root-x1.letsencrypt.org/
    Valid:  2016-10-06T15:43:55Z..2021-10-06T15:43:55Z (10 months)
    CA: true Max Intermediate: 0
    
    CN:     ISRG Root X1 (signed by locally-trusted root)
    Pin:    sha256/0b9fa5a59eed715c26c1020c711b4f6ec42d58b0015e14337a39dad301c5afc3
    SAN:    <N/A>
    Key Usage: KeyCertSign, CRLSign
    Valid:  2015-06-04T11:04:38Z..2035-06-04T11:04:38Z (14 years)
    CA: true
    
    Strict Transport Security: max-age=604800
    
    OCSP status: GOOD
    

    Existing certificates are signed by DST Root CA X3 which expires in September 2021.

    $  ./cft --host letsencrypt.org 
    CN:     lencr.org
    Pin:    sha256/b93116ebda5e22efe089e7710b221557eb80a2e13c60a58687c0ce0369afd68a
    SAN:    lencr.org, letsencrypt.org, www.lencr.org, www.letsencrypt.org
    Key Usage: DigitalSignature, KeyEncipherment
    Ext Key Usage: serverAuth, clientAuth
    Authority Info Access:
        ocsp: http://ocsp.int-x3.letsencrypt.org
        caIssuers: http://cert.int-x3.letsencrypt.org/
    Valid:  2020-11-03T21:00:55Z..2021-02-01T21:00:55Z (2 months)
    CA: false
    
    CN:     Let's Encrypt Authority X3
    Pin:    sha256/60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18
    SAN:    <N/A>
    Key Usage: DigitalSignature, KeyCertSign, CRLSign
    Authority Info Access:
        ocsp: http://isrg.trustid.ocsp.identrust.com
        caIssuers: http://apps.identrust.com/roots/dstrootcax3.p7c
    Valid:  2016-03-17T16:40:46Z..2021-03-17T16:40:46Z (4 months)
    CA: true Max Intermediate: 0
    
    CN:     DST Root CA X3 (signed by locally-trusted root)
    Pin:    sha256/563b3caf8cfef34c2335caf560a7a95906e8488462eb75ac59784830df9e5b2b
    SAN:    <N/A>
    Key Usage: KeyCertSign, CRLSign
    Valid:  2000-09-30T21:12:19Z..2021-09-30T14:01:15Z (10 months)
    CA: true
    
    Strict Transport Security: max-age=31536000
    
    OCSP status: GOOD
    

    Available Fixes

    Name Versions API level ISRG Root X1 network
    security
    config
    OkHttp 3.12
    + Fix
    No official codename 1 1
    1.1 2
    Cupcake 1.5 3
    Donut 1.6 4
    Eclair 2.0 – 2.1 5 – 7
    Froyo 2.2 – 2.2.3 8
    Gingerbread 2.3 – 2.3.7 9 – 10 X
    Honeycomb 3.0 – 3.2.6 11 – 13 X
    Ice Cream Sandwich 4.0 – 4.0.4 14 – 15 X
    Jelly Bean 4.1 – 4.3.1 16 – 18 X
    KitKat 4.4 – 4.4.4 19 – 20 X
    Lollipop 5.0 – 5.1.1 21 – 22 X
    Marshmallow 6.0 – 6.0.1 23 X
    Nougat 7.0 – 7.1.2 24 – 25 7.1.1+ X X
    Oreo 8.0 – 8.1 26 – 27 X X X
    Pie 9 28 X X X
    Android 10 10 29 X X X
    Android 11 11 30 X X X