HTTPS Client Certificate Authentication With Java
Tags: java, https, ssl, vertx
Published: 2021-03-07 23:26:45

---
title: HTTPS Client Certificate Authentication With Java
published: true
description: Interfacing with an internally secured web service using Java.
tags: java, https, ssl, vertx
canonical_url: https://kevinmacksa.me/post/20210307-https-client-cert-auth-java-keytool/
---

I recently had to develop a Java client to interface with an internal service over HTTPS that required client certificate authentication. It is not often that I need to dive into SSL certificates, and doing so usually requires me to step back and relearn some things. This situation was no different, but in an attempt to burn this stuff into my brain, I am writing about it here.

## It's a Trust Thing

First thing's first: the client needs to trust the HTTPS connection that the service wants to establish. The server certificate used by the service is signed by an internal certificate authority (CA). Since the client code runs on the Java Virtual Machine (JVM), it is by default subject to the collection of trusted CA certificate chains (Chain of Trust) used the JVM, which -- and rightly so -- does not include the CA that signed the service's server certificate.

The JVM's Chain of Trust is contained inside the file `cacerts` located in the Java Development Kit's (JDK) security directory. You can use `keytool`, which comes with the Java Development Kit (JDK), if you are curious about which CAs are trusted by your JVM. In fact, `keytool` has the command-line option `-cacerts` to short-circuit specifying the JDK's `cacerts` as the command's keystore. For example, if you wanted to know if your JDK trusts certificates generated by Let's Encrypt, you would want to know that it trusts IdenTrust’s "DST Root CA X3":

```powershell
PS C:\Program Files\Java\zulu11.41.23-jdk11.0.8> .\bin\keytool.exe -list -cacerts | Select-String "IdenTrust"
Enter keystore password:

identrustcommercial [jdk], Jan 16, 2014, trustedCertEntry,
identrustdstx3 [jdk], Sep 30, 2000, trustedCertEntry,      # <-- Bingo!
identrustpublicca [jdk], Jan 16, 2014, trustedCertEntry,
```

Client code using the default JVM Chain of Trust will not allow a secure connection to be established with the service. The service's server certificate, inspected by the client's JVM during the SSL/TLS handshake, is seen as untrustworthy since it is signed by a CA not found in `cacerts`. This handshaking and rejection of trust is taken care of by the JVM networking and security code running beneath the client code.

