Implementing SSL/TLS in Springboot — Mutual TLS (mTLS) part-2

Paras Bansal
8 min readAug 2, 2024

--

This is part-2 of a two part series where I show how to implement SSL/TLS in springboot using self-signed certificates. This post covers the mutual TLS (mTLS) implementation. You can read about the standard TLS implementation here.

Mutual TLS is an extension of the standard TLS (Transport Layer Security) protocol, where both the client and the server authenticate each other. In a typical TLS connection, only the server presents a certificate to the client to prove its identity. In Mutual TLS, both parties present certificates to authenticate each other, ensuring a higher level of security.

How Mutual TLS Works

Client                               Server
| |
|--- Client Hello ------------------>|
| |
|<-- Server Hello -------------------|
|<-- Server Certificate -------------|
|<-- Client Certificate Request -----|
| |
|--- Client Certificate ------------>|
|--- Key Exchange ------------------>|
|--- Client Finished ----------------|
| |
|<-- Server Finished ----------------|
| |
|--- Secure Communication ---------->|
|<-- Secure Communication -----------|
  1. Client Hello: The client initiates the TLS handshake by sending a “Client Hello” message, including the supported cipher suites and TLS versions.
  2. Server Hello: The server responds with a “Server Hello” message, selecting the cipher suite and TLS version, and sends its certificate to the client.
  3. Client Certificate Request: The server requests a certificate from the client as part of the Mutual TLS process.
  4. Client Certificate: The client sends its certificate to the server, proving its identity.
  5. Key Exchange: Both parties use the certificates to exchange keys securely.
  6. Client Finished: The client sends a “Client Finished” message, indicating the end of the handshake from the client side.
  7. Server Finished: The server sends a “Server Finished” message, indicating the end of the handshake from the server side.
  8. Secure Communication: Once the handshake is complete, both parties can communicate securely using the established encrypted connection.

When Mutual TLS is more secure, why its not being used over internet?

For everyday purposes standard TLS works fine. The goal of standard tls is to ensure that client verifies the server identity only. This accomplishes:

  1. Server is genuine and is not a spoofed one
  2. The data that is travelling from client to the server is encrypted
  3. And the data is not altered during transit

Certificate Authorities in Mutual TLS

In case of standard TLS the certificates are issued by External organization which acts as certificate authority. When the browser makes a connect to the server it is able to identify the server's identity using the certificate presented by the server and it knows it is a genuine certificate because it is issued by a external organization known to the browser.

In case of mutual TLS the organization who implements it, itself acts as a Certificate Authority. A “root” TLS certificate is mandatory for implementing a mutual TLS. The certificates presented by both the clients and the server have to correspond to this “root” certificate. This enables organization implementing the mutual TLS to act as Certificate Authority implementing the “root” certificate.

Mutual TLS Use Cases

Mutual TLS is important for scenarios like:

  1. Business-to-business (B2B) data transmissions
  2. Internet of Things (IoT) sensors
  3. Connecting cloud services
  4. High-security applications
  5. Authenticating users into applications
  6. Authenticating devices onto a corporate or private network
  7. Content Delivery Networks (CDNs) or cloud security services onto backend servers

Trust Store

In this post since we will be implementing the mutual TLS we need to act as Certificate Authority as well. So we will bring an additional concept of trust store. A trust store is a place where we store the certificate of Certificate Authority which is used to sign all the other certificates. The trust store is basically placed on server side.

The benefits —

  1. You don't have to update the server certificate again and again while issuing the client certificate
  2. In case the client certificate expires, organization pretty much need to renew the certificate and trust store need not be updated
  3. The server can pretty much authenticate any client with a client certificate issued by the Certificate Authority

So let’s jump into how do we implement Springboot microservices implementing mutual TLS

First step is to generate the Certificate Authority key and its certificate —

#Generate the CA Key and Certificate
openssl genpkey -algorithm RSA -out ca.key
openssl req -new -x509 -days 365 -key ca.key -out ca.crt -subj "/C=US/ST=California/L=San Francisco/O=MyCompany/OU=IT/CN=myca.com"

Next step is to generate server and client certificates that are signed by the CA certificate as generated above.

# Generate server private key
openssl genpkey -algorithm RSA -out server.key

# Generate server certificate signing request (CSR)
openssl req -new -key server.key -out server.csr -subj "/C=US/ST=California/L=San Francisco/O=MyCompany/OU=IT/CN=server.mycompany.com"

# Generate server certificate by signing that with CA certificate and CA key
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt

# Generate client private key
openssl genpkey -algorithm RSA -out client.key

# Generate client certificate signing request (CSR)
openssl req -new -key client.key -out client.csr -subj "/C=US/ST=California/L=San Francisco/O=MyCompany/OU=IT/CN=client.mycompany.com"

# Generate client certificate by signing that with CA certificate and CA key
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt

Next we create the client and server keystores and trust stores —

# Create server keystore, here also we'll use CA cert to ensure "root" certificate chain is present
openssl pkcs12 -export -in server.crt -inkey server.key -out server-keystore.p12 -name server -CAfile ca.crt -caname root -chain

# Create client keystore, here also we'll use CA cert to ensure "root" certificate chain is present
openssl pkcs12 -export -in client.crt -inkey client.key -out client-keystore.p12 -name client -CAfile ca.crt -caname root -chain

# Create server truststore using CA certificate, so server know it can trust any certificates signed by CA (root) certificate
keytool -import -file ca.crt -alias ca -keystore server-truststore.jks

# Create client truststore using CA certificate, so client know it can trust any certificates signed by CA (root) certificate
# Pretty much same command as for server
keytool -import -file ca.crt -alias ca -keystore client-truststore.jks

