Exploring SSL/TLS - Part 3


Bhaskar S 10/28/2017


Overview

In Part-2, we explored a simple echo client and a server, by creating and using a self-signed SSL/TLS server certificate for secure communication via the default SSL/TLS setup using Java system properties.

In this article, we will explore how to perform all the necessary setup for SSL/TLS communication between the client and the server using the lower level JSSE APIs.

Terminology

In this section, we will list and briefly describe some of the terms referred to in this article.

Term Description
java.security.KeyStore a class that implements an in-memory representation of the keystore for storing the keys and the associated identify certificates for a subject (either the client or the server)
javax.net.ssl.SSLContext a core engine class that implements the SSL/TLS protocol and acts factory for creating secure sockets
javax.net.ssl.KeyManager a class that uses the KeyStore to determine which private key entry (and the corresponding certificate) to use for sending to the remote peer for authentication based on the chosen algorithm
javax.net.ssl.KeyManagerFactory a factory class for creating an instance of KeyManager
javax.net.ssl.TrustManager a class that uses the KeyStore to determine which public key entry (and the corresponding certificate) to use for validating the peer identity certificate
javax.net.ssl.TrustManagerFactory a factory class for creating an instance of TrustManager

Hands-on SSL/TLS using Java - Part 3

We will leverage the server keystore server.ks and the client truststore client.ts we created in Part-2 to demonstrate the use of SSL/TLS for secure communication between the client and the server using the lower level JSSE APIs.

The following is the simple SSL enabled echo server using the lower level JSSE APIs:

SecureEchoServer3.java
/*
 *
 *  Name:        SecureEchoServer3
 *  Description: Echo server that loads a KeyStore and uses SSLContext
 *  
 */

package com.polarsparc.pki;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;

import java.security.KeyStore;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;

