Switching Java HTTP client code over to HTTPS can go without a hitch when hitting well-established production servers. When it comes to in-house boxes or development and test environments though, hokey server certificates can throw up a few problems.
HTTP client code often starts out as just that. Whether it’s as complex as calling a big web service or as simple as retrieving some XML or JSON from a RESTful service, chances are the journey kicked off with an http:// address.
URL requestUrl = new URL("http://maps.googleapis.com/maps/api/" + "geocode/json?address=Buckingham+Palace&sensor=false"); try(BufferedReader reader = new BufferedReader(new InputStreamReader(requestUrl.openStream()))) { String thisLine = null; while((thisLine = reader.readLine()) != null) { System.out.println(thisLine); } }
And if you’re hitting a well-established, Internet facing production service, switching to HTTPS when you need to is likely to go without a hitch:-
URL requestUrl = new URL("https://maps.googleapis.com/maps/api/" + "geocode/json?address=Buckingham+Palace&sensor=false"); try(BufferedReader reader = new BufferedReader(new InputStreamReader(requestUrl.openStream()))) { String thisLine = null; while((thisLine = reader.readLine()) != null) { System.out.println(thisLine); } }
However, if it’s a development server, test environment or even a production B2B environment without an SSL certificate issued by a well known Certificate Authority, switching to HTTPS might have some nasty surprises in store:-
URL requestUrl = new URL("https://localhost:20443/HttpsClients/doRequest"); try(BufferedReader reader = new BufferedReader(new InputStreamReader(requestUrl.openStream()))) { String thisLine = null; while((thisLine = reader.readLine()) != null) { System.out.println(thisLine); } }
Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target … Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target … Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target …
This particular log full of stack traces happens because your client code, by default, needs to be configured to “trust” the certificate of an SSL server. The certificate also needs to be valid and match the name of the server you’re hitting. Any violation of those rules will cause it to fail to communicate and this can be a real hassle at times, especially in more chaotic lesser development and test environments.
Fortunately there are a couple of easy solutions plus a tempting cheat-mode option for those lesser environments too.
Trust the server certificate
Whether it’s a self-signed certificate like this one, or a properly signed certificate from a backwater or internal Certificate Authority, you always have the option of trusting the certificate itself by simply adding it to your keystore.
To do this you first need to get a copy of the certificate file itself. You can do this by copying the server URL into your web-browser, clicking on the padlock (or whatever https icon it gives you to identify secure sites), viewing the certificate and then saving a copy to your machine. In Safari you simply drag the certificate image to your local filesystem, in other browsers look for the export or save button.
You then use the keytool command provided with the JDK to add this file to either an existing truststore or to a newly created one. There will be one in your JDK but it’s best not to modify that as you’ll both risk losing it when upgrading and losing track of the certificates you need when running your application elsewhere:-
keytool -import -keystore my-truststore.jks -storepass changeit -alias localhost -file myServerCertificate.cer
Here we’re creating a new truststore in the current directory called my-truststore.jks and using the frequently recognised default password of changeit to “secure” it. The alias option is simply a label for use when managing multiple certificates in a single store.
The command will list out the details of the certificate and then ask you if you want to trust it:-
Trust this certificate? [no]:
Type yes followed by the enter key and the certificate will be written into the keystore file:-
Certificate was added to keystore
Presuming you’re not adding the certificate to your JDK’s trust store, you’ll also need to tell your application where to find it. Stick the following two -D options on your java command:-
-Djavax.net.ssl.trustStore=my-truststore.jks -Djavax.net.ssl.trustStorePassword=changeit
You’ll probably need to fully path the trustStore file name and ensure the trustStorePassword matches the one assigned to your keystore. Your application should now connect with no problems
Trust the server certificate issuer
The above is pretty much your only choice with self-signed certificates, but in cases like this one here, where the certificate has been signed by an in-house Certificate Authority you can also trust the issuer itself, which will mean your app will trust any other certificates signed by it too.
You can get the CA certificate from your web browser in the same way as the server certificate, you just need to select the root of the chain before exporting or saving it.
Then use the same command you used before but with this CA certificate instead:-
keytool -importcert -alias devsumoCA -file devsumo-ca.crt -keystore my-ca-truststore.jks -keypass changeit
Point your application at this truststore and you should connect okay, and be able to connect to any other server with a certificate issued by the same CA.
Trust everything
Development and test environments can be messy. Domain names and IPs can be fluid, you may be dealing with a mixture of self-signed and internally issued certificates which will all eventually expire and need updating and you may well need to manage these across multiple development and test environments. You may be wishing you didn’t need to care. Fortunately, with a little effort, you don’t have to if you tell your application to use a trust manager that trusts everything.
Taking a closer look at the full stack trace for our original error, you’ll notice references to an X509TrustManagerImpl class (or something similar depending on your JDK):-
… at sun.security.ssl.X509TrustManagerImpl.validate (X509TrustManagerImpl.java:326) at sun.security.ssl.X509TrustManagerImpl.checkTrusted (X509TrustManagerImpl.java:231) at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted (X509TrustManagerImpl.java:126) at sun.security.ssl.ClientHandshaker.serverCertificate (ClientHandshaker.java:1323) …
You can create a class implementing X509TrustManager yourself, one that trusts absolutely anything:-
public class OpenTrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return null; } public static void apply(HttpsURLConnection conn) throws NoSuchAlgorithmException, KeyManagementException { SSLContext sc = SSLContext.getInstance("TLS"); sc.init(null, new TrustManager[] { new OpenTrustManager() }, new java.security.SecureRandom()); conn.setSSLSocketFactory(sc.getSocketFactory()); } }
This class simply implements the three methods defined for X509TrustManager and ensures they throw no errors. For convenience we’ve also added a static apply method which will attach it to our freshly minted HttpsURLConnection instances.
If we modify our client code to call this method for HTTPS requests then our code should call any HTTPS service.
URL requestUrl = new URL("https://localhost:20443/HttpsClients/doRequest"); URLConnection conn = requestUrl.openConnection(); if(conn instanceof HttpsURLConnection) { OpenTrustManager.apply((HttpsURLConnection)conn); } try(BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { String thisLine = null; while((thisLine = reader.readLine()) != null) { System.out.println(thisLine); } }
Well, almost any. Unfortunately, if we replace localhost with 127.0.0.1 instead, we’re back in stack-trace country:-
Exception in thread "main" javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No name matching localhost found … Caused by: java.security.cert.CertificateException: No name matching localhost found …
Our certificate may pass muster with our OpenTrustManager but the mismatch between the hostname we’re using and the hostname of the certificate is still stonewalling us. HttpsURLConnection uses a HostNameVerifier instance to do this check and we can get around this by installing our own; one which gives the nod to any name or IP we’re using:-
URL requestUrl = new URL("https://127.0.0.1:20443/HttpsClients/doRequest"); URLConnection conn = requestUrl.openConnection(); if(conn instanceof HttpsURLConnection) { OpenTrustManager.apply((HttpsURLConnection)conn); ((HttpsURLConnection)conn).setHostnameVerifier( new HostnameVerifier(){ public boolean verify(String arg0, SSLSession arg1) { return true; } } ); } try(BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { String thisLine = null; while((thisLine = reader.readLine()) != null) { System.out.println(thisLine); } }
This probably isn’t code you want running in a production environment (or at least it isn’t code you want to be responsible for running in a production environment). However with a little more effort we can switch it in based on a factory class driven by a system property which would enable it in our lesser environments but keep it well away from production. And it can make managing complex development and test environments that little bit easier.
After all, every little helps!
works perfect!