Popular searches
//

X.509 client certificates with Spring Security

20.8.2018 | 9 minutes reading time

A disclaimer: this blogpost is a story about the reasons why I ended up securing my API using the X.509 client certificate, in addition to a step-by-step guide on how to implement this yourself. Someone will hopefully find it useful.

Securing your application or an API is always a challenge, and lack of experience with the topic makes it even more complicated. Deciding on what security approach to take, how to properly implement it, what vectors of attacks you’ll be vulnerable to, dealing with the soup of acronyms and nouns such as SSL/TLS, CA, CRT, public/private keys, keystore, truststore – you quickly find yourself with a panicky feeling in your stomach. And this is a pretty common reaction.

First of all, X.509 is a digital certificate which uses the X.509 public key infrastructure standard to verify that a public key, which belongs to a user, service or a server, is contained within the certificate, as well as the identity of said user, service, or server.
The certificate can be signed by a trusted certificate authority, or self-signed.
SSL and TLS are most widely known protocols which use the X.509 format. They are routinely used to verify the identity of servers each time you open your browser and visit a webpage via HTTPS.

The goal in mind is to secure communication from a known server to my service. The decision ultimately came down to use the client certificate approach since authenticating users is not my concern – users do not interact with me directly. This means that there are no username/passwords being sent back and forth, no cookies and no sessions – which means that we maintain statelessness of our REST API. Also, as I am the certificate authority, I’m always going to stay in control of who gets a valid certificate, meaning I only trust myself to manage and maintain who can talk to my service.

The general workflow

In order to secure and authenticate communication between client and the server, they both need to have valid certificates. When you send a browser request to an HTTPS website, your browser would just verify that the site is certified by a trusted authority. In this case, not only the server’s identity is verified, but also the server gets to verify the client.

client certificate

The first thing the client has to do in order to communicate with the secured service is to generate a private key and a certificate signing request (CSR). This CSR is then sent to a Certificate Authority (CA) to be signed. In my use case, I represent both the server and the CA, since I want to be in charge of managing who gets to talk to my service. Signing the CSR produces the client certificate which is then sent back to the client.
In order to send a valid and authenticated HTTPS request, the client also needs to provide the signed certificate (unlocked with the client’s private key), which is then validated during the SSL handshake with the trusted CA certificate in the Java truststore on the server side.

Enough theory, let’s see what the implementation looks like.

Spring Security Configuration

My REST service is a regular spring-boot 2.0.2 app using the spring-boot-starter-security dependency:

The configuration class:

Usually known to be cumbersome, in this case the SpringSecurityConfig class is pretty lightweight, since we want to authenticate all requests coming into the service, and we want to do so using x509 authentication.
SessionCreationPolicy.NEVER tells Spring to not bother creating sessions since all requests must have a certificate.
We can also disable cross-site request forgery protection since we aren’t using HTML forms, but only send REST calls back and forth. You must do so if you’re going to follow this blog to the end, because CURL requests won’t pass through Spring’s csrf filter.

Enabling HTTPS on the REST service itself is just a manner of setting a couple of properties in our application.properties file:

And this is pretty much it, you can go on and create your @RestControllers with endpoints fully secured behind a x509 certificate.

Generating a server CA certificate

Let’s see what has to be done on the server’s side with regards to creating the certificate:

First of all, we have to generate an rsa key encrypted by aes256 encryption which is 2048 bits long. 4096 length would be more secure, but the handshake would be slowed down quite significantly. 1024 is also an option for faster handshakes but is obviously less secure. Used server as pass phrase here.

Now, we use the generated key in order to create a x509 certificate and sign it with our key. A form must be filled out which will map the certificate to an identity. Most of the fields can be filled out subjectively, except the CN (common name) which must match the domain we are securing (in this case, it’s localhost).

imports our server CA certificate to our Java truststore. The stored password in this case is changeit.

exports the server CA certificate to our keystore. The stored password is again changeit.

Note: you could use .jks as the format of the keystore instead of .p12, you can easily convert it with:

Generating a client certificate

The client has to go through a similar process:

Again, the first thing we have to do is to create the private key. Interactively asks for a passphrase, I’m using client here.

Now we create the certificate signing request and sign it with the client’s private key. We are asked to fill a form to map the identity to the output certificate. Similar to the step 2 when generating the Server CA, the CN is the most important field and must match the domain.

Client sends the CSR to the CA

CA does this step, not the client. We sign the certificate signing request using the server’s private key and the CA.crt. client.crt is produced, and it has to be securely sent back to the client.

Certificates in action

Now that we have everything configured and signed, it’s time to see if it all ties in properly.
First thing, we can send a request without the certificate:

and this will produce an error, just as we would have hoped:

This time we create a request with the certificate (using the client’s private key):

at this point we are asked for the key’s passphrase, type in client
produces a nice “200 OK” response!

Example POST request:

type in client as before

You can set

in your application.properties to trace the handshake.

We can see that the received certificate is signed by our own trusted serverCA.crt (Issuer: EMAILADDRESS being ognjen.misic@codecentric.de – the email was set in the second step when generating the serverCA.crt, and the Subject: EMAILADDRESS is ognjen.misic@client.com, the value that was set when the client was generating the CSR).

The security principal:

And that would be it!

Special thanks to Jonas Hecht, whose example helped me quite a bit to grasp the workflow of this topic (you can find it here: https://github.com/jonashackt/spring-boot-rest-clientcertificate ) and to Daniel Marks, for helping me fill out the missing pieces of the puzzle.