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:
- Create a certificate signing request - this should be performed by the client
- 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:
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: