Part 2: Custom client-server Java application that communicates over a Mutually Authenticated SSL (MASSL) Connection

By 01

Thursday, September 11, 2008

Introduction

In Part 1 of this series, we explored how to build Truststores and Keystores for use with a client-server Java application that will communicate over a MASSL connection.  In Part 2, a simple, client-server Java application will be presented, which uses the certificates and CA produced in Part 1.

The application will implement a very simple "counting" protocol that is simplistic, but complex enough to provide a non-trivial example.  Ideally, the reader has some experience with the Java network APIs prior to reading this article.  If not, I'd recommend Sun's client-server networking tutorial.  The code for both a client and server program will be introduced; scripts to run each program and an environment script will also be presented.  It is assumed that the following files from Part 1 of this series are present:  server-keystore.jks, server-truststore.jks, client-keystore.jks, and client-truststore.jks.  If you do not have these files located in a working directory, you will need to create them following the instructions presented in Part 1.  Or, all prequisite pieces will be available for download at the bottom of this article; however, it is recommended that you run through both tutorials to understand how it all works.  For those who do not have that kind of time,  scroll to the bottom of this page and follow the instructions in the last section.

In this article, I will refer to the server program (Server.class) as "the server".  Likewise, I will refer to the client program (Client.class) as the "the client".  Also, I am assuming that the client and the server are running on the same machine--this makes everything simple.  The same steps can be easily extended to work across a network (assuming connectivity and firewall configuration).

This example was created with Sun Java 1.5.0_16 on a Fedora Core 9 Linux system.

SSL & Java:

Links to the SSL & TLS protocol specs were given in Part 1, but are repeated here for completeness.  A future article will discuss these protocols in more details.  For our purposes, it is enough to understand that these protocols provide transport-layer encryption and authentication of clients and servers through trust chains established through CA certificates.  There are several implementations of the SSL specification (API & protocol) in common use today.  Each Java vendor is required by the Java spec to provide SSL to their implementation of the Java Runtime Environment (JRE) as a part of Java Secure Socket Extention (JSSE).  JSSE is one piece of the Java security offering.

Network coding in Java, in general, is greatly simplified over the equivalent in C/C++.  In a similar fashion, using the SSL API in Java is fairly straightforward, especially when compared to doing the same in C/C++.

In the absence of any special configuration, the default socket factories (SSLServerSocketFactory and SSLSocketFactory ) will return objects that use the default keystore (and truststore) for the JDK located at $JAVA_HOME/jre/lib/security/cacerts.  This is not what we want; to modify this behavior, you have two options: specify a custom keystore and truststore file on the command-line (what we will be doing) or use a KeyManager and TrustManager to define the custom files.  There are details to this, but that is beyond the scope of this article; perhaps, a more elaborate example will be explored in a future article.  Java also assumes that when a keystore is specified, there is only one personal certificate present in a keystore that will be used--this is how our keystores work, the client and server keystores each have a single private certificate present.  If multiple certificates will be present in a keystore, more work has to be done--again, beyond the scope of what this tutorial is attempting to accomplish.  The important lesson is if you want to keep things straightforward, specify a custom truststore and keystore on the command line.  If you are using MASSL, it is always a good idea to keep the minimum number of CA certificates in the truststore possible.

If you've skipped ahead, you will probably notice that the keystore and truststore passwords are being passed in on the command-line as well.  From a security perspective, this is not exactly ideal.  In fact, securely storing passwords becomes a complex and very expensive proposition--another topic that is beyond the scope of this article.  So, what this example is doing isn't the recommend approach for a production environment where security is important, but it gets the job done for learning purposes.  Note, our approach here is probably being used in plenty of production environments around the world.

The Count Protocol

I created a simple protocol the client and server can use to coordinate an initial handshake, decide how high to count, and begin counting together(ie, a client sends in a number and the server echos it back to the client).  Admitedly, not terrible sophisticated, but I wanted a little more than an echo service.

Following successful completion of an SSL handshake, the client sends the string "<<<<<".  If the server receives this string, it returns ">>>>>".  The server now considers the protocol handshake to be complete.  If the client receives ">>>>>", it considers the protocol handshake to be complete.

Next, the client sends an integer to the server--this is the number the client and server will count too.  The server will return either "ACCEPT" or "DENIED" depending upon whether the number is an integer greater than zero.  If the client receives "DENIED", it must abort.  If the client receives "ACCEPT", it will begin sending numbers and wait for a reply from the server for each number.  The client cannot send another number until the previous number's echo is received from the client.  The reply must be the same integer that was just sent.  The server must also check each integer the client sends to ensure that it is the expected number.

When the maximum integer is received, both the client and server should gracefully close their socket connections.