As a quick demonstration, the following ([Spock](https://spockframework.org/)) test asserts that the client JVM code fails to create an SSL connection with the service. Note that I chose to use [Vert.x Web Client](https://vertx.io/docs/vertx-web-client/java/) to handle interacting with the service, but don't let this decision distract from the core content of this post. Nevertheless, if you haven't used Vert.x, I encourage you to try it out -- especially for building server-side network applications.

```groovy
class ClientTest extends Specification {
  Vertx vertx
  WebClientOptions webClientOptions
  WebClient webClient
  AsyncConditions conditions

  def setup() {
    vertx = Vertx.vertx()
    webClientOptions = new WebClientOptions().setSsl(true)
    conditions = new AsyncConditions(1)
  }

  def "cannot establish secure connection with an untrusted CA"() {
    given:
    webClient = WebClient.create(vertx, webClientOptions)

    expect:
    webClient.get(443, "service.cn.local", "/api/endpoint").send({ event ->
      conditions.evaluate {
        assert event.failed()
        assert SSLHandshakeException.class.isInstance(event.cause())
        assert "Failed to create SSL connection" == event.cause().message
      }
    })
  }

  def cleanup() {
    webClient?.close()
    vertx.close()
  }
}
```

The solution is straight-forward: we need to tell the JVM that it can trust the service's server certificate.

## Trust Me, I Got This

There are a couple of ways to have the JVM trust that the connection attempted to be made by the service really is secure:

1. Add the CA certificate chain to the JVM's `cacerts`.
2. Create a separate keystore and use that in the client code.

Although the first option provides a quick solution to the problem, it will effectively allow any future code that runs on the JVM with the modified `cacerts` to trust certificates signed by the internal CA. This may be perfectly acceptable given the CA;
e.g., if it's the root CA of your organization, then it probably makes sense to include it in your JVM's default Chain of Trust. In my case, the internal CA is for a dedicated and isolated system,
and I actually do not want to create an environment where any Java application running on the JVM can interface with the internal service. I only want the one application that I am developing to have this capability. Additionally, the client code does not need to interface with any other system over a secure connection, so I do not need to trust any SSL certificates besides those that are signed by the internal CA. Therefore, the second option suits me better.

### Create a Java Keystore

The good people at IT sent me the internal CA certificate chain and a client certificate for authenticating with the service. The CA certificate chain was a PFX file and the client certificate was a P12 file. I installed both on my Windows machine to enable my web browsers to trust and authenticate with the internal service.

From this setup, a separate Java keystore containing the internal CA certificate chain can be created. First, export the root CA certificate and its intermediate certificates. These can be found on a Windows machine at `Control Panel > Internet Options > Content (Tab) > Certificates (Button)`. From there,

1. find and export the trusted root CA as a DER encoded binary X.509 (CER), and
2. find and export each intermediate certificates as a DER encoded binary X.509 (CER).

Use `keytool` to create a new keystore file that trusts the exported certificates (add the root CA first):

```powershell
PS C:\Program Files\Java\zulu11.41.23-jdk11.0.8> .\bin\keytool.exe -importcert -alias internal-ca-root -file C:\Temp\internal-ca-root.cer --storetype JKS -keystore C:\Temp\internal.jks
Enter keystore password:
Re-enter new password:

Trust this certificate? [no]:  yes
Certificate was added to keystore

PS C:\Program Files\Java\zulu11.41.23-jdk11.0.8> .\bin\keytool.exe -importcert -alias internal-ca-intermediate -file C:\Temp\internal-ca-intermediate.cer --storetype JKS -keystore C:\Temp\internal.jks
Enter keystore password:
Re-enter new password:

Trust this certificate? [no]:  yes
Certificate was added to keystore
```

Because the first command creates the keystore, the password you enter when prompted will set the password for it. When prompted for the password after entering the second command, the same password must be used.

### Configure Client to Use Keystore

The created Java keystore containing the Chain of Trust can now be used in the client code:

```groovy
class ClientTest extends Specification {
  
  // ...

  def "can establish secure connection with separate keystore"() {
    given:
    webClientOptions
        .setTrustStoreOptions(new JksOptions()
            .setPath(CUSTOM_KEYSTORE_PATH)
            .setPassword(CUSTOM_KEYSTORE_PASSWORD))

    webClient = WebClient.create(vertx, webClientOptions)

    when:
    webClient.get(443, "service.cn.local", "/api/endpoint").send({ event ->
      conditions.evaluate {
        assert event.succeeded()
        assert CONNECTED_BUT_NOT_AUTHENTICATED == event.result().bodyAsString()
      }
    })

    then:
    conditions.await(5)
  }
}
```

Although a secure connection is now established between the client and service, the two-way SSL authentication is not yet satisfied. Hence, the service responds with a message indicating the client cannot reach the desired endpoint because it was not authenticated.

## Certificate, Please

As mentioned at the beginning of the post
-- and the original motivation for writing this --
the internal web service uses two-way SSL authentication. This means a client is authenticated when it presents a client certificate to the service that is issued by the root CA. If a client certificate is not provided (like in the test above) or it is not signed by the root CA, then the service denies the client's request.

The final step, therefore, is to configure the client code to authenticate with the server using the client certificate:

```groovy
class ClientTest extends Specification {
  
  // ...

  def "can establish secure connection and authenticate with client certificate"() {
    given:
    webClientOptions
        .setTrustStoreOptions(new JksOptions()
            .setPath(CUSTOM_KEYSTORE_PATH)
            .setPassword(CUSTOM_KEYSTORE_PASSWORD))
        .setPfxKeyCertOptions(new PfxOptions()
            .setPath(CLIENT_CERT_PATH)
            .setPassword(CLIENT_CERT_PASSWORD))

    webClient = WebClient.create(vertx, webClientOptions)

    when:
    webClient.get(443, "service.cn.local", "/api/endpoint").send({ event ->
      conditions.evaluate {
        assert event.succeeded()
        assert CONNECTED_AND_AUTHENTICATED == event.result().bodyAsString()
      }
    })

    then:
    conditions.await(5)
  }
}
```

That's it. The client-server connection is secured over HTTPS, and the service authenticates the client. Note that Vert.x Web Client makes this easy, but I assume it is just as easy with other HTTP client libraries that support HTTPS.

## Conclusion

Sometimes you need to establish secure connections with servers that have not been signed by a globally trusted entity. In these situations, it is can be desirable to create a custom keystore containing the Chain of Trust associated with the server(s) you need to interface with. Java's `keytool` can be used to do this.
If a server employs two-way SSL authentication, you can use an HTTP library, like Vert.x Web Client, to add a client certificate to your HTTPS connection.

_Fin_.