1. Introduction

Apache HttpClient is a low-level, lightweight client-side HTTP library for communicating with HTTP servers. In this tutorial, we’ll learn how to configure the supported Transport Layer Security (TLS) version(s) when using HttpClient. We’ll begin with an overview of how TLS version negotiation works between a client and a server. Afterward, we’ll look at three different ways of configuring the supported TLS versions when using HttpClient. In section 3, we describe how Apache HttpClient 4 differs regarding the static setting of TLS configuration.

2. Apache HttpClient 5

2.1. TLS Version Negotiation

TLS is an internet protocol that provides secure, trusted communication between two parties. It encapsulates application layer protocols like HTTP. The TLS protocol has been revised several times since it was first published in 1999. Therefore, it’s important for the client and server to first agree on which version of TLS they will use when establishing a new connection. The TLS version is agreed on after the client and server exchange hello messages:

  1. The client sends a list of supported TLS versions.
  2. The server chooses one and includes the selected version in the response.
  3. The client and server continue the connection setup using the selected version.

It’s important to correctly configure the supported TLS versions of a web client because of the risk of a downgrade attack. Note that in order to use the latest version of TLS (TLS 1.3), we must be using Java 11 or later.

2.2. Setting the TLS Version Statically

2.2.1. HttpClientConnectionManager

First, let’s create a connection manager with our custom TLS configuration. Then we set this connection manager to a custom ClosableHttpClient created with HttpClients.custom()

final HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
  .setDefaultTlsConfig(TlsConfig.custom()
     .setHandshakeTimeout(Timeout.ofSeconds(30))
     .setSupportedProtocols(TLS.V_1_2, TLS.V_1_3)
      .build())
   .build();

 return HttpClients.custom().setConnectionManager(cm).build();

The returned Httpclient object can now execute HTTP requests. By setting the supported protocols explicitly the client will only support communication over TLS 1.2 or TLS 1.3. 

2.2.2. Java Runtime Argument

Alternatively, we can configure the supported TLS versions using Java’s https.protocols system property. This method prevents having to hard-code values into the application code. Instead, we’ll configure the HttpClient to use the system properties when setting up connections. The HttpClient API provides two ways to do this. The first is via HttpClients#createSystem:

CloseableHttpClient httpClient = HttpClients.createSystem();

If more client configuration is required, we can use the builder method instead:

CloseableHttpClient httpClient = HttpClients.custom().useSystemProperties().build();

Both methods tell HttpClient to use system properties during connection configuration. This allows us to set the required TLS versions with a command-line argument during application runtime. For example:

$ java -Dhttps.protocols=TLSv1.1,TLSv1.2,TLSv1.3 -jar webClient.jar

2.3. Setting the TLS Version Dynamically

It’s also possible to set the TLS version based on connection details such as hostname and port. We’ll extend the SSLConnectionSocketFactory and override the prepareSocket method. The client calls the prepareSocket method before it initiates a new connection. This will let us decide which TLS protocols to use on a per-connection basis. It’s also possible to enable support for older TLS versions, but only if the remote host has a specific subdomain:

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(SSLContexts.createDefault()){

    @Override
    protected void prepareSocket(SSLSocket socket) {

        String hostname = socket.getInetAddress().getHostName();
        if (hostname.endsWith("internal.system.com")){
            socket.setEnabledProtocols(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3" });
        }
        else {
            socket.setEnabledProtocols(new String[] {"TLSv1.3"});
        }
    }
};<br />
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();

In the example above, the prepareSocket method first gets the remote hostname that the SSLSocket will connect to. The hostname is then used to determine with TLS protocols to enable. Now, our HTTP Client will enforce TLS 1.3 on every request except if the destination hostname is of the form *.internal.example.com. With the ability to insert custom logic before the creation of a new SSLSocket, our application can now customize the TLS communication details.

3. Apache HttpClient 4

3.1. Setting the TLS Version Statically

3.1.1. SSLConnectionSocketFactory

Let’s use the HttpClientBuilder exposed by the HttpClients#custom builder method in order to customize our HTTPClient configuration. This builder pattern allows us to pass in our own SSLConnectionSocketFactory, which will be instantiated with the desired set of supported TLS versions:

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
  SSLContexts.createDefault(),
  new String[] { "TLSv1.2", "TLSv1.3" },
  null,
  SSLConnectionSocketFactory.getDefaultHostnameVerifier());

CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();

The returned Httpclient object can now execute HTTP requests. By setting the supported protocols explicitly in the SSLConnectionSocketFactory constructor, the client will only support communication over TLS 1.2 or TLS 1.3. Note that in Apache HttpClient versions prior to 4.3, the class was called SSLSocketFactory.

4. Conclusion

In this article, we looked at three different ways of configuring the supported TLS versions when using the Apache HttpClient library. We’ve seen how the TLS versions can be set for all connections, or on a per-connection basis. The code samples used in this article are available over on GitHub. The v4 snippets are found here.