Next we configure our server springboot application —

server:
port: 8443
ssl:
# Accepting HTTPS connections only
enabled: true
# The path to the keystore containing the certificate
key-store: classpath:server-keystore.p12
# The password used to generate the certificate
key-store-password: server
# The format used for the keystore
key-store-type: PKCS12
# The alias mapped to the certificate
key-alias: server
# The path to the truststore containing the client certificate
trust-store: classpath:server-truststore.jks
# The password used to generate the truststore
trust-store-password: serverjks
# Truststore alias
trust-store-alias: ca
# Client authentication is must
client-auth: need

Testing

I executed the below curl command by providing the client certificate, key and CA cert for my testing (If client is another springboot application, it will working using client-keystore.p12 and server-truststore.jks as we configured above) —

curl --location 'https://localhost:8443/api/execute' \
--header 'Content-Type: application/json' \
--cert client.crt --key client.key \
--data '{"data": "demo"}'

##I got below error -
curl: (60) SSL certificate problem: self signed certificate in certificate chain
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.

The same error as we got in the previous post. This problem is with the self-signed certificate as Curl is not able to find out a legitimate certificate for this. To avoid this problem either we tell Curl to ignore the legitimacy of the certificate by using the option -k OR we provide the CA certificate with the request itself as below —

curl --location 'https://localhost:8443/api/execute' \
--header 'Content-Type: application/json' \
--cert client.crt --key client.key --cacert ca.crt \
--data '{"data": "demo"}'

##I got below error -
curl: (60) SSL: certificate subject name 'server.mycompany.com' does not match target hostname 'localhost'
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.

This is a new issue, let’s review this problem in detail

Hostname Verification

When we generated the server certificate signing request we used below command —

openssl req -new -key server.key -out server.csr -subj "/C=US/ST=California/L=San Francisco/O=MyCompany/OU=IT/CN=server.mycompany.com"

Note here —

Common Name (e.g. server FQDN or YOUR name): server.mycompany.com

This indicates that the Common Name (CN) in the server certificate does not match the hostname you’re using to access the server. Hostname verification in HTTPS sounds simple enough. You call “https://server.mycompany.com", save off the “server.mycompany.com” bit, and then check it against the X.509 certificate from the server. If the names don’t match, you terminate the connection.

The scenario that requires hostname verification is when an attacker is on your local network, and can subvert DNS or ARP, and somehow redirect traffic through his own machine. Read more about this.

Solution

There are three solutions —

  1. During the certificate signing request (CSR) you can update the common name to localhost
openssl req -new -key server.key -out server.csr -subj "/C=US/ST=California/L=San Francisco/O=MyCompany/OU=IT/CN=localhost"

And then we generate the certificate and key store

2. Another thing that you can do is update your hosts file. Ensure server.mycompany.com resolves to the IP address of your server. You can update your /etc/hosts file (on Linux/Mac) or C:\Windows\System32\drivers\etc\hosts file (on Windows) to add an entry for server.mycompany.com pointing to 127.0.0.1.

Once done, you can send you curl request with — https://server.mycompany.com:8443/api/execute

3. What if I want the certificate to recognize both server.mycompany.com and localhost. I brought up this scenario because in real world we do want the certificates to support multiple domain names and don’t want the hostname verification to fail. For this scenario we provide the alternative DNS entries during the CSR and certificate generation.

Let’s see the process here —

For this, first you create a servercsr configuration file by providing the SSL config (servercsr.conf) —

[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no

[ req_distinguished_name ]
C = US
ST = California
L = San Francisco
O = MyCompany
OU = IT
CN = server.mycompany.com

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = server.mycompany.com
DNS.2 = localhost

Now we regenerate the CSR, certificate and the keystore files —

# Generate server certificate signing request (CSR) using servercsr.conf
openssl req -new -key server.key -out server.csr -config servercsr.conf

# Generate server certificate by signing that with CA certificate and CA key and using servercsr.conf
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -extensions req_ext -extfile servercsr.conf

# Create server keystore, here also we'll use CA cert to ensure "root" certificate chain is present
openssl pkcs12 -export -in server.crt -inkey server.key -out server-keystore.p12 -name server -CAfile ca.crt -caname root -chain

Now we copy the replace the server-keystore.p12 in the application and restart. Time to test —

curl -v --location 'https://localhost:8443/api/execute' \
--header 'Content-Type: application/json' \
--cert client.crt --key client.key --cacert ca.crt \
--data '{"data": "demo"}'

* Host localhost:8443 was resolved.
...
* Server certificate:
* subject: C=US; ST=California; L=San Francisco; O=MyCompany; OU=IT; CN=server.mycompany.com
* start date: Aug 2 05:22:08 2024 GMT
* expire date: Aug 2 05:22:08 2025 GMT
* subjectAltName: host "localhost" matched cert's "localhost"
* issuer: C=US; ST=California; L=San Francisco; O=MyCompany; OU=IT; CN=myca.com
* SSL certificate verify ok.
...
>
* upload completely sent off: 16 bytes
< HTTP/1.1 200
...
<
API HIT MESSAGE: demo* Connection #0 to host localhost left intact

The request goes file and you can see that hostname “localhost” was matched with the “subjectAltName”

With this approach you can always add more domains supported by the same server certificate for real world use cases.

This same step will be needed with client certificate and keystore as well when you plan to use them in the client application.

Make sure that you never loose the CA certificate and store it for safe keeping as you can use that to sign more client certificates and server will still honor the request as it has the CA truststore.

That’s it for this post, thank you for reading!

--

--

Paras Bansal
Paras Bansal

Written by Paras Bansal

Senior Architect, Engineer by heart

No responses yet