public class SecureEchoServer3 {
    private static final int _SSL_PORT = 8443;
    private static final String _PROTOCOL = "TLSv1.2";
    
    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.printf("Usage: java com.polarsparc.pki.SecureEchoServer3 <keystore> <password>\n");
            System.exit(1);
        }
        
        try {
            String kst = KeyStore.getDefaultType();
            
            System.out.printf("Echo (server-3), default key store type: %s\n", kst);
            
            KeyStore ks = KeyStore.getInstance(kst);
            try (InputStream fs = new FileInputStream(args[0])) {
                ks.load(fs, args[1].toCharArray());
            }
            catch (Exception ioEx) {
                throw ioEx;
            }
            
            String ksa = KeyManagerFactory.getDefaultAlgorithm();
            
            System.out.printf("Echo (server-3), default key manager algorithm: %s\n", ksa);
            
            KeyManagerFactory ksf = KeyManagerFactory.getInstance(ksa);
            ksf.init(ks, args[1].toCharArray());
            
            KeyManager[] km = ksf.getKeyManagers();
            
            SSLContext context = SSLContext.getInstance(_PROTOCOL);
            context.init(km, null, null);
            
            SSLServerSocketFactory factory = context.getServerSocketFactory();
            
            SSLServerSocket server = (SSLServerSocket) factory.createServerSocket(_SSL_PORT);
            
            System.out.printf("Echo (server-3) started on %d\n", _SSL_PORT);
            
            for (;;) {
                try (SSLSocket client = (SSLSocket) server.accept()) {
                    try (BufferedReader input = new BufferedReader(new InputStreamReader(client.getInputStream()))) {
                        String line = null;
                        while ((line = input.readLine()) != null) {
                            System.out.printf("-> Echo (server-3): %s\n", line);
                            System.out.flush();
                        }
                    }
                    catch (Exception inputEx) {
                        inputEx.printStackTrace();
                    }
                }
                catch (Exception sockEx) {
                    sockEx.printStackTrace();
                }
            }
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

Let us explain and understand some of the methods used in the SecureEchoServer3 code shown above.

The KeyStore.getDefaultType() static method returns the default type of the KeyStore. In the standard Java implementation, it is of type JKS, which is an acronym for Java KeyStore.

The KeyStore.getInstance(String) static method returns an instance of a KeyStore of the specified type.

The load(InputStream, char[]) instance method initializes the KeyStore with keys and certificates from the specified keystore file as an input stream. The second argument is the character array of the password to unlock the keystore file.

The KeyManagerFactory.getDefaultAlgorithm() static method returns the name of the default algorithm used. In the standard Java implementation, it is SunX509.

The KeyManagerFactory.getInstance(String) static method returns an instance of a KeyManagerFactory that implements the specified algorithm.

The init(KeyStore, char[]) instance method initializes the KeyManagerFactory with the specified instance of the KeyStore. The second argument is the character array of the password to unlock a key entries from the KeyStore instance.

The getKeyManagers() instance method returns an array of KeyManager instances, one for each type of key entry in the KeyStore.

The SSLContext.getInstance(String) static method returns an instance of an SSLContext that implements the specified protocol. We have chosen TLSv1.2 as it is the most current and secure version.

The init(KeyManager[], TrustManager[], null) instance method initializes the instance of the SSLContext with the specified array of KeyManager and TrustManager. In our example, we pass in a null for the array of TrustManager since the server does not need a truststore.

Open a new Terminal window, and execute the following command to start the SSL/TLS echo server with the appropriate keystore:

java -cp build/classes com.polarsparc.pki.SecureEchoServer3 ./resources/server.ks server.123

The following should be the typical output:

Output.1

Echo (server-3), default key store type: jks
Echo (server-3), default key manager algorithm: SunX509
Echo (server-3) started on 8443

The following is the simple SSL enabled echo client using the lower level JSSE APIs:

SecureEchoClient3.java
/*
 *
 *  Name:        SecureEchoClient3
 *  Description: Echo client that loads a TrustStore and uses SSLContext
 *  
 */

package com.polarsparc.pki;

import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStreamWriter;

import java.security.KeyStore;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

public class SecureEchoClient3 {
    private static final int _SSL_PORT = 8443;
    private static final String _PROTOCOL = "TLSv1.2";
    private static final String _SSL_HOST = "localhost";
    
    public static void main(String[] args) {
        if (args.length != 3) {
            System.out.printf("Usage: java com.polarsparc.pki.SecureEchoClient3 <truststore> <password> <message>\n");
            System.exit(1);
        }
        
        try {
            String kst = KeyStore.getDefaultType();
            
            System.out.printf("Echo (client-3), default key store type: %s\n", kst);
            
            KeyStore ks = KeyStore.getInstance(kst);
            try (InputStream fs = new FileInputStream(args[0])) {
                ks.load(fs, args[1].toCharArray());
            }
            catch (Exception ioEx) {
                throw ioEx;
            }
            
            String tsa = TrustManagerFactory.getDefaultAlgorithm();
            
            System.out.printf("Echo (client-3), default trust manager algorithm: %s\n", tsa);
            
            TrustManagerFactory tsf = TrustManagerFactory.getInstance(tsa);
            tsf.init(ks);
            
            TrustManager[] tm = tsf.getTrustManagers();
            
            SSLContext context = SSLContext.getInstance(_PROTOCOL);
            context.init(null, tm, null);
            
            SSLSocketFactory factory = context.getSocketFactory();
            
            SSLSocket socket = (SSLSocket) factory.createSocket(_SSL_HOST, _SSL_PORT);
            
            BufferedWriter output = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            
            output.write(args[2]+"\n");
            output.flush();
            
            socket.close();
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

Let us explain and understand some of the methods used in the SecureEchoClient3 code shown above.

The TrustManagerFactory.getDefaultAlgorithm() static method returns the name of the default algorithm used. In the standard Java implementation, it is PKIX.

The TrustManagerFactory.getInstance(String) static method returns an instance of a TrustManagerFactory that implements the specified algorithm.

The init(KeyStore, char[]) instance method initializes the TrustManagerFactory with the specified instance of the KeyStore. The second argument is the character array of the password to unlock a key entries from the KeyStore instance.

The getTrustManagers() instance method returns an array of TrustManager instances, one for each type of key entry in the KeyStore.

The SSLContext.getInstance(String) static method returns an instance of an SSLContext that implements the specified protocol. We have chosen TLSv1.2 as it is the most current and secure version.

The init(KeyManager[], TrustManager[], null) instance method initializes the instance of the SSLContext with the specified array of KeyManager and TrustManager. In our example, we pass in a null for the array of KeyManager since the client does not need a keystore.

Open another Terminal window, and execute the following command to start the SSL/TLS echo client:

java -cp build/classes com.polarsparc.pki.SecureEchoClient3 ./resources/client.ts client.123 "Hello SSL World"

The following should be the typical output:

Output.2

Echo (client-3), default key store type: jks
Echo (client-3), default trust manager algorithm: PKIX

From the terminal window where the server was started, we see the following output:

Output.3

-> Echo (server-3): Hello SSL World

Mutual Authentication

The following are the basic steps involved in the two-way SSL/TLS handshake:

The following diagram illustrates the above steps in a pictorial form:

Two-Way SSL
Two-Way SSL/TLS

Setup

In this section, we will create a digital certificate for the client that will be verified by the server to demonstrate the two-way SSL/TLS mutual authentication.

The private key and the identify certificate for the client will be stored in a keystore file called client.ks that will be protected with a keystore password. The certificate will be valid for 365 days.

To create the client certificate using the keytool, execute the following command:

keytool -genkeypair -alias client -keystore ./resources/client.ks -keyalg rsa -keysize 2048 -validity 365

The following should be the typical output:

Output.4

Enter keystore password: client.123
Re-enter new password: client.123
What is your first and last name?
  [Unknown]:  client
What is the name of your organizational unit?
  [Unknown]:  testing
What is the name of your organization?
  [Unknown]:  polarsparc
What is the name of your City or Locality?
  [Unknown]:  na
What is the name of your State or Province?
  [Unknown]:  ny
What is the two-letter country code for this unit?
  [Unknown]:  us
Is CN=client, OU=testing, O=polarsparc, L=na, ST=ny, C=us correct?
  [no]:  yes

To self-sign the client certificate from the keystore client.ks using the keytool, execute the following command:

keytool -selfcert -alias client -keystore ./resources/client.ks -validity 365

The following should be the typical output:

Output.5

Enter keystore password: client.123

For the server to validate a client certificate, it needs a truststore with the public key and the CA certificate. Since we self-signed the server certificate, we need to extract the public key and the CA certificate to a truststore file called server.ts. It is a two-step process - first export the CA certificate to a file and then import the CA certificate from the file into the truststore.

To export the CA certificate from the keystore client.ks into a file called client.cer in the rfc 1421 format using the keytool, execute the following command:

keytool -exportcert -alias client -keystore ./resources/client.ks -rfc -file ./resources/client.cer

The following should be the typical output:

Output.6

Enter keystore password: client.123 
Certificate stored in file <./resources/client.cer>

To import the CA certificate from the file client.cer in the rfc 1421 format into the truststore server.ts using the keytool, execute the following command:

keytool -importcert -alias client -file ./resources/client.cer -keystore ./resources/server.ts

The following should be the typical output:

Output.7

Enter keystore password: server.123
Re-enter new password: server.123
Owner: CN=client, OU=testing, O=polarsparc, L=na, ST=ny, C=us
Issuer: CN=client, OU=testing, O=polarsparc, L=na, ST=ny, C=us
Serial number: 3d5f9f96
Valid from: Fri Oct 27 23:05:11 EDT 2017 until: Sat Oct 27 23:05:11 EDT 2018
Certificate fingerprints:
   SHA1: DF:ED:5A:19:8B:B2:53:31:80:D7:B1:95:8A:CB:A0:8D:56:31:03:F0
   SHA256: 05:8F:0F:11:5F:7E:F3:72:23:E4:3A:C6:58:9C:81:B7:B0:4F:1E:6F:A9:C8:B6:30:09:23:A8:8B:92:86:11:92
Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 2048-bit RSA key
Version: 3

Extensions: 

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 62 4E EE 82 9C B0 AE 02   54 E3 3D AE 32 6B 09 4B  bN......T.=.2k.K
0010: C6 8C 08 FE                                        ....
]
]

Trust this certificate? [no]:  yes
Certificate was added to keystore

At this point, we should have two keystore files - one for the server (server.ks from Part 2) and one for the client (client.ks from above). Similarly, we should have two truststore files - one for the client (client.ts from Part 2) and one for the server (server.ts from above).

Hands-on Two-way SSL/TLS using Java

The following is the simple SSL enabled echo server that uses the lower level JSSE APIs for the two-way SSL/TLS mutual authentication:

SecureEchoServer4.java
/*
 *
 *  Name:        SecureEchoServer4
 *  Description: Echo server that loads both the KeyStore and the TrustStore and uses SSLContext
 *  
 */

package com.polarsparc.pki;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;

import java.security.KeyStore;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

public class SecureEchoServer4 {
    private static final int _SSL_PORT = 8443;
    private static final String _PROTOCOL = "TLSv1.2";
    
    public static void main(String[] args) {
        if (args.length != 4) {
            System.out.printf("Usage: java com.polarsparc.pki.SecureEchoServer4 <keystore> <ks-secret> <truststore> <ts-secret>\n");
            System.exit(1);
        }
        
        try {
            String kst = KeyStore.getDefaultType();
            
            System.out.printf("Echo (server-4), default key store type: %s\n", kst);
            
            KeyStore ks = KeyStore.getInstance(kst);
            try (InputStream fs = new FileInputStream(args[0])) {
                ks.load(fs, args[1].toCharArray());
            }
            catch (Exception ioEx) {
                throw ioEx;
            }
            
            String ksa = KeyManagerFactory.getDefaultAlgorithm();
            
            System.out.printf("Echo (server-4), default key manager algorithm: %s\n", ksa);
            
            KeyManagerFactory ksf = KeyManagerFactory.getInstance(ksa);
            ksf.init(ks, args[1].toCharArray());
            
            KeyStore ts = KeyStore.getInstance(kst);
            try (InputStream fs = new FileInputStream(args[2])) {
                ts.load(fs, args[3].toCharArray());
            }
            catch (Exception ioEx) {
                throw ioEx;
            }
            
            String tsa = TrustManagerFactory.getDefaultAlgorithm();
            
            System.out.printf("Echo (server-4), default trust manager algorithm: %s\n", tsa);
            
            TrustManagerFactory tsf = TrustManagerFactory.getInstance(tsa);
            tsf.init(ts);
            
            KeyManager[] km = ksf.getKeyManagers();
            
            TrustManager[] tm = tsf.getTrustManagers();
            
            SSLContext context = SSLContext.getInstance(_PROTOCOL);
            context.init(km, tm, null);
            
            SSLServerSocketFactory factory = context.getServerSocketFactory();
            
            SSLServerSocket server = (SSLServerSocket) factory.createServerSocket(_SSL_PORT);
            server.setNeedClientAuth(true);
            
            System.out.printf("Echo (server-4) started on %d\n", _SSL_PORT);
            
            for (;;) {
                try (SSLSocket client = (SSLSocket) server.accept()) {
                    try (BufferedReader input = new BufferedReader(new InputStreamReader(client.getInputStream()))) {
                        String line = null;
                        while ((line = input.readLine()) != null) {
                            System.out.printf("-> Echo (server-4): %s\n", line);
                            System.out.flush();
                        }
                    }
                    catch (Exception inputEx) {
                        inputEx.printStackTrace();
                    }
                }
                catch (Exception sockEx) {
                    sockEx.printStackTrace();
                }
            }
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

The setNeedClientAuth(true) instance method on the SSLServerSocket is what enables the SSL/TLS client authentication.

For the two-way SSL/TLS mutual authentication, the SSL/TLS server needs both a keystore to store its private keys and identify certificate as well as a truststore to store the trusted root CA certificate to validate the SSL/TLS client identity certificate.

The init(KeyManager[], TrustManager[], null) instance method initializes the instance of the SSLContext with the specified array of KeyManager and TrustManager. In this example, we specify valid instances for both the array of KeyManager and TrustManager.

Open a new Terminal window, and execute the following command to start the SSL/TLS echo server with the appropriate keystore and truststore:

java -cp build/classes com.polarsparc.pki.SecureEchoServer4 ./resources/server.ks server.123 \

     ./resources/server.ts server.123

The following should be the typical output:

Output.8

Echo (server-4), default key store type: jks
Echo (server-4), default key manager algorithm: SunX509
Echo (server-4), default trust manager algorithm: PKIX
Echo (server-4) started on 8443

The following is the simple SSL enabled echo client that uses the lower level JSSE APIs for the two-way SSL/TLS mutual authentication:

SecureEchoClient4.java
/*
 *
 *  Name:        SecureEchoClient4
 *  Description: Echo client that loads both the KeyStore and the TrustStore and uses SSLContext
 *  
 */

package com.polarsparc.pki;

import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStreamWriter;

import java.security.KeyStore;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

public class SecureEchoClient4 {
    private static final int _SSL_PORT = 8443;
    private static final String _PROTOCOL = "TLSv1.2";
    private static final String _SSL_HOST = "localhost";
    
    public static void main(String[] args) {
        if (args.length != 5) {
            System.out.printf("Usage: java com.polarsparc.pki.SecureEchoClient4 <keystore> <ks-secret> <truststore> <ts-secret> <message>\n");
            System.exit(1);
        }
        
        try {
            String kst = KeyStore.getDefaultType();
            
            System.out.printf("Echo (client-4), default key store type: %s\n", kst);
            
            KeyStore ks = KeyStore.getInstance(kst);
            try (InputStream fs = new FileInputStream(args[0])) {
                ks.load(fs, args[1].toCharArray());
            }
            catch (Exception ioEx) {
                throw ioEx;
            }
            
            String ksa = KeyManagerFactory.getDefaultAlgorithm();
            
            System.out.printf("Echo (client-4), default key manager algorithm: %s\n", ksa);
            
            KeyManagerFactory ksf = KeyManagerFactory.getInstance(ksa);
            ksf.init(ks, args[1].toCharArray());
            
            KeyStore ts = KeyStore.getInstance(kst);
            try (InputStream fs = new FileInputStream(args[2])) {
                ts.load(fs, args[3].toCharArray());
            }
            catch (Exception ioEx) {
                throw ioEx;
            }
            
            String tsa = TrustManagerFactory.getDefaultAlgorithm();
            
            System.out.printf("Echo (client-4), default trust manager algorithm: %s\n", tsa);
            
            TrustManagerFactory tsf = TrustManagerFactory.getInstance(tsa);
            tsf.init(ts);
            
            KeyManager[] km = ksf.getKeyManagers();
            
            TrustManager[] tm = tsf.getTrustManagers();
            
            SSLContext context = SSLContext.getInstance(_PROTOCOL);
            context.init(km, tm, null);
            
            SSLSocketFactory factory = context.getSocketFactory();
            
            SSLSocket socket = (SSLSocket) factory.createSocket(_SSL_HOST, _SSL_PORT);
            
            BufferedWriter output = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            
            output.write(args[4]+"\n");
            output.flush();
            
            socket.close();
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

For the two-way SSL/TLS mutual authentication, the SSL/TLS client needs both a keystore to store its private keys and identify certificate as well as a truststore to store the trusted root CA certificate to validate the SSL/TLS server identity certificate.

The init(KeyManager[], TrustManager[], null) instance method initializes the instance of the SSLContext with the specified array of KeyManager and TrustManager. In this example, we specify valid instances for both the array of KeyManager and TrustManager.

Open another Terminal window, and execute the following command to start the SSL/TLS echo client:

java -cp build/classes com.polarsparc.pki.SecureEchoClient4 ./resources/client.ks client.123 \

     ./resources/client.ts client.123 "Hello SSL World"

The following should be the typical output:

Output.9

Echo (client-4), default key store type: jks
Echo (client-4), default key manager algorithm: SunX509
Echo (client-4), default trust manager algorithm: PKIX

From the terminal window where the server was started, we see the following output:

Output.10

-> Echo (server-4): Hello SSL World

YIPPEE !!! We have successfully demonstrated the two-way SSL/TLS authetication.

References

Exploring SSL/TLS - Part 1

Exploring SSL/TLS - Part 2