The Client:

The client program is implemented in a single class: Client.  The source code for the Client class can be found here.  The Client class will establish a MASSL connection to the server program, run through its implementation of the Count Protocol, close the connection, and exit.

A shell script can be used to make running the Client class easier:

#!/bin/bash
. ./env.linux
export CLASSPATH=.
java -Djavax.net.debug=all \
  -Djavax.net.ssl.trustStore=client-truststore.jks\
  -Djavax.net.ssl.trustStorePassword=changeit \
  -Djavax.net.ssl.keyStore=client-keystore.jks \
  -Djavax.net.ssl.keyStorePassword=changeit \
Client $* > client.out

This shell script is present in the download as the clientRun file.  It sets the environment needed by the Java command (using an environment setup script called env.linux); we'll look at the contents of env.linux in a moment.  In the meantime, look at the command-line arguments given to the java command in clientRun.  Notice, that a custom Truststore & Keystore are specified for use in this JVM--these are the client certificate databases that were created in Part One of this series.  A custom Truststore can be specified on the command line via: "-Djavax.net.ssl.trustStore".  A custom Keystore can be specified on the command line via: "-Djavax.net.ssl.keyStore".  The easiest way to specify a password for each of these certificate databases is to use "-Djavax.net.ssl.keyStore.Password" and "-Djavax.net.ssl.trustStore.Password" to set a password for the Keystore and Truststore, respectively. The "-Djavax.net.debug=all" will print debug information regarding network traffic.  In particular, it prints out debug information about the SSL handshake protocol, all encrypted packets read, all encrypted packets written, and the corresponding unencrypted data.  This is a standard debug flag; I know it will work in either a Sun or IBM Java 1.5 JVM without any additional effort.  When dealing with a production J2EE container where disabling SSL is not an option, this can be invaluable.  Unfortunately, a restart of the java process is required to enable it.  All output is redirected to a file called "client.out".  All debug output and program output will be sent to this file;  any exceptions that are generated will be written to the screen (unless standard error is redirected).

The clientRun program is used as follows:

                   ./clientRun host port N


This tells the Client class to establish a MASSL connection to a server running at host:port and count to N.

So, without further ado, let's look at the source code for Client.  In an effort to be brief, I'm not reviewing the source code line-by-line; instead, I'm only hitting the pieces that are important.  See the source code for the full details. 

The main method creates an instance of the Client class and calls its start() method.  This constructor takes three arguments: a hostname string, a port integer, and a count integer.  Then, the Client.connect() method is called.  This initiates the connection to the server.

   Client client = new Client(args[0],Integer.parseInt(args[1]),Integer.parseInt(args[2]));
   client.connect();


The connect() method creates the default SSLSocketFactory object using the SSLSocketFactory.getDefault() static method.  This SSLSocketFactory is used to create an SSLSocket object that can be used to establish a connection to the server located at the hostname:port combination given at the command line. 

In order read and write data to the new socket object, an InputStream and OutputStream must be created.  This is done with the SSLSocket.getInputStream() & SSLSocket.getOutputStream() methods, respectively.  The InputStream & OutputStream objects can then be used to create an InputStreamReader & OutputStreamWriter, respectively.  These objects, in turn, are given to BufferedReader & BufferedWriter objects, respectively--it is the BufferedReader & BufferedWriter that the Client class directly uses to communicate with the server.  Details of the java.io.* package are beyond the scope of this article.  However, it should be noted that the same I/O classes would be used to communicate over an unencrypted socket.

Nothing special was done in this client to ensure that the SSL connection uses Mutual Authentication.  That detail is determined on the server side of the SSL handshake protocol; thus, you will find API calls to set this option in the server program.

Now, the handleCommunication() method is called to run through the Count Protocol's client-side.  We will get to the details of this method in a moment.  To complete the connect() method, once all communication is finished, all of the I/O objects mentioned previously and the socket Object need to be closed.  Resource management of this nature is extremely important.  It wouldn't take long in a busy production environment for a process to run out of file descriptors or a similar system resource limitation to be reached.

        socket = socketFactory.createSocket(ip_,port_);
        in = socket.getInputStream();
        isr = new InputStreamReader(in);
        br = new BufferedReader(isr);
        out = socket.getOutputStream();
        osw  = new OutputStreamWriter(out);
        bw = new BufferedWriter(osw);
        handleCommunication(br,bw);
        br.close();
        isr.close();
        in.close();
        bw.close();
        osw.close();
        out.close();
        socket.close();


This brings us to the handleCommunication() method.  This method takes two arguments: a BufferedReader and a BufferedWriter.  These objects can be used to read and write any string to the SSLSocket object (and on to the server).  The handleCommunication() method implements the Count Protocol that was described earlier in this article.  It will be left as an exercise to the reader to work through how the handleCommunication() method works.

