top of page
  • Writer's pictureMichael Paltsev

Hands-on TLS with Java


In my last post about TLS, we expanded our theoretical knowledge about TLS and learned how TLS v1.3 works. But what is this knowledge worth without putting it to practice? In this post, I would like to take you to the practical aspects of TLS. We will create a very basic server in Java, we'll create a secure and encrypted connection to it using TLS v1.3, and will try to debug it using several tools. While doing so we will also acquire some knowledge about different settings that you can set for your application. All of the code of course will be available to you in my GitHub so you can try to run all of the examples yourself.


What are we waiting for then? Let's begin!


Some Tools

First things first, we will need some tools. If you are a Java developer, chances are that Java, Maven, and Docker are already installed on your machine, if not, install them.

Another important tool is Wireshark. Maybe you've heard about it, maybe you've even used it and maybe it's the first time you see its name. Either way, we'll be using it throughout this article. So I'll talk about it in the next paragraph but I won't be giving a thorough tutorial about it here.


As you've probably guessed, it can be very helpful to see what is going on when you are trying to debug an issue. Can Wireshark help me with that? Yes, it can! Wireshark is an open-source and free network protocol analyzer. That means that it can help you see the traffic, at different network layers, flowing between different parties in your network. And if we want to be precise, to and from a selected network interface. Another question that you may have is: if TLS encrypts traffic, can Wireshark see it? Keep reading to get an answer to this question.


Our Simple Java REST Server

First, let's create a very simple REST server in Java using the Quarkus framework. The code is very simple:

@Path("/hello")
public class GreetingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello from ZenNote";
    }
    
}

Let's run this first without enabling TLS and look at the traffic with Wireshark.

We will have to build it, create the docker image, and run it.

Go to the root folder of the code and run the following commands:

mvnw clean package

This will use maven to build the application. Next, we'll build the docker image that contains the JAR.

docker build -f simple-server/src/main/docker/Dockerfile.plain -t zennote.simple-server:plain .

After the image is built, we can run our container with an exposed 8080 port:

docker run -p 8080:8080 zennote.simple-server:plain

I like using containers for these tasks because it allows me to get the same results wherever I'm running the code. But, you can also run the code on your machine.


Analyzing Plain Traffic with Wireshark

Before being able to look at the traffic, we need to understand what network interface we should monitor. We'll get that info from the running docker container by executing:

docker ps

To get the container id (in my case it was a8266353a448) and then execute this command to get the number of the interface that is being used by the docker container.

docker exec -it a8266353a448 cat /sys/class/net/eth0/iflink

In my case it was number 8, with which I will try to get the interface name:

ip link | grep 8

This gives me:

8: veth44220d6@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default 

So, I'm searching for veth44220d6 in Wireshark, and here it is:

Double-click on it and we will start listening...

Now, let's send a request to the server using curl:

We've sent a request and got the response we've been waiting for. Now, what about Wireshark? We can see the TCP handshake, the HTTP request, and also the response.

That's brilliant!


Ok, so we did some very simple debugging. No TLS involved, so no worries. We are about to spice things up, let us introduce TLS.


Trust Store and Key Store

Before we plunge into the rabbit hole and start talking about TLS configuration in Java, there are two important topics we need to introduce: the trust store and the key store.


Let us go back a little and remember how a TLS connection is made. During the handshake, the server (and sometimes the client)send their public certificate for the other party to authenticate them. That's great in theory, but who generates the certificate and where do we keep it? Well, we know that we can get a certificate from a CA. In that case, usually, you will receive a PEM (Privacy-enhanced Electronic Mail) file, which is a Base64 encoded DER (Distinguished Encoding Rules) data.


But what if you want to create a certificate for development purposes? How do you do that? Well, there are at least two options: use openssl which is a command line tool, or if you are using Java, you can use the keytool which is also a command line tool that comes with Java.


The keytool can be used to create a key store which is a file that acts as a container for private/public key pairs such as a private key and a corresponding public key (for asymmetrical encryption), a secret key (for symmetrical encryption) and certificates that allow us to identify a party.


