CertPathValidatorException 连接到 Android M 或更早版本的 Let's Encrypt 主机

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

[编辑:如果您是来参加 2021 年 1 月 Let's Encrypt 到期活动的,请先阅读此文 https://letsencrypt.org/2020/12/21/extending-android-compatibility.html]

在 Android M 或更早版本上通过 OkHttp 连接到 https://valid-isrgrootx1.letsencrypt.org/ 失败,而在 N 或更早版本上连接正常。

    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.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.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access[=12=]0(ParentRunner.java:58)
at org.junit.runners.ParentRunner.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.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access[=12=]0(ParentRunner.java:58)
at org.junit.runners.ParentRunner.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

[编辑:如果您是来参加 2021 年 1 月 Let's Encrypt 到期活动的,请先阅读这篇文章 https://letsencrypt.org/2020/12/21/extending-android-compatibility.html]

作为一般建议,如果您需要将根 CA 添加到较旧的 Android 设备,此示例将说明其工作原理。

此问题与已知的 Let's encrypt 的 ISRG 根证书在 2021 年到期有关。此(测试)服务器正在使用仅在 Android 版本上受支持的替换证书N (7.1.1) 及更高版本。

以下代码将针对 lets encrypt 将来使用的根证书起作用。它建立在 okhttp-tls.

之上

注意:none 此建议与 CertificatePinner 结合使用,如果您选择同时固定证书,请与您的内部安全团队讨论您的策略。

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

新主机证书由 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

现有证书由 DST Root CA X3 签署,将于 2021 年 9 月到期。

$  ./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

可用修复

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

在 Android Nougat (7) 上,您可以 add/update android:networkSecurityConfig 在 AndroidManifest.xml 中指向本地证书。

https://www.danieldent.com/blog/android-apps-lets-encrypt-dst-root-expiry/

<network-security-config>
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="@raw/isrg_root_x2" />
            <certificates src="@raw/isrg_root_x1" />
            <certificates src="system" />
        </trust-anchors>
    </base-config>
</network-security-config>

替代答案基于@Yuri Schimke 的代码,适用于 OkHttp 3.1.2、Volley 和 HttpsUrlConnection。

由于那些没有 HandshakeCertificates class 的繁重工作,您需要自己动手:

public TrustManagerFactory getTrustManagerFactory() throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException {
    //Note: hardcode it, because the device might not even have the certificate to download it over https
    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(StandardCharsets.UTF_8)));

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

    //Default TrustManager to get device trusted CA
    TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    defaultTmf.init((KeyStore) null);

    X509TrustManager trustManager = (X509TrustManager) defaultTmf.getTrustManagers()[0];
    int number = 0;
    for(Certificate cert : trustManager.getAcceptedIssuers()) {
        keyStore.setCertificateEntry(Integer.toString(number), cert);
        number++;
    }

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

然后就可以配置了,对于OkHttp:

public static OkHttpClient getHttpClient() throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
    OkHttpClient.Builder builder = new OkHttpClient.Builder();

    if (Build.VERSION.SDK_INT <= 25) {
        TrustManagerFactory tmf = getTrustManagerFactory();
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, tmf.getTrustManagers(), null);
        builder.sslSocketFactory(context.getSocketFactory(), (X509TrustManager) tmf.getTrustManagers()[0]);
    }
    return builder.build();
}

排球:

public RequestQueue configureRequestQueue(Context context) {
    if (Build.VERSION.SDK_INT <= 25) {
        try {
            TrustManagerFactory tmf = NetworkUtility.getTrustManagerFactory();
            // Create an SSLContext that uses our TrustManager
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), null);

            HurlStack httpStack = new HurlStack(null, sslContext.getSocketFactory());
            return Volley.newRequestQueue(context, httpStack);
        } catch (IOException | CertificateException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
            e.printStackTrace();
        }
    }
    return Volley.newRequestQueue(context);
}

对于 HttpsUrlConnection:

public void configureHttpsUrlConnection() {
    if (Build.VERSION.SDK_INT <= 25) {
        try {
            TrustManagerFactory tmf = getTrustManagerFactory();
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, tmf.getTrustManagers(), null);
            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
        } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | IOException | CertificateException e) {
            e.printStackTrace();
        }
    }
}

对于 Glide,只需为 GlideUrl 模型 class.

注册一个 OkHttp ModelLoaderFactory
@GlideModule
class GlobalGlideModule : AppGlideModule() {
    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
            try {

                val isrgRootX1 = ... // X509Certificate

                val handshakeCertificates = HandshakeCertificates.Builder()
                    .addTrustedCertificate(isrgRootX1)
                    .addPlatformTrustedCertificates()
                    .build()

                val okHttpClient = OkHttpClient.Builder()
                    .sslSocketFactory(handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager())
                    .build()

                // use our custom okHttp instead of default HTTPUrlConnection
                registry.replace(
                    GlideUrl::class.java,
                    InputStream::class.java,
                    OkHttpUrlLoader.Factory(okHttpClient)
                )
            } catch (t: Throwable) {
                super.registerComponents(context, glide, registry)
            }
        } else {
            super.registerComponents(context, glide, registry)
        }
    }
}

参见示例 in this PR

对于 2021 年 1 月的 Let's Encrypt 到期事件,不再需要这样做。

申请前阅读https://letsencrypt.org/2020/12/21/extending-android-compatibility.html