1. Overview

HTTPS is an extension of HTTP that allows secure communications between two entities in a computer network. HTTPS uses the TLS (Transport Layer Security) protocol to achieve secure connections.

TLS can be implemented with one-way or two-way certificate verification. In the one-way, the server shares its public certificate so the client can verify that it’s a trusted server. The alternative is two-way verification. Both the client and the server share their public certificates to verify each other’s identity.

This article will focus on two-way certificate verification, where the server will also check the client’s certificate.

2. Java and TLS Versions

TLS 1.3 is the latest version of the protocol. This version is more performant and secure. It has a more efficient handshake protocol and uses modern cryptographic algorithms.

Java started supporting this version of the protocol in Java 11. We will use this version to generate certificates and implement a simple client-server pair that uses TLS to authenticate each other.

3. Generating Certificates in Java

Since we’re doing a two-way TLS authentication, we’ll need to generate certificates for the client and the server.

In a production environment, it’s recommended to purchase the certificates from a Certificate Authority. However, for testing or demo purposes, it’s good enough to use self-signed certificates. For this article, we’re going to use Java’s keytool to generate the self-signed certificates.

3.1. Server Certificate

First, we generate the server key store:

keytool -genkey -alias serverkey -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore serverkeystore.p12 -storepass password -ext san=ip:,dns:localhost

We use the keytool -ext option to set the Subject Alternative Names (SAN) to define the local hostname/IP address that identifies the server. In general, we can specify multiple addresses with this option. However, the clients will be constrained to use one of these addresses to connect to the server.

Next, we export the certificate to the file server-certificate.pem:

keytool -exportcert -keystore serverkeystore.p12 -alias serverkey -storepass password -rfc -file server-certificate.pem

Finally, we add the server certificate to the client’s trust store:

keytool -import -trustcacerts -file server-certificate.pem -keypass password -storepass password -keystore clienttruststore.jks

3.2. Client Certificate

Similarly, we generate the client key store and export its certificate:

keytool -genkey -alias clientkey -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore clientkeystore.p12 -storepass password -ext san=ip:,dns:localhost

keytool -exportcert -keystore clientkeystore.p12 -alias clientkey -storepass password -rfc -file client-certificate.pem

keytool -import -trustcacerts -file client-certificate.pem -keypass password -storepass password -keystore servertruststore.jks

In the last command, we added the client’s certificate to the server trust store.

4. Server Java Implementation

Using java sockets the server implementation is trivial. The SSLSocketEchoServer class gets a SSLServerSocket to easily support TLS authentication. We just need to specify  the cipher and protocols and the rest is just a standard echo server that replies the same messages that are sent by the client:

public class SSLSocketEchoServer {

    static void startServer(int port) throws IOException {

        ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
        try (SSLServerSocket listener = (SSLServerSocket) factory.createServerSocket(port)) {
            listener.setEnabledCipherSuites(new String[] { "TLS_AES_128_GCM_SHA256" });
            listener.setEnabledProtocols(new String[] { "TLSv1.3" });
            System.out.println("listening for messages...");
            try (Socket socket = listener.accept()) {
                InputStream is = new BufferedInputStream(socket.getInputStream());
                byte[] data = new byte[2048];
                int len = is.read(data);
                String message = new String(data, 0, len);
                OutputStream os = new BufferedOutputStream(socket.getOutputStream());
                System.out.printf("server received %d bytes: %s%n", len, message);
                String response = message + " processed by server";
                os.write(response.getBytes(), 0, response.getBytes().length);

The server listens for client connections. The invocation of the listener.setNeedClientAuth(true) requires the client to share its certificate with the server. In the background, the SSLServerSocket implementation authenticates the client using the TLS protocol.

In our case, the self-signed client certificate is in the server trust store so that the socket will accept the connection. The server proceeds to read the message using the InputStream. It then uses the OuputStream to echo back the incoming message appending an acknowledgment.

5. Client Java Implementation

In the same fashion as we did with the server, the client implementation is a simple SSLScocketClient class:

public class SSLScocketClient {

    static void startClient(String host, int port) throws IOException {

        SocketFactory factory = SSLSocketFactory.getDefault();
        try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
            socket.setEnabledCipherSuites(new String[] { "TLS_AES_128_GCM_SHA256" });
            socket.setEnabledProtocols(new String[] { "TLSv1.3" });
            String message = "Hello World Message";
            System.out.println("sending message: " + message);
            OutputStream os = new BufferedOutputStream(socket.getOutputStream());
            InputStream is = new BufferedInputStream(socket.getInputStream());
            byte[] data = new byte[2048];
            int len = is.read(data);
            System.out.printf("client received %d bytes: %s%n", len, new String(data, 0, len));

First, we create an SSLSocket that establishes a connection with the server. In the background, the socket will set up the TLS connection establishment handshake. As part of this handshake, the client will verify the server’s certificate and check that it’s in the client truststore.

Once the connection has been successfully established, the client sends a message to the server using the output stream. It then reads the server’s response with the input stream.

6. Running the Applications

To run the server, open a command window and run:

java -Djavax.net.ssl.keyStore=/path/to/serverkeystore.p12 \ 
  -Djavax.net.ssl.keyStorePassword=password \
  -Djavax.net.ssl.trustStore=/path/to/servertruststore.jks \ 
  -Djavax.net.ssl.trustStorePassword=password \

We specify the system properties for javax.net.ssl.keystore and javax.net.ssl.trustStore to point to the serverkeystore.p12 and servertruststore.jks files that we created before with keytool.

To run the client, we open another command window and run:

java -Djavax.net.ssl.keyStore=/path/to/clientkeystore.p12 \ 
  -Djavax.net.ssl.keyStorePassword=password \ 
  -Djavax.net.ssl.trustStore=/path/to/clienttruststore.jks \ 
  -Djavax.net.ssl.trustStorePassword=password \ 

Similarly, we set the javax.net.ssl.keyStore and javax.net.ssl.trustStore system properties to point to the clientkeystore.p12 and clienttruststore.jks files that we generated before with keytool.

7. Conclusion

We’ve written a simple client-server Java implementation that uses server and client certificates to do a bidirectional TLS authentication.

We used keytool to generate the self-signed certificates.

The source code of the examples can be found over on GitHub.

Course – LSS (cat=Security/Spring Security)
announcement - icon

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security:


res – Security (video) (cat=Security/Spring Security)
Inline Feedbacks
View all comments