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.