There are several supported formats for key stores where JKS was the default one up until Java 8 and from Java 9 the default became PKCS12. In Java, we refer to a key store when we want to store keys that allow us to encrypt our data and certificates that we send to other parties when we are asked to identify ourselves (as in the TLS handshake). A trust store is also a key store, but it has a different purpose. It stores the trust certificates that allow us to identify the party with which we are trying to communicate.


Based on that, if we want to enable TLS in our environment, we will need to have a key store for our server, that will hold the signed certificate, and a trust store for our client that will hold the CA certificate.


Java has a default trust store that holds the CA certificates. This file is usually located under the $JAVA_HOME/lib/security folder and it's called cacerts. The default password for the cacerts file is "changeit". If you don't have the $JAVA_HOME environment variable set, there are a couple of ways to get it but the best way is to install sdkman - this tool will help you manage your SDKs and will also set the $JAVA_HOME folder for you.


That being said, you should not use it and always generate a trust store or/and a key store for your application by yourself with a strong password.


Adding TLS to our Server

To enable TLS on our server we will build a new docker image with a self-signed certificate and will see what happens.


The dockerfile has the following command:

keytool -genkey -keyalg RSA -alias self-signed -keystore $KEY_STORE_FILE -validity 365 -keysize 2048 -keypass $KEY_STORE_PASSWORD -storepass $KEY_STORE_PASSWORD -dname CN=UNKNOWN

This will generate our self-signed certificate, encrypt it with a password provided by us, and store it in /tmp/simple-server.pkcs12.


Execute:

docker build -f simple-server/src/main/docker/Dockerfile.selfsigned --build-arg KEY_STORE_PASSWORD=changeit -t zennote.simple-server:selfsigned 
docker run -p 8080:8080 -p 443:443 zennote.simple-server:selfsigned

To build a new docker image, and run it.


