Dynamic Java Truststore for a JAX-WS Client

When making a HTTPS request from a Java Client, the client receives a certificate and will verify the server by checking if the certificate exists in its truststore. If the certificate exists, the client will proceed the request, otherwise it will throw an exception. Even if the certificate is verified by a CA, it must be found in the truststore to proceed. Some solutions are:

  • Accept all certificates (trust any server)
  • Add the certificate file directly into the JRE truststore (usually located at JAVA_HOME/libs/security/cacerts)
  • Add the certificate to a keystore and override the default keystore by setting the system property javax.ssl.trustStore

Accepting all certificates just isn't a very secure solution and adding the certificate file to the default truststore may become tedious if the client is deployed on multiple systems with different operating systems. Specifying the custom keystore is also secure, but the client would lose access to the other certificates, unless you specify the location of the truststore before each web request to ensure the proper certificate is located.

The following solution provides the user complete control over the available certificates and update/remove certificates as one sees fit. This involves loading both the default truststore while importing the desired certificates to manage the truststore in-memory. This post builds upon John Calcote's nice guide here.

ReloadableX509TrustManager

This class, as the name suggests, is going to manage the client's trusting of other certificates. Reloadable implies that we'll be able to load new certificates into the client at any point during runtime and make changes on-the-fly. The class implements the X509TrustManager interface delegates the actual trusting logic to the X509TrustManager class.

class ReloadableX509TrustManager implements X509TrustManager {
  private String trustStorePath;
  private X509TrustManager trustManager;
  private ArrayList<Certificate> certList;

  public ReloadableX509TrustManager(String trustStorePath) throws Exception {
    this.trustStorePath = trustStorePath;
    certList = new ArrayList<Certificate>();
    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) {
      // Development logic only. Trusts the incoming untrusted certificate.
      // addCertificate(chain[0]);
      // reloadTrustManager();
      // trustManager.checkServerTrusted(chain, authType);
    }
  }

  @Override
  public X509Certificate[] getAcceptedIssuers() {
    return trustManager.getAcceptedIssuers();
  }

  /**
   * Instantiates a new X509TrustManager with the latest certificates
   */
  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); 
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      in.close(); 
    }

    // Add queued certificates to the keystore
    for (Certificate cert : tempCertList) {
      ts.setCertificateEntry(UUID.randomUUID(), cert);
    }

    // Initialize a new TrustManagerFactory with the truststore we created
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(ts);

    // Get the X509TrustManager from the 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");
  }

  /**
   * Adds cert to the TrustManager. Automatically reloads the TrustManager
   * @param cert is not null
   * @throws Exception if cannot be reloaded
   */
  private void addCertificate(Certificate cert) throws Exception {
    certList.add(cert);
    reloadTrustManager();
  }

  /**
   * Removes a certificate from the pending list. Automatically reloads the TrustManager
   * @param cert is not null and was already added
   * @throws Exception if cannot be reloaded
   */
  public void removeCertificate(Certificate cert) throws Exception {
    certList.remove(cert)
    reloadTrustManager();
  }

  /**
   * Adds a list of certificates to the manager. Automatically reloads the TrustManager
   * @param certs is not null
   * @throws Exception if cannot be reloaded
   */
  public void addCertificates(List<Certificate> certs) throws Exception {
    certList.addAll(certs);
    reloadTrustManager();
  }
}

From Calcote's post, public certificates are publicly visibile in any key or trust store, so a password is not needed to load the public certificates in the default trust store.

We are bascially instantiating a new instance of X509TrustManager each time we want to add new certificates to be trusted (or a list). But the question becomes, how do we use this code?

Implementing the trust manager in a JAX-WS Client

The goal here is to tell the runtime that you want to use this new class to verify certificates. We have to instantiate a new SSLContext with our ReloadableX509TrustManager and use this to specify a new SSLSocketFactory.

try {
    String trustStorePath = "path to a truststore that you have";
    String trustStorePassword = "password of trustStore";
    String defaultTrustStore = "path to default truststore";

    // Initialize the new trustManager with the default trust store
    ReloadableX509TrustManager trustManager = new ReloadableX509TrustManager(defaultTrustStore);

    // Load the new Keystore and decrypt it
    KeyStore ks = KeyStore.getInstance("JKS");
    ks.load(new FileInputStream(trustStorePath), trustStorePassword.toCharArray());

    // Add all of the certficates in the truststore and add them to the trust manager
    Enumeration<String> enumerator = ks.aliases();
    ArrayList<Certificate> certs = new ArrayList<>();
    while (enumerator.hasMoreElements()) {
        String currentAlias = enumerator.nextElement();
        certs.add(ks.getCertificate(currentAlias));
    }
    trustManager.addCertificates(certs);

    // Initialize the SSLContext and add it to the client conduit.
    SSLContext sc = SSLContext.getInstance("SSL");
    sc.init(null, new TrustManager[] {trustManager}, null);

    // Set the new TrustManager in the client.
    HTTPConduit httpConduit = (HTTPConduit) ClientProxy.getClient(service).getConduit();
    TLSClientParameters tlsCP = new TLSClientParameters(); 
    httpConduit.setTlsClientParameters(tlsCP);
    tlsCP.setSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
    e.printStackTrace();
}

This code is placed in the constructor of my client. service is the actual JAX-WS client object.

There are other ways to specify the SSLSocketFactory, but they didn't work for me, perhaps for you:

((BindingProvider) service).getRequestContext().put(JAXWSProperties.SSL_SOCKET_FACTORY, sc.getSocketFactory());

or

HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); 

Other

In this post you've seen how to manage an in-memory trustmanager in (in my opinion) the most flexible way. To make permanent changes to the truststore, we can follow Calcote's post and execute a truststore update command:

Runtime.getRuntime().exec("keytool -importcert ...");

To implement this on a server, the method checkServerTrusted should be changed to checkClientTrusted.

To make the ReloadableX509TrustManager thread-safe, the wasy way would be to add synchronize to the method signatures.

USB Serial for Blue Pill (STM32) with PlatformIO My First Hackathon Adventure
 

Add a comment