Configuring Apache HTTP client for Mutual Authentication over SSL

Some HTTP servers use mutual authentication over SSL (MASSL) to authenticate their clients and they reject requests that don’t present a valid and trusted certificate. In this post we will build a custom Apache HTTP client that can make HTTPS calls to a server that requires mutual authentication.

1. TLS client key and certificate

In order to send HTTPS requests to a server that requires MASSL, first we need to obtain a client certificate signed by a CA that is trusted by our target server. To do that:

  1. First, we generate a private key and save it in a file named client.key.pem.

  2. Then we generate a Certificate Signing Request (CSR) using our private key and submit it to to a CA trusted by the target server to have it signed and the CA will provide us with a signed certificate (e.g. client.cer.pem) and the certificate chain used to sign our certificate (e.g. client.cer.ca.pem).

  3. Finally we should provide our certificate (client.cer.pem) to the administrators of our target server to have it whitelisted.

2. Generating our program’s trust store and key store

Now that the target server trusts our certificate, and in order to conveniently use our cryptographic objects in our Java program, we should add the target server’s certificate to a trust store and our client key and certificate chain to a key store.

For the trust store, we have two options:

  1. Trusting the server’s certificate

  2. Trusting the server certificate’s issuer

In this example, we go with option 1.

2.1. Generating trust store

To obtain the target server’s certificate, we can use OpenSSL’s s_client command:

Listing 1. Using openssl s_client to obtain example.com’s certificate
echo -n | openssl s_client \
                  -connect example.com:448 \
                  -servername example.com \
                  -key  /path/to/example.key.pem \
                  -cert /path/to/example.cer.pem \
                  | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' \
                  | tee example.com.cer.pem

Then we will generate a trust store for our program and add the server certificate (example.com.cer.pem) to the trust store:

Listing 2. Adding the server’s certificate to a trust store
keytool -import \
        -alias example.com \
        -file example.com.cer.pem \
        -keystore truststore.jks \
        -storepass changeit \
        -noprompt

2.2. Generating client key store

In this step, we generate our client key store, containing:

  1. Our client private key

  2. Our client certificate

  3. Our client certificate chain

# Concatenating the client certificate chain, the client certificate, and the client private key

cat client.cer.ca.pem \
    client.cer.pem \
    client.key.pem \
    client.cer.chain.pem

# Producing a PKCS12 key store containing the key chain

openssl pkcs12 -export \
               -in client.cer.chain.pem \
               -out client.cer.chain.p12 \
               -name client.com \
               -noiter \
               -nomaciter \
               -passout pass:changeit

# Converting the PKCS12 key chain to JKS

keytool -importkeystore \
        -srckeystore client.cer.full-chain.p12 \
        -srcstoretype pkcs12 \
        -srcalias client.com \
        -destkeystore keystore.jks \
        -destalias client.com \
        -srcstorepass changeit \
        -deststorepass changeit \
        -destkeypass changeit \
        -noprompt

JKS is a proprietary key store format. “Newer” versions of the Java runtime also support the PKCS12 format out of the box. On these runtimes it is not necessary to convert our key store from PKCS12 format to JKS.

3. Creating HTTP client

Now that we have a key store that contains our private key and associated certificate and a trust store that contains the certificate of the Document Service Engine server, we can create a custom HTTPS client to make HTTPS calls to Document Service Engine. This is shown in Listing 3 and Listing 4:

Listing 3. MutualHttpsClientFactory.groovy
package com.example

import org.apache.http.client.HttpRequestRetryHandler
import org.apache.http.client.config.RequestConfig
import org.apache.http.conn.ssl.SSLConnectionSocketFactory
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClients
import org.apache.http.protocol.HttpContext
import org.apache.http.ssl.PrivateKeyDetails
import org.apache.http.ssl.PrivateKeyStrategy
import org.apache.http.ssl.SSLContexts

import javax.net.ssl.SSLContext
import java.security.KeyStore

class MutualHttpsClientFactory {

    static CloseableHttpClient newHttpsClient(KeyStoreConfig trustStoreConfig,
                                             KeyStoreConfig keyStoreConfig,
                                             KeyMaterialConfig keyMaterialConfig) {
        def keyStore = buildKeyStore(keyStoreConfig)
        def trustStore = buildKeyStore(trustStoreConfig)
        def sslContext = buildSslContext(
                keyStore,
                trustStore,
                keyMaterialConfig
        )

        def sslSocketFactory = new SSLConnectionSocketFactory(sslContext)

        return HttpClients.custom()
                          .setSSLSocketFactory(sslSocketFactory)
                          .setRetryHandler(retryHandler())
                          .setDefaultRequestConfig(requestConfig())
                          .build()
    }

    private static HttpRequestRetryHandler retryHandler() {
        return new HttpRequestRetryHandler() {
            @Override
            boolean retryRequest(IOException exception,
                                 int executionCount,
                                 HttpContext context) {

                return executionCount < 5
            }
        }
    }

    private static RequestConfig requestConfig() {
        def timeout = 5_000
        return RequestConfig.custom()
                            .setConnectTimeout(timeout)
                            .setConnectionRequestTimeout(timeout)
                            .setSocketTimeout(timeout)
                            .build()
    }

    private static KeyStore buildKeyStore(KeyStoreConfig config) {
        def keyStore = KeyStore.getInstance(config.type)
        keyStore.load(getClass().getResourceAsStream(config.path), config.password)

        return keyStore
    }

    private static SSLContext buildSslContext(KeyStore keyStore,
                                              KeyStore trustStore,
                                              KeyMaterialConfig keyMaterialConfig) {
        def privateKeyStrategy = new PrivateKeyStrategy() {
            @Override
            String chooseAlias(Map<String, PrivateKeyDetails> aliases,
                               Socket socket) {
                return keyMaterialConfig.alias
            }
        }

        return SSLContexts.custom()
                          .loadKeyMaterial(
                                  keyStore,
                                  keyMaterialConfig.password,
                                  privateKeyStrategy
                          )
                          .loadTrustMaterial(trustStore, null)
                          .build()
    }
}
Listing 4. Creating an HTTPS client
def truststoreConfig = new KeyStoreConfig(
        path: '/path/to/truststore.jks',
        type: 'JKS',
        password: 'changeit'
)

def keystoreConfig = new KeyStoreConfig(
        path: '/path/to/keystore.jks',
        type: 'JKS',
        password: 'changeit'
)

def keyMaterialConfig = new KeyMaterialConfig(
        alias: 'example.com',
        password: 'changeit'
)

def client = MutualHttpsClientFactory.newHttpClient(
        truststoreConfig,
        keystoreConfig,
        keyMaterialConfig
)