I’ve written in the last several months about creating a client for a RESTful web-based auditing service. In that client, I had to implement client-side authentication, which is much more involved (or it should be anyway) than writing a simple secure web client that accesses content from secure public web servers.
Such a simple secure web client has only a little more functionality than a non-secure (http) web client. Essentially, it must perform a check after each connection to the secure web server to ensure that the server certificate is valid and trustworthy. This involves basically two steps:
- Verifying the server’s certificate chain.
- Verifying the server’s host name against that certificate.
Verifying the Certificate
The purpose of step 1 is to ensure that the service you’re attempting to use is not trying
to pull something shady on you. That is, the owner of the service was willing to put his or her name on the line with a Certificate Authority (CA) like Entrust or VeriSign. When you purchase a CA-signed certificate, you have to follow various procedures that document who you are, and why you’re setting up the service. But don’t worry – the CA doesn’t get to determine if your service is worthy of public consumption. Rather, only that you are who you say you are. The CA verifies actual existence, names, addresses, phone numbers, etc. If there’s any question about the service later, a consumer may contact that CA to find out the details of the service provider. This is dangerous for scam artists because they can be tracked and subsequently prosecuted. Thus, they don’t want to deal with Certificate Authorities if they don’t have to.
The client’s verification process (step 1) usually involves following the certificates in the certificate chain presented by the server back to a CA-signed certificate installed in its own trust store. A normal Sun JRE comes with a standard JKS truststore in $JAVA_HOME/lib/security/cacerts. This file contains a list of several dozen world-renowned public Certificate Authority certificates. By default, the SSLContext object associated with a normal HTTPSURLConnection object refers to a TrustManager object that will compare the certificates in the certificate chain presented by servers with the list of public CA certificates in the cacerts trust store file.
If you have an older cacerts file that doesn’t happen to contain a certificate for a site to which you are connecting, or if you’ve set up the site yourself using a self-signed certificate, then you’ll encounter an exception when you attempt to connect:
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
Ouch! Does this mean you can’t connect to your test server while writing your client code? Can you only test against public servers? No, of course not, but unfortunately, it does mean a bit more work for you. You have basically two options. The first is to install your test server’s self-signed certificate into your default trust store. I first learned about this technique from a blog entry by Andreas Sterbenz in October of 2006. Nice article, Andreas. Thanks!
However, there is another way. You can write some temporary code in the form of your own sort of dumb trust manager that accepts any certificate from any server. Of course, you don’t want to ship your client with this code in place, but for testing and debugging, it’s nice not to have to mess up your default trust store with temporary certs that you may not want in there all the time. Writing DumbX509TrustManager is surprisingly simple. As with most well-considered Java interfaces, the number of required methods for the X509TrustManager interface is very small:
public class MyHttpsClient { private Boolean isSecure; private String serverURL; private class DumbX509TrustManager implements X509TrustManager { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {} public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[] {}; } } ...
To make use of this trust manager, simply obtain an SSLSocketFactory object in your client’s constructor that you can configure with your dumb trust manager. Then, as you establish https connections to your test server, install your preconfigured SSLSocketFactory object, like this:
... private SSLSocketFactory getSocketFactory() { SSLSocketFactory socketFactory = null; try { SSLContext context = SSLContext.getInstance("SSLv3"); context.init(null, new X509TrustManager[] { new DumbX509TrustManager() }, null); socketFactory = context.getSocketFactory(); } catch (Exception e) { e.printstacktrace(); } return socketFactory; } public MyHttpsClient(String serverURL) { this.serverURL = serverURL; if (isSecure = serverURL.startsWith("https:")) sslSocketFactory = getSocketFactory(); } public void process() { try { HttpURLConnection conn = null; URL url = new URL(serverURL); try { conn = (HttpURLConnection)url.openConnection(); if (isSecure) { HttpsURLConnection sconn = (HttpsURLConnection)conn; sconn.setSSLSocketFactory(sslSocketFactory); } conn.setRequestMethod(verb); conn.setDoOutput(false); conn.setDoInput(true); conn.connect(); ...
That’s it. Warning: Don’t ship your client with DumbX509TrustManager in place. You don’t need it for public secure web servers anyway. If you know your client will only ever be used against properly configured public secure web servers, then you can rely on the default trust manager in the default socket factory associated with HttpsURLConnection.
If you think your client may be expected to work with non-public secure web servers with self-signed, or company-signed certificates, then you have more work to do. Here, you have two options. You can write some code similar to that found in browsers, wherein the client displays a dialog box upon connection, asking if you would like to connect to this “unknown” server just this once, or forever (where upon, the client then imports the server’s certificate into the default trust store). Or you can allow your customer to pre-configure the default trust store with certificates from non-public servers that he or she knows about in advance. But these are topics for another article.
Verifying the Server
Returning to the original two-step process, the purpose of step 2 (host name verification) is to ensure that the certificate you received from the service to which you connected was not stolen by a scammer.
When a CA-signed certificate is generated, the information sent to the Certificate Authority by the would-be service provider includes the fully qualified domain name of the server for which the new cert is intended. This FQDN is embedded in a field of the certificate, which the client uses to ensure that the server is really the owner of the certificate that it’s presenting.
As I mentioned in a previous article, Java’s keytool utility won’t let you generate self-signed certs containing the FQDN in the proper field, thus the default host name verification code will always fail with self-signed certs generated by keytool. Again, a simple dummy class comes to the rescue in the form of the DumbHostnameVerifier class. Just implement the HostnameVerifier interface, which has one required method, verify. Have it return true all the time, and you won’t see anymore Java exceptions like this:
HTTPS hostname wrong: should be <jmc-linux-64.provo.novell.com>
Here’s an example:
... private class DumbHostnameVerifier implements HostnameVerifier { public boolean verify(String arg0, SSLSession arg1) { return true; } } ... public void process() { ... if (isSecure) { HttpsURLConnection sconn = (HttpsURLConnection)conn; sconn.setSSLSocketFactory(sslSocketFactory); sconn.setHostnameVerifier(new DumbHostnameVerifier()); } ...
Scoping the Changes
A final decision you should make is the proper scope for setting the dummy trust manager and hostname verifier objects. The JSSE framework is extremely flexible. You can set these on a per-request basis, or as the class defaults, so that whenever a new HttpsURLConnection object is created, your objects are automatically assigned to them internally. For instance, you can use the following code to setup class default values:
public class MyHttpsClient { private static class DumbX509TrustManager implements X509TrustManager { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {} public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[] {}; } } private static class DumbHostnameVerifier implements HostnameVerifier { public boolean verify(String arg0, SSLSession arg1) { return true; } } private static SSLSocketFactory getSocketFactory() { SSLSocketFactory socketFactory = null; try { SSLContext context = SSLContext.getInstance("SSLv3"); context.init(null, new X509TrustManager[] { new DumbX509TrustManager() }, null); socketFactory = context.getSocketFactory(); } catch (Exception e) { e.printstacktrace(); } return socketFactory; } static { HttpsURLConnection.setDefaultHostnameVerifier( new DumbHostnameVerifier()); HttpsURLConnection.setDefaultSSLSocketFactory( getSocketFactory()); } private String serverURL; public MyHttpsClient(String serverURL) { this.serverURL = serverURL; } ...
You can now remove the isSecure check in the process routine, because new instances of HttpsURLConnection will automatically be assigned objects of your new trust manager and hostname verifier classes – the default objects you stored in the classes with the HttpsClient class’s static initializer.
With that, you’re set to connect to any https server. Here’s a little insight for you: The difficult part – the real work – of writing https clients involves writing real code for these classes. I’ll write a future article that provides details on these processes. Again, I remind you: Don’t accidentally ship your clients with DumbHostnameVerifier in place! (Unless, of course, you want to. After all, it’s your code…)
