javasslkeystoretruststore

Using a custom truststore in java as well as the default one


I'm writing an application in Java which connects to two web servers via HTTPS. One got a certificate trusted via the default chain of trust, the other uses a self signed certificate. Of course, connecting to the first server worked out of the box, whereas connecting to the server with the self signed certificate did not work until I created a trustStore with the certificate from that server. However, the connection to the by default trusted server does not work any more, because apparently the default trustStore gets to be ignored once I created my own.

One solution I found was to add the certificates from the default trustStore to my own. However, I don't like this solution, because it requires me to keep managing that trustStore. (I cannot assume these certificates remain static in the foreseeable future, right?)

Apart from that I found two 5 year old threads with a similar problem:

Registering multiple keystores in JVM

How can I have multiple SSL certificates for a Java server

They both go deep into the Java SSL infrastructure. I was hoping that by now there is a more convenient solution which I can explain easily in a security review of my code.


Solution

  • You could use a similar pattern to what I've mentioned in a previous answer (for a different problem).

    Essentially, get hold of the default trust manager, create a second trust manager that uses your own trust store. Wrap them both in a custom trust manager implementation that delegates call to both (falling back on the other when one fails).

    TrustManagerFactory tmf = TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
    // Using null here initialises the TMF with the default trust store.
    tmf.init((KeyStore) null);
    
    // Get hold of the default trust manager
    X509TrustManager defaultTm = null;
    for (TrustManager tm : tmf.getTrustManagers()) {
        if (tm instanceof X509TrustManager) {
            defaultTm = (X509TrustManager) tm;
            break;
        }
    }
    
    FileInputStream myKeys = new FileInputStream("truststore.jks");
    
    // Do the same with your trust store this time
    // Adapt how you load the keystore to your needs
    KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    myTrustStore.load(myKeys, "password".toCharArray());
    
    myKeys.close();
    
    tmf = TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(myTrustStore);
    
    // Get hold of the default trust manager
    X509TrustManager myTm = null;
    for (TrustManager tm : tmf.getTrustManagers()) {
        if (tm instanceof X509TrustManager) {
            myTm = (X509TrustManager) tm;
            break;
        }
    }
    
    // Wrap it in your own class.
    final X509TrustManager finalDefaultTm = defaultTm;
    final X509TrustManager finalMyTm = myTm;
    X509TrustManager customTm = new X509TrustManager() {
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            // If you're planning to use client-cert auth,
            // merge results from "defaultTm" and "myTm".
            return finalDefaultTm.getAcceptedIssuers();
        }
    
        @Override
        public void checkServerTrusted(X509Certificate[] chain,
                String authType) throws CertificateException {
            try {
                finalMyTm.checkServerTrusted(chain, authType);
            } catch (CertificateException e) {
                // This will throw another CertificateException if this fails too.
                finalDefaultTm.checkServerTrusted(chain, authType);
            }
        }
    
        @Override
        public void checkClientTrusted(X509Certificate[] chain,
                String authType) throws CertificateException {
            // If you're planning to use client-cert auth,
            // do the same as checking the server.
            finalDefaultTm.checkClientTrusted(chain, authType);
        }
    };
    
    
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, new TrustManager[] { customTm }, null);
    
    // You don't have to set this as the default context,
    // it depends on the library you're using.
    SSLContext.setDefault(sslContext);
    

    You don't have to set that context as the default context. How you use it depends on the client library you're using (and where it gets its socket factories from).


    This being said, in principle, you'd always have to update the truststore as required anyway. The Java 7 JSSE Reference Guide had an "important note" about this, now downgraded to just a "note" in version 8 of the same guide:

    The JDK ships with a limited number of trusted root certificates in the java-home/lib/security/cacerts file. As documented in keytool reference pages, it is your responsibility to maintain (that is, add and remove) the certificates contained in this file if you use this file as a truststore.

    Depending on the certificate configuration of the servers that you contact, you may need to add additional root certificates. Obtain the needed specific root certificates from the appropriate vendor.