Mutual TLS with Client Certs

Mutual TLS (where both the client and server have certificates that each validate) can be enabled by setting the withClientCertificateTrustManager(TrustManager) setting on the HttpsConfigBuilder.

You may want to do this if you want to authenticate your users using certificates that you sign.

This page will describe one way of how client certificates can be created and signed which are subsequently validated by mu-server. For information on setting up server-side TLS, see the HTTPS config documentation.

High level overview

You, the server owner, control who has access to your service by signing client certificates for clients. In other words, you need to become your own certificate authority.

Clients that want to access your service create a certificate request, which you can then sign and return to the client.

When the client makes a call to your web service, they include their signed certificate with the request to the HTTPS endpoint of your service. It is often referred to as "mutual" TLS because while the client verifies the TLS certificate of the server (as is normal for any HTTPS request) the server also verifies the certificate sent by the client was signed by your certificate authority.

Assuming the certificate is valid, then the server can assume that the client is who they say they are (or, like any secret-based authentication scheme, the client may be an attacker who has stolen the legitimate client's certificate).

The next section shows how to create a Certificate Authority, generate and sign client certificates, and make it work with mu-server.

Step by step instructions

First you need to create a Certificate Authority key and certificate which will let you sign certificates and then later verify that if was you that signed the certificate. One way to do this is using openssl from the command line. The following will create files called ca.key, ca.cert and ca.p12. Anyone with these files will be able to generate client certificates on your behalf so these must be stored securely.

openssl req -newkey rsa:4096 -keyform PEM -keyout ca.key -x509 -days 3650 -outform PEM -out ca.cer

The pass phrase entered on the first step will be needed in the next step and in the future when certificates are signed. Next step is to convert the certificate to PKCS12 format so that we can easily load it in Java.

openssl pkcs12 -export -inkey ca.key -in ca.cer -out ca.p12

You may be asked for a pass phrase for ca.key (which you created in step one) and then for another password which will be the password for the PKCS12 file. In this example, password was used for both.

We now need a mu-server created that loads this certificate into a javax.net.ssl.TrustManager object so that client certificates can be verified. Assuming the files are on the classpath, the following example creates the Trust Manager, passes it to the HTTPS config builder, and starts the server with a single handler that inspects the certificate.

public class ClientCert {
    public static void main(String[] args) throws Exception {

        KeyStore certificateAuthorityStore = KeyStore.getInstance("PKCS12");
        try (InputStream caStream = ClientCert.class.getResourceAsStream("/samples/client-cert/ca.p12")) {
            certificateAuthorityStore.load(caStream, "password".toCharArray());
        }
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX");
        trustManagerFactory.init(certificateAuthorityStore);
        TrustManager trustManager = Stream.of(trustManagerFactory.getTrustManagers())
            .filter(tm -> tm instanceof X509TrustManager)
            .findFirst()
            .orElseThrow(() -> new RuntimeException("Could not find the certificate authority trust store"));

        HttpsConfigBuilder httpsConfig = HttpsConfigBuilder.unsignedLocalhost()
            .withClientCertificateTrustManager(trustManager);

        MuServer server = muServer()
            .withHttpsPort(10443)
            .withHttpsConfig(httpsConfig)
            .addHandler(Method.GET, "/", (req, resp, pp) -> {
                Optional<Certificate> certificate = req.connection().clientCertificate();
                if (certificate.isPresent() && certificate.get() instanceof X509Certificate) {
                    X509Certificate clientCert = (X509Certificate) certificate.get();
                    X500Principal subject = clientCert.getSubjectX500Principal();

                    resp.sendChunk("Client cert received\n" +
                        "\nName: " + subject.getName() +
                        "\nValid dates: " + clientCert.getNotBefore() + " to " + clientCert.getNotAfter() +
                        "\n\n"
                    );

                    try {
                        clientCert.checkValidity();
                        resp.sendChunk("The certificate is valid");
                    } catch (CertificateNotYetValidException | CertificateExpiredException e) {
                        resp.sendChunk("The certificate not current");
                    }

                } else {
                    resp.write("No valid client certificate was sent");
                }
            })
            .start();

        System.out.println("Server started at " + server.uri());

    }
}
(see full file)

We now have a server that can verify and inspect client certificates.

Creating a client certificate

This is a two-step process:

  1. Create a certificate signing request - this should be performed by the client
  2. Sign the request with the certificate authority store

Create a request

The following command will create a private key that the client should keep secret:

openssl genrsa -out client.key 4096

The following creates a certificate request using that key:

openssl req -new -key client.key -out client.req

Only the client.req file should be shared with the service owner for them to sign.

Sign the request

The client should send client.req to the service owner. You can now use your ca.cer and ca.key files to create a signed certificate client.cer which they pass back to the client.

openssl x509 -req -in client.req -CA ca.cer -CAkey ca.key -set_serial 101 -extensions client -days 365 -outform PEM -out client.cer

If the client wants the certificate in PKCS12 format, it can be converted to create client.p12:

openssl pkcs12 -export -inkey client.key -in client.cer -out client.p12

Note: all the created files above are available here.

Send requests with the certificate

This is outside of the scope of this article as every client may do this differently. In general though, if client.p12 is registered with the user's browser then the browser can send the certificate with the request.

For a Java example where OkHttpClient is used, you can refer to Mu Server's client cert test which creates a client that uses a p12 certificate to make requests with.

In the following example on Windows, the client.p12 certificate was double-clicked on to register it in the Windows certificate store, and when loading the above server code the browser asks which certificate to send:

Prompt in Microsoft Edge asking which client certificate to send

The handler in the example above can then access various properties from the certificate which has been verified as being signed by the server's certificate authority:

Prompt in Microsoft Edge asking which client certificate to send