The JVM will find the single personal certificate (client-cert) that is defined in the client Keystore.  The client will present this to the server when asked for a client certificate during the SSL handshake.  The client Keystore will be searched for CA certificates when the JVM attempts to establish a trust chain for the server certificate.  In our example, this should be successful.

The only real difference between a client that uses a clear-text socket and an SSL socket is the socket factory class that is used.  There is also the Keystore and Truststore configuration, but that is outside of the Java code.  It should be noted that SSLSocketFactory configuration can become much more complex, especially if you begin storing multiple personal certificates inside the same Truststore, using a different certificate database format, or managing multiple certificate databases in Java code.

The Server:

The server program is implemented in a Java class, appropriately, called: Server.  This program has more details than the client.  It will listen on a port provided by the user at startup time and wait for incoming connections.  Client programs must connect using Mutually Authenticated SSL; so, clients will be required to pass in a valid certificate in order to establish an SSL connection with the Server.  Once a connection is established, the Count Protocol will be carried out, the client will disconnect, and the server program will go back to waiting for a new incoming connection.

This Server class spawns a thread that implements the server(i.e., a new Java thread is spawned that executes the Server.run() method).  Beyond this, the Server is single-threaded and can only handle one request at a time.  In order to provide concurrency, a seperate thread would need to process each request and a dedicated listener thread would need to be utilized.

The Server class can be run by calling the serverRun script:

#!/bin/bash
. ./env.linux
export CLASSPATH=.
java -Djavax.net.debug=all \
  -Djavax.net.ssl.trustStore=server-truststore.jks\
  -Djavax.net.ssl.trustStorePassword=changeit \
  -Djavax.net.ssl.keyStore=server-keystore.jks \
  -Djavax.net.ssl.keyStorePassword=changeit \
Server $* > server.out


This script is very similar to the clientRun script that was described in the last section.  The main differences are the Keystore and TrustStore that are assigned to the JVM.  Also, standard out is being redirected to a different file.  All debug output from the server program can be found in the file called server.out.

The serverRun script is called as follows:

./serverRun NNNNN

where NNNNN is an integer representing a valid port, which the java process can listen on.

When the Server program starts, the main() method instantiates a Server object, gives the constructor the port number, and calls Thread.start() .  This causes the JVM to create a new Java thread, which calls the Server class's run() method.

    Server ssls = new Server(Integer.parseInt(args[0]));
    ssls.start();

The run() method creates a default SSLServerSocketFactory object by calling SSLServerSocketFactory.getDefault(). 


ServerSocketFactory ssocketFactory = SSLServerSocketFactory.getDefault();


The default SSLServerSocketFactory can be used to create an SSLServerSocket object by calling SSLServerSocketFactory.createServerSocket().

ssocket = ssocketFactory.createServerSocket(port);

In order to ensure that a Mutually Authenticated SSL connection is used, the SSLServerSocket.setNeedClientAuth() method must be called with 'true' as an argument.


((SSLServerSocket)ssocket).setNeedClientAuth(true);


The server now enters a loop (from which it will never exit) that begins with a call to SSLServerSocket.accept().  This method waits until a MASSL handshake has completed successfully.  When a connection has been successfully established, accept() returns a Socket object that can be used to communicate with the client.  At this point, the Server code looks very similar to what we saw on the client-side.  A BufferedReader and a BufferedWriter objects are created using the same sequence of I/O objects that were described for the Client.

Now, the server calls its implementation of the handleCommunication() method.  This method is essentially the mirror opposite of the method found in the Client code.  This Server's handleCommunication() method are left as an exercise for the reader.

When handleCommunication() returns, the client has closed (or, is in the process of closing) its connections and the server should do the same by calling Socket.close().  Likewise, each I/O objects should have its close() method called.  This is done in the same manner as the Client.

