【问题标题】:HTTPS with client authentication not working on Android带有客户端身份验证的 HTTPS 在 Android 上不起作用
【发布时间】:2016-06-03 01:44:10
【问题描述】:

我目前正在编写一个 Android 应用程序(最低 SDK 16),它可以查询 HTTPS 服务器以获取数据。服务器(Debian 8 上的 Apache 2.4)使用我们自己的 CA 签名的证书,并要求客户端也有一个由它签名的证书。在导入 PKCS 格式的 CA 和客户端证书后,这与 Firefox 完美配合。

但是,我无法让它在 Android 中运行。我正在使用 HttpsURLConnections,因为最近 Android 已弃用 Apache HTTP 客户端。信任我们的自定义 CA 是可行的,但只要我需要客户端证书,就会出现以下异常:

java.lang.reflect.InvocationTargetException
        [...]
    Caused by: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
        at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:282) 
        at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:192) 
        at eu.olynet.olydorfapp.resources.CustomTrustManager.checkServerTrusted(CustomTrustManager.java:96) 
        at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:614) 
        at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method) 
        at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:406) 
        at com.android.okhttp.Connection.upgradeToTls(Connection.java:146) 
        at com.android.okhttp.Connection.connect(Connection.java:107) 
        at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:294) 
        at com.android.okhttp.internal.http.HttpEngine.sendSocketRequest(HttpEngine.java:255) 
        at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:206) 
        at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:345) 
        at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:296) 
        at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:503) 
        at com.android.okhttp.internal.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:136) 
        at org.jboss.resteasy.client.jaxrs.engines.URLConnectionEngine.invoke(URLConnectionEngine.java:49) 
        at org.jboss.resteasy.client.jaxrs.internal.ClientInvocation.invoke(ClientInvocation.java:436) 
        at org.jboss.resteasy.client.jaxrs.internal.proxy.ClientInvoker.invoke(ClientInvoker.java:102) 
        at org.jboss.resteasy.client.jaxrs.internal.proxy.ClientProxy.invoke(ClientProxy.java:64) 
        at $Proxy9.getMetaNews(Native Method) 
        at java.lang.reflect.Method.invokeNative(Native Method) 
        at java.lang.reflect.Method.invoke(Method.java:515) 
        at eu.olynet.olydorfapp.resources.ResourceManager.fetchMetaItems(ResourceManager.java:372) 
        at eu.olynet.olydorfapp.resources.ResourceManager.getTreeOfMetaItems(ResourceManager.java:542) 
        at eu.olynet.olydorfapp.tabs.NewsTab$1.doInBackground(NewsTab.java:51) 
        at eu.olynet.olydorfapp.tabs.NewsTab$1.doInBackground(NewsTab.java:45) 
        at android.os.AsyncTask$2.call(AsyncTask.java:288) 
        at java.util.concurrent.FutureTask.run(FutureTask.java:237) 
        at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231) 
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112) 
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587) 
        at java.lang.Thread.run(Thread.java:841) 

在我看来,这似乎无法验证服务器证书,但事实并非如此。

这就是代码的样子:

private static final String CA_FILE = "ca.pem";
private static final String CERTIFICATE_FILE = "app_01.pfx";
private static final char[] CERTIFICATE_KEY = "password".toCharArray();

[...]

CertificateFactory cf = CertificateFactory.getInstance("X.509");
String algorithm = TrustManagerFactory.getDefaultAlgorithm();

InputStream ca = this.context.getAssets().open(CA_FILE);
KeyStore trustStore = KeyStore.getInstance("PKCS12");
trustStore.load(null);
Certificate caCert = cf.generateCertificate(ca);
trustStore.setCertificateEntry("CA Name", caCert);
CustomTrustManager tm = new CustomTrustManager(trustStore);
ca.close();

InputStream clientCert = this.context.getAssets().open(CERTIFICATE_FILE);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(clientCert, CERTIFICATE_KEY);
Log.e("KeyStore", "Size: " + keyStore.size());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
kmf.init(keyStore, CERTIFICATE_KEY);
clientCert.close();

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), new TrustManager[]{tm}, null);

[...]

((HttpsURLConnection) con).setSSLSocketFactory(sslContext.getSocketFactory());

CustomTrustManager 的相关函数(其中 localTrustManager 只包含我们的 CA,defaultTrustManager 是系统的 CA):

public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        try {
            localTrustManager.checkServerTrusted(chain, authType);
        } catch (CertificateException ce) {
            defaultTrustManager.checkServerTrusted(chain, authType);
        }
    }

我已经尝试过将 PKCS 文件转换为 BKS 文件(当然还有调整 KeyStore),但没有成功。我也在这里看到了类似的问题,但没有一个解决方案对我有用。

【问题讨论】:

    标签: java android ssl https httpsurlconnection


    【解决方案1】:

    我发现除了根 CA 之外,还可以添加中间 CA(直接签署服务器证书的 CA)。我不明白为什么这是必要的,因为如果服务器不需要客户端证书,则验证仅适用于根 CA。对我来说,这似乎是 HttpsURLConnections 或相关类的 Android 实现中的某种错误。如果我错了,请不吝赐教。

    工作代码:

    private static final String CA_FILE = "ca.pem";
    private static final String INTERMEDIATE_FILE = "intermediate.pem";
    private static final String CERTIFICATE_FILE = "app_01.pfx";
    private static final char[] CERTIFICATE_KEY = "password".toCharArray();
    
    [...]
    
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    String algorithm = TrustManagerFactory.getDefaultAlgorithm();
    
    /* trust setup */
    InputStream ca = this.context.getAssets().open(CA_FILE);
    InputStream intermediate = this.context.getAssets().open(INTERMEDIATE_FILE);
    KeyStore trustStore = KeyStore.getInstance("PKCS12");
    trustStore.load(null);
    Certificate caCert = cf.generateCertificate(ca);
    Certificate intermediateCert = cf.generateCertificate(intermediate);
    trustStore.setCertificateEntry("CA Name", caCert);
    trustStore.setCertificateEntry("Intermediate Name", intermediateCert);
    CustomTrustManager tm = new CustomTrustManager(trustStore);
    ca.close();
    intermediate.close();
    
    /* client certificate setup */
    InputStream clientCert = this.context.getAssets().open(CERTIFICATE_FILE);
    KeyStore keyStore = KeyStore.getInstance("PKCS12");
    keyStore.load(clientCert, CERTIFICATE_KEY);
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
    kmf.init(keyStore, CERTIFICATE_KEY);
    clientCert.close();
    
    /* SSLContext setup */
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(kmf.getKeyManagers(), new TrustManager[]{tm}, null);
    
    [...]
    
    ((HttpsURLConnection) con).setSSLSocketFactory(sslContext.getSocketFactory());
    

    【讨论】:

      猜你喜欢
      • 2013-02-21
      • 2017-04-08
      • 1970-01-01
      • 1970-01-01
      • 2015-07-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-12
      相关资源
      最近更新 更多