How to make Apache HttpClient trust Let's Encrypt Certificate Authority

Apache HttpClient is a popular library that many other frameworks build upon. Naming one for all - you can find it in Spring Framework RestTemplate. It's quite suprising that its not easy to find compherensible walkthroughs how to make it trust server certificates that are not validable by certificate chains baked in standard Java installation. Spending a few hours searching and tweaking the code I've decided to document easy - step by step solution how to do this for emerging Let's Encrypt certificate authority (but it's applicable to any other CA too).

Do you want to save several hours of yours? Read on :)

Let's make one thing clear in the beginning - this walkthrough guides you how to make certain CA trusted only by your application that you are developing. If you can and want to make CA trusted by entire Java installation (ie. all JVM instances running from it), you can add certificate to global Java truststore by following walkthrough. The reason I didn't want to go this way is that I didn't want to add certificate to truststore of all environments my application runs on (it's quite impractical).

If you want to keep this internal to your project, you need to ...

1) Download certificate of CA

In case of Let's Encrypt authority you'll find all certificates on this page. Namely you need this one:

https://letsencrypt.org/certs/isrgrootx1.pem

Download it, place it in your src/main/resources/somedirectory path as letsencrypt.cer (naming is not important here).

2) Download backup certificate of IdenTrust CA

You may also want to include IdentTrust root chain in your trust store - in case something goes wrong with LetsEncrypt root certificate:

https://www.identrust.com/certificates/trustid/root-download-x3.html

Download it, place it in your src/main/resources/somedirectory path as identrust.cer (naming is not important here).

3) Create truststore in a file on your classpath

Now we need to create new truststore file where we'll import this CA certificate. You can do this by running keytool in the directory where you saved letsencrypt.cer in previous step:

[source]keytool -keystore letsencrypt-truststore -alias isrgrootx -importcert -file letsencrypt.cer
keytool -keystore letsencrypt-truststore -alias identrust -importcert -file identrust.cer[/source]

File naming is again not important - but to successfully finish this walkthrough stick with these names. You can always rename files once your unit test is green. When executing this command you'll be asked for truststore passphrase. Enter passcode letsencrypt and hit enter. Next you'll be asked to confirm that you trust letsencrypt.cer file and you need to again confirm this.

If you want to verify that CA certificate is safely in your truststore run:

[source]keytool -list -v -keystore letsencrypt-truststore[/source]

If you want to play more with trustore read official documentation here.

Note aside: Do you ever wonder how truststore differs from keystore when Java code always uses only KeyStore class? Read more detailed description here but in short - trust store is only name convention for file where you store external certificates you trust compared to keystore where you store digital keys you own.

4) Exclude file from Maven filtering

We've created nice binary file on classpath that will be probably immediatelly corrupted by wonderful Maven feature, that is called filtering. So let's tell Maven not to touch our files on classpath:


<build>
   <resources>
      <resource>
         <directory>src/main/resources</directory>
         <filtering>false</filtering>
      </resource>
   </resources>
</build>

We can be more specific and let Maven filter some files while others not - but that's a task you can solve on your own.

5) Implement TrustManagerDelegate

Now let's do some coding. We need to teach Java to use our own truststore on classpath but fallback to its default trustore preinstalled with Java (we want to trust to every CA Java does and add a new CA on top of it). So we create delegating TrustManager that will ask main TrustManager managing our classpath truststore if certificate chain can be trusted and if not it delegates decision to fallback TrustManager which is the main Java one:


import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class TrustManagerDelegate implements X509TrustManager {
   private final X509TrustManager mainTrustManager;
   private final X509TrustManager fallbackTrustManager;
   public TrustManagerDelegate(X509TrustManager mainTrustManager, X509TrustManager fallbackTrustManager) {
      this.mainTrustManager = mainTrustManager;
      this.fallbackTrustManager = fallbackTrustManager;
   }
   @Override
   public void checkClientTrusted(final X509Certificate[] x509Certificates, final String authType) throws CertificateException {
      try {
         mainTrustManager.checkClientTrusted(x509Certificates, authType);
      } catch(CertificateException ignored) {
         this.fallbackTrustManager.checkClientTrusted(x509Certificates, authType);
      }
   }
   @Override
   public void checkServerTrusted(final X509Certificate[] x509Certificates, final String authType) throws CertificateException {
      try {
         mainTrustManager.checkServerTrusted(x509Certificates, authType);
      } catch(CertificateException ignored) {
         this.fallbackTrustManager.checkServerTrusted(x509Certificates, authType);
      }
   }
   @Override
   public X509Certificate[] getAcceptedIssuers() {
      return this.fallbackTrustManager.getAcceptedIssuers();
   }
}

