OkHttp: SSLPeerUnverifiedException 发生了一个找不到信任的证书,该证书签署了 X.509 证书的错误。

6

实际上,这个星期,我的问题和下面链接里的一样:

Android中OkHttpClient和证书验证

在我的手机(摩托罗拉,Android 4.1.2),我禁用了所有DigiCert CAs(在设置 - 安全 - 可信证书 - 系统里)。

以下是我的代码:

public class CertPinActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cert_pin);

        try {
            CertificatePinner certificatePinner = new CertificatePinner.Builder()
                    .add("github.com", "sha256/WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=")
                    .build();
            OkHttpClient client = new OkHttpClient.Builder()
                    .sslSocketFactory(getSSLSocketFactory())
                    .certificatePinner(certificatePinner)
                    .build();

            Request request = new Request.Builder()
                    .url("https://github.com/square/okhttp/wiki/HTTPS")
                    .build();
            client.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    Log.e("onFailure","-------------------------------------------------");
                    e.printStackTrace();
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    Log.d("onResponse", response.body().string());
                }
            });
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    private SSLSocketFactory getSSLSocketFactory()
            throws CertificateException, KeyStoreException, IOException,
            NoSuchAlgorithmException, KeyManagementException {
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        InputStream caInput = getResources().openRawResource(R.raw.github); // this is exported from Chrome then stored inside \app\src\main\res\raw path
        Certificate ca = cf.generateCertificate(caInput);
        caInput.close();
        KeyStore keyStore = KeyStore.getInstance("BKS");
        keyStore.load(null, null);
        keyStore.setCertificateEntry("ca", ca);
        String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
        tmf.init(keyStore);
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, tmf.getTrustManagers(), null);
        return sslContext.getSocketFactory();
    }
}

我的应用程序得到了以下的logcat(抱歉,我截取了一部分因为太长了)

06-14 09:10:10.065 30176-30211/com.example.okhttps3 E/onFailure: -------------------------------------------------
06-14 09:10:10.065 30176-30211/com.example.okhttps3 W/System.err: javax.net.ssl.SSLPeerUnverifiedException: Failed to find a trusted cert that signed X.509 Certificate:
06-14 09:10:10.065 30176-30211/com.example.okhttps3 W/System.err: [
06-14 09:10:10.065 30176-30211/com.example.okhttps3 W/System.err: [
06-14 09:10:10.065 30176-30211/com.example.okhttps3 W/System.err:   Version: V3
06-14 09:10:10.065 30176-30211/com.example.okhttps3 W/System.err:   Subject: CN=DigiCert SHA2 Extended Validation Server CA,OU=www.digicert.com,O=DigiCert Inc,C=US
06-14 09:10:10.065 30176-30211/com.example.okhttps3 W/System.err:   Signature Algorithm: SHA256WithRSAEncryption, params unparsed, OID = 1.2.840.113549.1.1.11
06-14 09:10:10.065 30176-30211/com.example.okhttps3 W/System.err:   Key: 
...................................................................................
06-14 09:17:56.813 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.internal.tls.CertificateChainCleaner$BasicCertificateChainCleaner.clean(CertificateChainCleaner.java:132)
06-14 09:17:56.813 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.CertificatePinner.check(CertificatePinner.java:149)
06-14 09:17:56.813 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.internal.io.RealConnection.connectTls(RealConnection.java:252)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.internal.io.RealConnection.establishProtocol(RealConnection.java:196)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.internal.io.RealConnection.buildConnection(RealConnection.java:171)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.internal.io.RealConnection.connect(RealConnection.java:111)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.internal.http.StreamAllocation.findConnection(StreamAllocation.java:187)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:123)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.internal.http.StreamAllocation.newStream(StreamAllocation.java:93)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.internal.http.HttpEngine.connect(HttpEngine.java:296)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.internal.http.HttpEngine.sendRequest(HttpEngine.java:248)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.RealCall.getResponse(RealCall.java:243)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.RealCall$ApplicationInterceptorChain.proceed(RealCall.java:201)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:163)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.RealCall.access$100(RealCall.java:30)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.RealCall$AsyncCall.execute(RealCall.java:127)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:569)
06-14 09:17:56.823 5934-5984/com.example.okhttps3 W/System.err:     at java.lang.Thread.run(Thread.java:856)

