My application has a personal keystore containing trusted self-signed certificates for use in the local network - say mykeystore.jks
. I wish to be able to connect to public sites(say google.com) as well as ones in my local network using self-signed certificates which have been provisioned locally.
The problem here is that, when I connect to https://google.com, path building fails, because setting my own keystore overrides the default keystore containing root CAs bundled with the JRE, reporting the exception
sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
However, if I import a CA certificate into my own keystore(mykeystore.jks
) it works fine. Is there a way to support both?
I have my own TrustManger for this purpose,
public class CustomX509TrustManager implements X509TrustManager {
X509TrustManager defaultTrustManager;
public MyX509TrustManager(KeyStore keystore) {
TrustManagerFactory trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustMgrFactory.init(keystore);
TrustManager trustManagers[] = trustMgrFactory.getTrustManagers();
for (int i = 0; i < trustManagers.length; i++) {
if (trustManagers[i] instanceof X509TrustManager) {
defaultTrustManager = (X509TrustManager) trustManagers[i];
return;
}
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
try {
defaultTrustManager.checkServerTrusted(chain, authType);
} catch (CertificateException ce) {
/* Handle untrusted certificates */
}
}
}
I then initialize the SSLContext,
TrustManager[] trustManagers =
new TrustManager[] { new CustomX509TrustManager(keystore) };
SSLContext customSSLContext =
SSLContext.getInstance("TLS");
customSSLContext.init(null, trustManagers, null);
and set the socket factory,
HttpsURLConnection.setDefaultSSLSocketFactory(customSSLContext.getSocketFactory());
The main program,
URL targetServer = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection) targetServer.openConnection();
If I don't set my own trust managers, it connects to https://google.com just fine. How do I get a "default trust manager" which points to the default key store?
The answer here is how I came to understand how to do this. If you just want to accept the system CA certs plus a custom keystore of certs I simplified it into a single class with some convenience methods. Full code available here:
https://gist.github.com/HughJeffner/6eac419b18c6001aeadb
In
trustMgrFactory.init(keystore);
you're configuring defaultTrustManager with your own personal keystore, not the system default keystore.Based on reading the source code for sun.security.ssl.TrustManagerFactoryImpl, it looks like
trustMgrFactory.init((KeyStore) null);
would do exactly what you need (load the system default keystore), and based on quick testing, it seems to work for me.I've run into the same issue with Commons HttpClient. Working solution for my case was to create delegation chain for PKIX TrustManagers in following way:
And initialize HttpClient in following way (yes it's ugly):
In your case with simple HttpsURLConnection you may get by with simplified version of delegating class:
For Android developers, this can be much easier. In summary, you can add a xml res file to config your custom certs.
Step 1: open your manifest xml add an attribute.
Step 2: Add network_security_config.xml to res/xml, config certs as you want.
Note: this xml can support many other usage, and this solution only works on api24+.
Official reference: here
When you initialize an SSLContext you supply an array of TrustManagers. You supply two: a default one that uses the JRE truststore, and another one that uses yours. The delegation model s the wrong answer here.