This is the latest (and probably last) in my series of client-side Java key and trust store management articles, and a good summary article for the topic, I hope.
It’s clear from the design of SSLContext
in the JSSE that Java key and trust stores are meant to contain static data. Yet browsers regularly display the standard security warning dialog when connecting to sites whose certificates have expired or whose administrators haven’t bothered to purchase a CA-signed certificate. This dialog generally offers you three choices:
- Get me out of here!
- I understand the risks: add certificate for this session only
- I understand the risks: add certificate permanently
In this article, I’d like to elaborate on what it means to “add certificate” – either temporarily or permanently.
Let’s start with a simple Java http(s) client:
public byte[] getContentBytes(URI uri, SSLContext ctx) throws Exception { URL url = uri.toURL(); URLConnection conn = url.openConnection(); if (conn instanceof HttpsURLConnection && ctx != null) { ((HttpsURLConnection)conn).setSSLSocketFactory( ctx.getSocketFactory()); } InputStream is = conn.getInputStream(); int bytesRead, bufsz = Math.max(is.available(), 4096); ByteArrayOutputStream os = new ByteArrayOutputStream(bufsz); byte[] buffer = new byte[bufsz]; while ((bytesRead = is.read(buffer)) > 0) os.write(buffer, 0, bytesRead); byte[] content = os.toByteArray(); os.close(); is.close(); return content; }
This client opens a URLConnection
, reads the input stream into a byte buffer, and then closes the connection. If the connection is https – that is, an instance of HttpsURLConnection
– it applies the SocketFactory
from the supplied SSLContext
.
NOTE: I’m purposely ignoring exception managment in this article to keep it short.
This code is simple and concise, but clearly there’s no way to affect what happens during application of the SSL certificates and keys at this level of the code. Certificate and key management is handled by the SSLContext
so if we want to modify the behavior of the SocketFactory
relative to key management, we’re going to have to do something with SSLContext
before we pass it to the client. The simplest way to get an SSLContext
is to call SSLContext.getDefault
in this manner:
byte[] bytes = getContentBytes( URI.create("https://www.example.com/"), SSLContext.getDefault());
The default SSLContext
is fairly limited in functionality. It uses either default key and trust store files (and passwords!) or else ones specified in system properties – often via the java command line in this manner:
$ java -Djavax.net.ssl.keyStore=/path/to/keystore.jks \ -Djavax.net.ssl.keyStorePassword=changeit \ -Djavax.net.ssl.trustStorePath=/path/to/truststore.jks \ -Djavax.net.ssl.trustStorePassword=changeit ...
In reality, there is no default keystore, which is fine for normal situations, as most websites don’t require X.509 client authentication (more commonly referred to as mutual auth). The default trust store is $JAVA_HOME/jre/lib/security/cacerts, and the default trust store password is changeit. The cacerts file contains several dozen certificate authority (CA) root certificates and will validate any server whose public key certificate is signed by one of these CAs.
More importantly, however, the default SSLContext
simply fails to connect to a server in the event that a trust certificate is missing from the default trust store. But that’s not what web browsers do. Instead, they display the aforementioned dialog presenting the user with options to handle the situation in the manner that suits him or her best.
Assume the simple client above is a part of a larger application that adds certificates to the trust store during execution of other code paths and then expects to be able to use this updated trust store later during the same session. This dynamic reload functionality requires some SSLContext
customization.
Let’s explore. SSLContext
is a great example of a composite design. It’s built from several other classes, each of which may be specified by the user when initializing a context object. This practically eliminates the need to sub-class SSLContext
in order to define custom behavior. The default context is eschewed in favor of a user-initialized instance of SSLContext
like this:
public SSLContext getSSLContext(String tspath) throws Exception { TrustManager[] trustManagers = new TrustManager[] { new ReloadableX509TrustManager(tspath) }; SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, trustManagers, null); return sslContext; }
At the heart of this method is the instantiation of a new ReloadableX509TrustManager
. The init
method of SSLContext
accepts a reference to an array of TrustManager
objects. Passing null
tells the context to use the default trust manager array which exihibits the default behavior mentioned above.
The init
method also accepts two other parameters, to which I’ve passed null
. The first parameter is a KeyManager
array and the third is an implementation of SecureRandom
. Passing null
for any of these three parameters tells SSLContext
to use the default. Here’s one implementation of ReloadableX509TrustManager
:
class ReloadableX509TrustManager implements X509TrustManager { private final String trustStorePath; private X509TrustManager trustManager; private List tempCertList = new List(); public ReloadableX509TrustManager(String tspath) throws Exception { this.trustStorePath = tspath; reloadTrustManager(); } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { trustManager.checkClientTrusted(chain, authType); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { try { trustManager.checkServerTrusted(chain, authType); } catch (CertificateException cx) { addServerCertAndReload(chain[0], true); trustManager.checkServerTrusted(chain, authType); } } @Override public X509Certificate[] getAcceptedIssuers() { X509Certificate[] issuers = trustManager.getAcceptedIssuers(); return issuers; } private void reloadTrustManager() throws Exception { // load keystore from specified cert store (or default) KeyStore ts = KeyStore.getInstance( KeyStore.getDefaultType()); InputStream in = new FileInputStream(trustStorePath); try { ts.load(in, null); } finally { in.close(); } // add all temporary certs to KeyStore (ts) for (Certificate cert : tempCertList) { ts.setCertificateEntry(UUID.randomUUID(), cert); } // initialize a new TMF with the ts we just loaded TrustManagerFactory tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); tmf.init(ts); // acquire X509 trust manager from factory TrustManager tms[] = tmf.getTrustManagers(); for (int i = 0; i < tms.length; i++) { if (tms[i] instanceof X509TrustManager) { trustManager = (X509TrustManager)tms[i]; return; } } throw new NoSuchAlgorithmException( "No X509TrustManager in TrustManagerFactory"); } private void addServerCertAndReload(Certificate cert, boolean permanent) { try { if (permanent) { // import the cert into file trust store // Google "java keytool source" or just ... Runtime.getRuntime().exec("keytool -importcert ..."); } else { tempCertList.add(cert); } reloadTrustManager(); } catch (Exception ex) { /* ... */ } } }
NOTE: Trust stores often have passwords but for validation of credentials the password is not needed because public key certificates are publicly accessible in any key or trust store. If you supply a password, the KeyStore.load
method will use it when loading the store but only to validate the integrity of non-public information during the load – never during actual use of public key certificates in the store. Thus, you may always pass null
in the second argument to KeyStore.load
. If you do so, only public information will be loaded from the store.
A full implementation of X509TrustManager
is difficult and only sparsely documented but, thankfully, not necessary. What makes this implementation simple is that it delegates to the default trust manager. There are two key bits of functionality in this implementation: The first is that it loads a named trust store other than cacerts. If you want to use the default trust store, simply assign $JAVA_HOME/jre/lib/security/cacerts to trustStorePath
.
The second bit of functionality is the call to addServerCertAndReload
during the exception handler in the checkServerTrusted
method. When a certificate presented by a server is not found in the trust manager’s in-memory database, ReloadableX509TrustManager
assumes that the trust store has been updated on disk, reloads it, and then redelegates to the internal trust manager.
A more functional implementation might display a dialog box to the user before calling addServerCertAndReload
. If the user selects Get me out of here!, the method would simply rethrow the exception instead of calling that routine. If the user selects It’s cool: add permanently, the method would add the certificate to the file-based trust store, reload from disk, and then reissue the delegated request. If the user selects I’ll bite: add temporarily, the certificate would be added to a list of temporary certificates in memory.
The way I’ve implemented the latter case is to add the certificate to a temporary list and then reload from disk. Strictly speaking, reloading from disk isn’t necessary in this case since no changes were made to the disk file but the KeyStore built from the disk image would have to be kept around for reloading into the trust manager (after the new cert was added to it), so some modifications would have to be made to avoid reloading from disk.
This same code might as well be used in a server-side setting but the checkClientTrusted
method would have to be modified instead of the checkServerTrusted
method as in this example.
Thanks- you’ve saved me a fair bit of time!
This post was helpful. Question: I don’t necessiarly have control to provide an SSLContext to those who want to use it. I would like to make this TrustManager usable anytime any piece of code does something that uses SSL. Such as people doing SSL JNDI sessions etc, or HTTP or anything.
How can I set this up for system wide use?
This post has helped me a lot but I was wondering how the code would vary for the server side. I am currently using a keystore for encryption/signature on a server which is to be used by many people and their certificates must be dynamically added without causing disruptions i.e without restarting tomcat. Do you know of any way that this would be possible ? I have tried re-loading the keystore and re-initializing the SSLContext but this didn’t work. Any help would be greatly appreciated.
Hi Daniel. Good question. When I mentioned the “server side” in the last paragraph of the article, I was thinking of a private http server implementation, not a servlet container like tomcat. To do what you want to do, I’m guessing you’d have to subclass servlet container components to add key/trust store reload functionality. It’s a lot harder than the client-side algorithm I mentioned. Not that it’s undoable, mind you. After all, this is software. Anything can be done if you’re willing to dig deeply enough.
One suggestion that comes to mind is to store a root signing certificate in your server-side truststore. Then sign your client-side trust certificates with this root signing cert. Now, when your clients use their new certs, the root signing cert is already in your truststore. I think this is a rather common approach to this problem. Of course, your needs may vary, but it keeps you from having to add dozens of newly created trust certs to your server-side truststore, and it solves the reload problem.
Thanks a lot for your response and I will be giving that a try 🙂
Hi, I have got the server working now with updating the keystore in real-time and I thought I would share how I made it work. I tried the couple of methods that you suggested, the first seemed too troublesome and the second wasn’t successful because the client has to use their private key for a signature but I did come up with the idea of creating a modified WSS4J jar where I altered the WSDoAllReceiver to validate the x509 certificate against the keystore which I would load for each call.
Its not quite as fast as using the keystore in memory but it saves the hassle of not having to restart after each alteration in the keystore.
keerthi here,
these lines of code from above,
// acquire X509 trust manager from factory
TrustManager tms[] = tmf.getTrustManagers();
for (int i = 0; i < tms.length; i++) {
if (tms[i] instanceof X509TrustManager) {
trustManager = (X509TrustManager)tms[i];
return;
}
}
i have seen them in so many places.. but i just wonder why we are gettin the first X509TrustManager instance only.. what are the other trust managers in that array.. thank you.
Hi Keerthi – There are multiple kinds of trust managers. X509 is only one of them. But there’s only one of each in the trust manager array. So, in this case, we’re looking for *the* X509 trust manager within the array of trust managers that might exist. In practice, there’s often only one or two, but we still have to look for the one we want.
thank u very much John.. I think I am fine understanding the concept now..
Thank u once again for your voluntary help.. very much appreciated,.. 🙂
Hello,
First of all, great tutorial! 🙂
Second, I have a question regarding a certificate issue. My server application is running on a WAS Server which is configured to run on https. I followed your tutorial and I wanted to make my client app, which should connect to the WAS sever through https from my Java code. The problem is that I don’t know what certificate should I put in my trustedStorePath, I’ve tryied to access the WAS server via my browser and save the certificate using my browser, but the problem is that I get an exception:
java.io.IOException: Invalid keystore format
how can I get around this?
Thanks!
Your help is greatly appreciated!
With this example, I encountered exceptions that looked like this:
Caused by: java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
These were completely valid exceptions thrown by the wrapped X509TrustManager’s checkServerTrusted method. I change the catch from CertificateException to just regular Exception and after that things worked quite nicely.
Hi John,
Thanks for the great post! I’d like to reuse the implementation of ReloadableX509TrustManager. Can you please let me know if there is any licensing or terms of use involved?
– Nishant
No licensing – help yourself.
java.io.IOException: Error initializing server socket factory SSL
context: Invalid keystore format
I was trying to do an SSL implementation in my webservice call to JBOSS ESB server, i use a self signed certificate. I get the above error.
Below is the small extract from jboss-esb.xml file, shows how i refer my keystore and truststore file inside the xml file. Please let me know where exactly i go wrong.
–
–
What keystore format are you using?
In what format do you store the certificate in your keystore and in what format are you reading it out?
format ? We are generating cert using command “keytool genkey” and storing as .keystore only. We are using self signed certificates only. I guess it is generating in the default format only, am not too familiar on that. I guess JBOSS ESB is having issues understandng it.
Appreciate your help if you could tell what exactly the command/steps to create keystore and truststore, i can try once.
Thanks in advance.
Hmm…your keystore is probably right, but maybe the self-signed certificate that you add to your keystore does not have the right format.
Thanks for your help. One note, javax.net.ssl.trustStorePath whould be javax.net.ssl.trustStore remove the “Path” from the name.