I'm currently writing an Android App (Min SDK 16) that queries a HTTPS server for data. The server (Apache 2.4 on Debian 8) uses a certificate signed by our own CA and requires clients to also have a certificate signed by it. This works perfectly with Firefox after importing both the CA and the client certificate in PKCS format.
I am, however, unable to get this to work in Android. I'm using HttpsURLConnections, as the Apache HTTP Client has been deprecated for Android recently. Trusting our custom CA works, but as soon as I require the client certificate, I get the following Exception:
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)
To me this looks like the server certificate cannot be verified, which should not be the case.
This is what the code looks like:
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());
Relevant function of the CustomTrustManager (where localTrustManager contains just our CA and defaultTrustManager the system's CAs):
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
localTrustManager.checkServerTrusted(chain, authType);
} catch (CertificateException ce) {
defaultTrustManager.checkServerTrusted(chain, authType);
}
}
I've already tried converting the PKCS file to a BKS file (and adapting the KeyStore, of course) without success. I've also seen the similar questions here but none of the solutions worked for me.