6) Persuade HttpClient to use our truststore

Now we have to wire it all together. Let's create Spring FactoryBean for constructing HttpClient instance that will be used in our RestTemplate. Note several important points in this class:

  • getKeyStore - this method loads contents of our truststore file from classpath and parses them into KeyStore memory object - it uses passphrase to open the file
  • createSslContext - creates two TrustManagers - javaDefaultTrustManager that contains multiple CA certificates that Java trusts by default, customCaTrustManager that contains only single CA certificate that we imported to our file; these two managers are composed together by TrustManagerDelegate decribed above

import org.apache.commons.io.IOUtils;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLInitializationException;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
@Service
public class RestTemplateFactory implements FactoryBean&lt;CloseableHttpClient&gt; {
   private CloseableHttpClient client;
   private final SecureRandom secureRandom = new SecureRandom();
   @PostConstruct
   public void init() throws Exception {
      final SSLContext sslContext = createSslContext();
      SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);
      PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
            RegistryBuilder.&lt;ConnectionSocketFactory&gt;create()
                  .register("http", PlainConnectionSocketFactory.getSocketFactory())
                  .register("https", sslSocketFactory)
                  .build()
      );
      client = HttpClients.custom()
                     .setConnectionManager(cm)
                     .build();
   }
   @Override
   public CloseableHttpClient getObject() throws Exception {
      return client;
   }
   @Override
   public Class&lt;?&gt; getObjectType() {
      return CloseableHttpClient.class;
   }
   @Override
   public boolean isSingleton() {
      return true;
   }
   @PreDestroy
   public void close() throws Exception {
      client.close();
   }
   private SSLContext createSslContext() throws KeyStoreException, IOException, CertificateException {
      final SSLContext sslContext;
      try {
         sslContext = SSLContext.getInstance("TLS");
         final TrustManagerFactory javaDefaultTrustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
         javaDefaultTrustManager.init((KeyStore)null);
         final TrustManagerFactory customCaTrustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
         customCaTrustManager.init(getKeyStore());
         sslContext.init(
               null,
               new TrustManager[]{
                     new TrustManagerDelegate(
                           (X509TrustManager)customCaTrustManager.getTrustManagers()[0],
                           (X509TrustManager)javaDefaultTrustManager.getTrustManagers()[0]
                     )
               },
               secureRandom
         );
      } catch (final NoSuchAlgorithmException ex) {
         throw new SSLInitializationException(ex.getMessage(), ex);
      } catch (final KeyManagementException ex) {
         throw new SSLInitializationException(ex.getMessage(), ex);
      }
      return sslContext;
   }
   private KeyStore getKeyStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
      KeyStore ks = KeyStore.getInstance("JKS");
      InputStream is = new ClassPathResource("META-INF/lib_rest_api/certificate/letsencrypt-truststore").getInputStream();
      try {
         ks.load(is, "letsencrypt".toCharArray());
      } finally {
         IOUtils.closeQuietly(is);
      }
      return ks;
   }
}

7) Write the test

Now we have to write test to prove that we are able to communicate with TEST server using Let's Encrypt SSL certificate as well as PRODUCTION server using SSL certificate of some other trusted certificate authority:

import org.apache.http.HttpHost;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.junit.Assert;
import org.junit.Test;
public class RestTemplateFactoryTest {
   private RestTemplateFactory tested;
   @Test
   public void shouldContactTestApi() throws Exception {
      tested = new RestTemplateFactory();
      tested.init();
      final CloseableHttpClient client = tested.getObject();
      final CloseableHttpResponse response = client.execute(new HttpHost("test.mysecureddomain.com", 443, "https"), new HttpGet("/api/test"));
      Assert.assertNotNull(response);
   }
   @Test
   public void shouldContactProductionApi() throws Exception {
      tested = new RestTemplateFactory();
      tested.init();
      final CloseableHttpClient client = tested.getObject();
      final CloseableHttpResponse response = client.execute(new HttpHost("www.mysecureddomain.com", 443, "https"), new HttpGet("/api/test"));
      Assert.assertNotNull(response);
 }
}

Oh wait! Shouldn't we started with the test in the first place? :D

Happy coding ...

Credits: thanks for corrections from v6ak