然而,在以下情况下调用onResponse

  1. 同时删除.sslSocketFactory(getSSLSocketFactory()).certificatePinner(certificatePinner)
  2. 仅删除.sslSocketFactory(getSSLSocketFactory());(实际上,在模拟器上,当系统CA被禁用时,将调用onFailure并在其中抛出java.security.cert.CertPathValidatorException:找不到认证路径的信任锚点
  3. 仅删除.certificatePinner(certificatePinner)

带有以下onResponse的Logcat(太长了,所以我截取了一部分):

06-14 09:06:23.143 26571-26616/com.example.okhttps3 D/onResponse: <!DOCTYPE html>
                                                                  <html lang="en" class="">
                                                                  .....

所以,我的问题是:

  1. 和我问题开头的链接一样(实际上是他的第一个问题);
  2. 为什么当使用certificatePinnergetSSLSocketFactory时,我的应用程序会收到javax.net.ssl.SSLPeerUnverifiedException: Failed to find a trusted cert that signed X.509 Certificate?请注意,这个SSLPeerUnverifiedException的内部消息与this JavaDoc中提到的Certificate pinning failure!不同。

更新:

对于第一个问题:

看起来这个问题(系统/用户受信任的凭证无效)只发生在我的运行Android 4.1.2的手机上,我已经检查了两台设备,所以我认为我应该联系制造商。

对于第二个问题:

根据@Robert下面的评论"我假设你必须包括完整的证书链(或者如果服务器向你发送链,则是根证书),而不仅仅是离开证书",我导出了根CA,如下面的截图所示,虽然不完整,但在getSSLSocketFactory内部,我改成了getResources().openRawResource(R.raw.github_rootca);

现在,我的第二个问题解决了!

enter image description here


我不明白你的第一个“问题”。 - Robert
@Robert:看起来这个问题实际上是他的第一个问题。因为我禁用了系统CA,但是即使删除了.sslSocketFactory(getSSLSocketFactory())onResponse仍然被调用。 - BNK
2个回答

5
如果您禁用了默认的系统证书信任库(使用或不使用自定义信任库的证书固定),则始终会出现异常,因为无法验证对等证书,因此。
  • 如果您不使用证书锁定并且您的默认系统证书信任库正常工作(使用Digicert High Assurance EV Root CA),则您的连接将OK
  • 如果您不使用证书锁定并且您的默认系统证书信任库不起作用,则您的连接将FAIL
  • 如果您使用证书锁定并且您的默认系统证书信任库正常工作(使用Digicert High Assurance EV Root CA),则您的连接将OK
  • 如果您使用证书锁定并且您的默认系统证书信任库不起作用,则您的连接将FAIL
  • 如果您使用证书锁定并且您的默认系统证书信任库不起作用,并且您设置了一个具有digicert CA的自定义信任库,则您的连接将OK
  • 如果您使用证书锁定并且您的默认系统证书信任库不起作用,并且您设置了一个没有digicert CA的自定义信任库,则您的连接将FAIL

一个使用证书固定+sslSocketFactory+自定义TrustManager的示例(Java)(基于OkHttp3 CustomTrust example

(digicert.cer包含Digicert High Assurance EV Root CA)

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
import java.util.Collection;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.junit.Test;

import junit.framework.TestCase;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class OkHttpTest extends TestCase {

    @Test
    public void test() {
        try {
            CertificatePinner certificatePinner = new CertificatePinner.Builder()
                    .add("github.com", "sha256/WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=").build();

            X509TrustManager trustManager;
            SSLSocketFactory sslSocketFactory;

            try {
                trustManager = trustManagerForCertificates(new FileInputStream(new File("/tmp/digicert.cer")));
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(null, new TrustManager[] { trustManager }, null);
                sslSocketFactory = sslContext.getSocketFactory();
            } catch (GeneralSecurityException e) {
                throw new RuntimeException(e);
            }

            OkHttpClient client = new OkHttpClient.Builder().sslSocketFactory(sslSocketFactory, trustManager)
                    .certificatePinner(certificatePinner).build();

            Request request = new Request.Builder().url("https://github.com/square/okhttp/wiki/HTTPS").build();

            client.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    e.printStackTrace();
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    System.out.println("onResponse: " + response.body().string());
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private X509TrustManager trustManagerForCertificates(InputStream in) throws GeneralSecurityException {
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
        if (certificates.isEmpty()) {
            throw new IllegalArgumentException("expected non-empty set of trusted certificates");
        }

        // Put the certificates a key store.
        char[] password = "password".toCharArray(); // Any password will work.
        KeyStore keyStore = newEmptyKeyStore(password);
        int index = 0;
        for (Certificate certificate : certificates) {
            String certificateAlias = Integer.toString(index++);
            keyStore.setCertificateEntry(certificateAlias, certificate);
        }

        // Use it to build an X509 trust manager.
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, password);
        TrustManagerFactory trustManagerFactory = TrustManagerFactory
                .getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
        if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
            throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
        }
        return (X509TrustManager) trustManagers[0];
    }

    private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            InputStream in = null;
            keyStore.load(in, password);
            return keyStore;
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }
}

1
谢谢,使用我的代码,如果我删除.sslSocketFactory(getSSLSocketFactory()).certificatePinner(certificatePinner)两个方法,在我的手机上,onResponse仍然被调用,在模拟器上,onFailure被调用。 - BNK
1
@vzamanillo:他是对的。即使在Android系统的受信任CA中禁用了特定证书,连接仍然会在我的手机上建立。 - Ashwin
@Ashwin和vzamanillo,请阅读我关于第一个问题的更新答案,谢谢。 - BNK
1
@BNK:感谢更新。我的手机也出现了这个问题,它运行的是Marshmallow(6.0)Motog3。这看起来像是制造商、操作系统或OkHttpClient方面的严重问题。我们必须报告这个问题! - Ashwin
可行的解决方案。谢谢。 - Zumbarlal Saindane

1
证书固定是一种额外的安全措施,因此证书必须由使用的TrustManager信任并且必须与固定的证书匹配。
由于您禁用了DigiCert CA证书,因此证书不受所使用的TrustManager信任,因此您会收到SSLPeerUnverifiedException。
我所描述的行为在CertificatePinner的JavaDoc中有记录:

关于自签名证书的说明

如果TrustManager不接受这样的证书,则不能使用CertificatePinner来固定自签名证书。


就像我的问题一样,如果只是删除.sslSocketFactory(getSSLSocketFactory()),那么就不会出现SSLPeerUnverifiedException - BNK
我认为 Github 使用的证书不是自签名的。 - BNK
文档链接中的异常信息是 SSLPeerUnverifiedException: Certificate pinning failure!,但是我的应用程序却出现了 SSLPeerUnverifiedException: Failed to find a trusted cert that signed X.509 Certificate - BNK
1
@BNK:正如我所说,您使用的TrustManager并没有按预期工作。虽然我没有测试过它,但我认为您必须包括完整的证书链(或者如果服务器向您发送链,则为根证书),而不仅仅是叶子证书。 - Robert
1
谢谢您的信息,但是如果是这样,我认为当系统CA被禁用时(例如在模拟器中),应该调用onFailure - BNK
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接