All code discussed in the last few paragraphs occurs inside of a try-catch-finally block.  When discussing the client, we didn't cover the contents of the finally block.  But, notice that the finally block doesn't assume that the Socket object and I/O objects are valid; so, each object is checked to see if it is null before attempting to call close().  If an exception occured in the try-block, it is possible some subset of the involved objects could be null.

    } finally {
      try {
        if(br != null) br.close();
        if(isr != null) isr.close();
        if(in != null) in.close();
        if(bw != null) bw.close();
        if(osw != null) osw.close();
        if(out != null) out.close();
        if(socketToClient != null) socketToClient.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }


Regardless of whether or not the current connection successfully completes the Count Protocol, the server will go back to waiting for a new connection.  In this regards, the server is stuck in an infinite loop; there is no graceful way to shutdown the server.  So, ^C or kill is needed to stop it.


Hopefully, that was relatively painless. 
In a future article, I will dissect the SSL handshake protocol and step through the SSL debug output line-by-line.


Environment Script:

The clientRun and serverRun scripts reference a common environment script called: env.linux.  Again, this was all tested on my Linux box.  The contents of this script are:


JAVA_HOME=/usr/java/jdk1.5.0_16
PATH=$JAVA_HOME/bin:$PATH
CLASSPATH=.
export JAVA_HOME PATH CLASSPATH


This sets up the Java environment on my box.  This script would need to be modified to accomodate your system.  Note, an assumption has been made that this is being run on a Unix system or a Windows box with Cygwin installed.


Quick Reference Instructions

  1. Download the sample Keystores and Truststores created in Part 1.
  2. Download the source code and start scripts created in Part 2.
  3. Extract the files from both of these tgz balls into the same directory.
  4. Install the Sun JDK (or your favorite vendor's) for your platform.
  5. Compile Server.java and Client.java.
  6. Modify JAVA_HOME in the file called env to point at your JDK.
  7. Run the server with the following command:

·       ./serverRun port

 

Where "port" is a port number that the container will listen for incoming connections.  You can use any available port on your system that your id has access too (generally above 1023).

  1. Run the client with the following command:

·       ./clientRun localhost port 100

Where "port" is the port number chosen for the server in step seven.  "localhost" can be any valid hostname where the server is reachable.  For this example, I'm assuming that the client and server are running on the same box.

  1. If everything functions as expected, within 5-10 seconds, the program should exit without issue.


If all was successful, all output from the server was sent to a file in the local directory called server.out; all output from the client was sent to a file called client.out.  You should see the SSL handshake proceed without incident.  Then, the client and server will count to 100 (or whatever the third argument to the clientRun command was) with the client sending the server a number, the server responding back to client with the same number, and both exiting at the end of the run.

Note, full SSL debugging is enabled in both programs with "-Djavax.net.debug=all" added to the java command-line args.  This makes it very easy to debug SSL protocol problems.  Output from the client and server programs is intermingled with the JVM debug output.  This debug parameter should work in either a Sun or IBM Java 1.5.0 JVM.


Downloads

Download source code and start scripts.  Download sample Keystores and Truststores.

References:


[1] http://java.sun.com/docs/books/tutorial/networking/sockets/clientServer.html
[2] http://java.sun.com/j2se/1.5.0/docs/api/
[3]http://www.freesoft.org/CIE/Topics/ssl-draft/3-SPEC.HTM
[4]http://en.wikipedia.org/wiki/Secure_Sockets_Layer
[5]http://www.ietf.org/html.charters/tls-charter.html
[6]http://en.wikipedia.org/wiki/Certificate_Authority
[7]http://java.sun.com/docs/books/jls/
[8]http://en.wikipedia.org/wiki/Java_Virtual_Machine
[9]http://java.sun.com/j2se/1.5.0/docs/guide/security/jsse/JSSERefGuide.html
[10]http://java.sun.com/j2se/1.5.0/docs/guide/security/
[11]http://java.sun.com/j2se/1.5.0/docs/api/javax/net/ssl/SSLServerSocketFactory.html
[12]http://java.sun.com/j2se/1.5.0/docs/api/javax/net/ssl/SSLSocketFactory.html

[13]http://java.sun.com/j2se/1.5.0/docs/api/javax/net/ssl/SSLSocket.html

[14]http://java.sun.com/j2se/1.5.0/docs/api/java/io/InputStream.html

[15]http://java.sun.com/j2se/1.5.0/docs/api/java/io/OutputStream.html

[16]http://java.sun.com/j2se/1.5.0/docs/api/java/io/InputStreamReader.html

[17]http://java.sun.com/j2se/1.5.0/docs/api/java/io/OutputStreamWriter.html

[18]http://java.sun.com/j2se/1.5.0/docs/api/java/io/BufferedReader.html

[19]http://java.sun.com/j2se/1.5.0/docs/api/java/io/BufferedWriter.html

[20]http://java.sun.com/j2se/1.5.0/docs/api/javax/net/ssl/SSLServerSocket.html

[21]http://java.sun.com/j2se/1.5.0/docs/api/javax/net/ssl/SSLServerSocketFactory.html

 

©2008 www.thinkmiddleware.com

All copyrights & trademarks belong to their respective owners.

The comments and opinions herein are that of the author.

Please direct all comments to 01.

While the information presented on this web site is believed to be correct, the author is not responsible for any damage, loss of data, or other issues that may arise from using the information posted here.

Made with CityDesk
Last Modified: Sunday, 09-Nov-2008 10:48:31 MST