Now our server is configured to run with TLS 1.3 (you can look into the application.properties and at the Dockerfile files to understand how I've configured it) and is exposing port 443 for us to reach it.

lf

We will execute the same flow for getting the network interface name and will start listening to it. After that we will execute the curl command but instead of hitting port 8080, we will hit https://localhost:443/hello. Let's see what happens:

Alas! It failed... curl tells us that it has a certificate problem, it's self-signed.

That means that curl failed to verify the legitimacy of the server and therefore could not

establish a secure connection to it (you can run curl with the -vvv option to get a more verbose output).


Let's take a look at what Wireshark has captured:

Again, we can see that the TCP handshake was made, a Client Hello (TLS v1.3) was sent following a Server Hello and an encrypted x509 certificate to which the client (curl) responded with an alert message and terminated the TLS handshake.


The easiest way to get the encrypted x509 cert (if we are not the ones who created it in the first place) is to run the following command (replace localhost:443 with the server you are fetching the certs from):

openssl s_client -showcerts -connect localhost:443

We know that we've self-signed a cert and that it has no valid CA which will fail the handshake (read my article about TLS). Let's create a cert that will allow us to talk to our server.


Creating a Signed Certificate

Let's begin, we need to create 2 (self-signed) certs: a Root CA - which will be used to sign the server certificate, and a Server Certificate - the actual server cert. These certs will defer by their alias and their DN. We will be using the keytool application to do all the heavy lifting. Please note that a real Certificate Authority will never use its own root CA to sign other certificates. it will always create intermediate certificates (that will form a chain) with which it will sign other certificates. Below you can see the commands I've used (which you can also find in the Dockerfile.ca file):

keytool -genkeypair -alias ca -dname "CN=RootCA, OU=RootCertificateAuthority, O=ZennoteCertificateAuthority, C=US" -ext KeyUsage=digitalSignature,keyCertSign -ext BasicConstraints=ca:true,PathLen:3 -keyalg RSA -keysize 2048 -keystore ca_root.pkcs12 -storepass changeit
keytool -genkeypair -alias server -dname "CN=backend.zennote.online, OU=Blog, O=Zennote, C=US" -ext KeyUsage=digitalSignature,keyEncipherment,keyAgreement -ext ExtendedKeyUsage=serverAuth,clientAuth -keyalg RSA -keysize 2048 -keystore simple-server.pkcs12 -storepass changeit

Pay close attention to the CN. We are going to use backend.zennote.online as our DNS name, we can achieve this by leveraging docker-compose (more about Docker in another post).

Now, we need to create a Certificate Signing Request (CSR) for the server certificate and sign it using the Root Certificate:

keytool -certreq -alias server -keyalg RSA -keystore simple-server.pkcs12 -storepass changeit | keytool -gencert -alias ca -keyalg RSA -keystore ca_root.pkcs12 -storepass changeit -ext KeyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement -ext ExtendedKeyUsage=serverAuth -rfc -outfile signed_server_cert.crt

Now, we will export the CA cert - we will use it later when we will import it for our client.

keytool -exportcert -rfc -alias ca -keystore ca_root.pkcs12 -storepass changeit -file zennote.crt.pem

But now we need to import it into our self-signed server cert together with the signed cert:

keytool -exportcert -alias ca -keystore ca_root.pkcs12 -storepass changeit -rfc | keytool -importcert -alias root -noprompt -trustcacerts -keystore simple-server.pkcs12 -storepass changeit
keytool -importcert -alias server -keystore simple-server.pkcs12 -storepass changeit -file signed_server_cert.crt

The only thing left for us to do is to delete the CA cert from the key store holding the signed server certificate:

keytool -delete -alias root -keystore simple-server.pkcs12 -storepass changeit


Using CURL with the Certificate

Brilliant, we now have our root CA public certificate. But how can we use it?

Let's first try and use our new certificate with curl. We have the CA cert so we can tell curl to use it.

We need to spawn our docker-compose up and connect to our curl container.

docker-compose up
docker-compose exec -it curl /bin/sh

We can now use the CA cert with curl:

curl --cacert /tmp/ca/zennote.crt.pem https://backend.zennote.online:443/hello

Before we do it, it'll be nice to see the traffic in Wireshark. Do the steps we did before to find the network interface of the curl container, open Wireshark, start monitoring the traffic of the container and run the curl command.

You'll see something like this:

Which is great because there are no errors. But we can't see the traffic... No worries, we can decrypt it. Let's see how?


Decrypting TLS Communication with Wireshark

We will do this by getting the Pre-Master and loading it to Wireshark. Let's begin.

First, on the machine that initiates the call, set the following environment variable:

export SSLKEYLOGFILE=/tmp/keyfiles/keyfile

Our docker-compose is configured to share the tmp folder of our host machine with the /tmp/keyfiles folder on the container so from the next curl run, it will store its calculated Pre-Master keys in that file.

Execute the curl command again and then go to the Wireshark preferences (ctrl + shift + p) then Protocols and then TLS. Enter the (Pre)-Master-Secret log filename location into the field and press OK. You can now decrypt the messages between the server and curl and it will look something like this:

We've successfully decrypted the traffic between curl and our server. We can now see the HTTP2 traffic flowing. All that is left for us is to create a trust store for our Java client and see how we can connect to our server from our client.


Creating a Trust Store for our Client

First, we will create a trust store:

keytool -importcert -trustcacerts -file /tmp/ca/zennote.crt.pem -alias zennote-ca -keystore /tmp/client-keystore.pkcs12 -storepass changeit

To use our newly created trust store with our REST client, we need to pass the location of the trust store and its password to it. We will be using two system-wide Java parameters for that:

  • javax.net.ssl.trustStore - will point to the location of the trust store

  • javax.net.ssl.trustStorePassword - will hold the password

All of this is already set up for you in our docker-compose configuration which you can find here: https://github.com/zennote-online/java-tls


Wrapping Up

We had quite a lot to discuss in this article. We've talked about key stores and trust stores, the keytool, how to debug our traffic with Wireshark, how to create certificates, and even sign them for development purposes.

Although it was a bit long, we still have quite a bit to cover - but we'll leave it to the next article.

If you haven't done so already, sign up for my newsletter to get notifications about new posts.

25 views0 comments

Recent Posts

See All
bottom of page