The Java Secure Socket Extensions

The Java Secure Socket Extension package and Java Run-Time Environment provide most of the tools you need to implement SSL within Java applications.


February 01, 2001
URL:http://www.drdobbs.com/security/the-java-secure-socket-extensions/184404478

Feb01: The Java Secure Socket Extensions

Authenticating and encrypting connections

Kirby is Chief Product Architect for Apigent Solutions and a contributing author to The Quick Python Book (Manning, 1999). He can be reached at kirbyangell@ hotmail.com.


You sit at your computer, marveling at your distributed Java application. Your code creates Socket and ServerSocket objects like crazy, sending data across the Internet. It is a sight to behold — until you realize that anyone can intercept the data being read, masquerade as one of your applications, and fill your system with bogus data.

As soon as you start looking into authenticating and encrypting the connections between applications, you find that you have entered a complex area. When dealing with encryption, you have to worry about many things — not just which algorithm you plan to use. Attacks to your system can involve the algorithm, protocol, passwords, and other factors you might not even consider.

Luckily, most of the messy details of authenticating and encrypting traffic between two socket-based applications have been worked out in the Secure Sockets Layer (SSL) specification. Sun Microsystems has an implementation of SSL in its Java Secure Socket Extension (JSSE; http://java.sun.com/security/) package. JSSE and the Java Run-Time Environment (JRE) provide most of the tools necessary to implement SSL within your Java application if your Java app is a client communicating with HTTPS servers. Since the JSSE documentation and tools are mostly geared toward this end, it takes some work to figure out how to use the toolset within an application where you need to create both the client and server sides of the connection. Consequently, in this article, I'll create a Java client and server that use the JSSE package to securely authenticate and encrypt traffic between them. In truth, the code is actually the simplest thing you will have to do to get your connections encrypted. The devil is in the key management.

The sample application (available electronically; see "Resource Center," page 5) implements a client/server-based credit-card authorization system. The server accepts credit-card transactions from any number of remote clients. Each application can be configured to use a standard socket or to authorize and encrypt sessions with an SSL socket. The SSL analogs of Socket and ServerSocket are SSLSocket and SSLServerSocket. Once you get past the initial setup of the SSL versions, you can use them in place of the standard socket classes. Listing One shows the client application's (client.java) sendData function, which is responsible for interacting with the server once the connection has been made. There is nothing specific to SSL in the code — it simply reads and writes the socket. Although the applications authenticate and encrypt the stream, all the details are encapsulated within the SSL classes. Listing Two shows the corresponding server code (server.java). The protocol is straightforward:

1. Client and server announce who they are (HELLO).

2. Server says it is ready to receive the credit-card information (READY).

3. Client sends the credit-card information.

4. Server acknowledges receipt (OK).

5. Client and server hang up.

The server has a few messy details relating to listening on the socket and handing off connections to new ClientProcessor threads. Since this isn't a tutorial on socket programming, I'll skip past that; download the sample code for the complete samples.

SSLSocket

The client side of an SSL connection is no easier/harder to set up than the server. They both can be pretty straightforward (remember, it's the keys that are the problem). Listing Three shows the main function for client.java. The function takes four parameters:

The socket type is important since it controls whether you use a standard socket or an SSL socket to connect to the server. The if statement in line 1 creates an SSL socket if you provide a socket type of Transport Layer Security (TLS) — the new name of SSL). Lines 2-10 set up an SSLContext object. An SSLContext is primarily used to group together the many services it takes to authenticate and encrypt communications.

The first to be created is a key-manager factory. The key-manager factory provides objects (KeyManagers) that manage particular types of key stores. Just as a keyring keeps your house keys organized, a KeyManager keeps the public and private keys needed for SSL communications organized. Lines 3-4 get the KeyManagerFactory and KeyStore up and running. A KeyStore contains the keys while the KeyManager knows how to provide access to the particular keys in the key store. The Java Key Store (JKS) is a binary file used to store keys. You could create other key store types that store keys in different formats; SQL or LDAP databases, for example. Depending on how you set up the system, you may want to have the server process store its keys in some sort of production-level database while using the JKS for the clients deployed in the field.

Lines 5-6 open the JKS using a passphrase. This passphrase was used to encrypt the key store when it was first created and must be supplied anytime the key store is accessed. Finally, KeyManagerFactory is initialized with the key store.

Lines 7-8 accomplish the somewhat simpler task of creating a TrustManagerFactory. Largely, it is easier because I reuse the same key store that was used to initialize the KeyManagerFactory. To finish up, the SSLContext finally gets what it needs in the way of key and trust managers.

SSL Protocol

A key ingredient of SSL is the ability to use public-key cryptography. Each party to the communication has two keys — one public key that can be freely sent to others and one private key it keeps to itself. The beauty of these systems is that anything encrypted with the public key can be decrypted with the private key, and vice versa. However, data encrypted with the public key cannot be decrypted with the public key, which is why the key can be sent out freely (the same is also true for the private key).

When two programs make an SSL connection, the following occurs:

At this point, both the client and server are satisfied that they are talking to the authorized and correct entities and have exchanged an encryption key they can use for this session. The encryption key is usually for some algorithm like DES, IDEA, or Blowfish. These algorithms are faster and use smaller keys than those used to implement public- or private-key systems. The security isn't significantly compromised though, because the session keys are only used once. Even if crackers were to break the key, they would only have access to that session's data. They could not use the key to decrypt future or past sessions, and they could not masquerade as either party because they would not have access to either party's private keys.

Key Authentication

Public keys are generally signed (the key is at least signed with itself), and the client and server look at who signed the key to see if that person can be trusted. What's important here is who you trust to sign keys.

There is a file in the JAVA_HOME/jre/ lib/security directory called "cacerts." If the key used to sign the remote process's public key is found in cacerts, then JSSE assumes the remote is okay and lets the connection proceed. If it isn't found, JSSE shuts the connection, and the remote is left out in the cold.

This is fine if you are implementing an HTTPS solution, but it is unsuitable for most purposes because cacerts is used by all JSSE applications and contains keys you didn't create (such as the Verisign key normally used to sign keys to be used by HTTPS servers). If you used this default implementation, then any client who paid Verisign $400 to sign its key could then gain access to your system: No need to crack the client's key — just pretend to be some other client with a valid key and gain access to the system. This isn't good.

Enter TrustManager

You can override the default cacerts functionality by creating your own TrustManager, because it is the default trust manager that is using cacerts to authenticate the keys. The cacerts file is actually just another key store and, to create a new trust manager, you create a new TrustManagerFactory and provide it the same key store used before (line 9 of Listing Three). Now your client and server will consider any key acceptable that has been signed by a key in their key store.

SSLServerSocket

Listing Four is the server side of the SSL connection. In lines 1-5, you create a server socket and start waiting for clients to connect. The interesting part is how you create the server socket factory. Lines 1-14 are the same as those in client.java. Remember that you are initializing the SSL context that is just a Java clearinghouse for all the information it takes to authenticate and encrypt a socket connection. Instead of getting a socket factory from the context, you get a server socket factory and return it to the main method.

Wrapping up the server, the main method passes the duties of creating the server socket and handling new connections from clients. Again, it is important to note that the acceptConnections method takes as a parameter a regular ServerSocket. Because SSLServerSocket is derived from ServerSocket, it can be passed to acceptConnections and used like any other ServerSocket. This means that it should be fairly easy to convert any plain socket application to one that uses SSL.

Installation

To use the sample code and develop your own applications, you need to install the JSSE package from http://java.sun.com/. Although you should read the installation instructions that come with the package, I'll give you the an express installation method to get you started. After you unzip the JSSE package, complete the following steps:

1. Copy jsse1.0.2/lib/* to JAVA_HOME jre/lib/ext.

2. Edit JAVA_HOME/jre/lib/security/ java.security.

3. Add the line security.provider.2=com.sun.net.ssl.internal.ssl .Provider to the file.

This gets JSSE working within Java with a minimum of fuss. (There are other ways to install JSSE, and this one might not be appropriate to your application.)

For the sample code, merely unzip the archive. You will find a jsse_article directory with client and server subdirectories. The directories contain all the files you need to run the applications. To start up the server, enter $ java server. Then start up the client:

$ java client plain localhost 5555-444-3333-2222 100.0

Connected to server.

Server ready.

Data sent.

$

The server console then displays:

$ java server

Client connected.

Credit card authorized.

The server writes the credit-card information to the transactions.log text file. If you have a packet sniffer handy, you will see what is shown in Figure 1. If the packet sniffer is being operated by someone with nefarious intent, then your customer could probably count on a credit-card bill larger than expected next month.

To fix that, kill the server and restart it with $ java server TLS. This tells the server to require all connections to be made through SSL. Start the client, and the displays on the console will be the same:

$ java client TLS localhost 5555-444-3333-2222 100.0

Connected to server.

Server ready.

Data sent.

$

The first thing you will notice is that the transaction takes far longer to complete. There is considerable overhead associated with starting up an SSL connection. In your own applications, you can save time if you cache the appropriate SSLContext objects between connections. You can do this if your client application will remain running and occasionally connect/disconnect from the server.

Now look at Figure 2 to see what the packet sniffer reveals about the conversation. The only text you can read is the information attached to the keys, and this isn't much help to any cracker wannabes because it reveals nothing without the private keys.

Key Management

Recall that the code is not the hard part of using the JSSE package. If you want to use JSSE to talk to HTTPS servers, then you are set, and the sample programs that come with JSSE will get you started. However, if you want to create your own servers and clients and authenticate both ends of the connection, then you have to know about key management.

There are many ways to store keys for access by your clients and servers. The easiest is to put each server's public key into the key store of each client, and each client's public key into each server's key store. This means everyone has each other's key and all can communicate with each other. The reason this works is that each person's public key is signed by their private key. When the programs validate the keys they were sent, they find that the key was signed by a trusted key in their key store. This gets to be a real problem if the applications involved can be both client and server. Every time you bring up a new client, you potentially have to go visiting a bunch of different nodes updating keys.

Instead of putting every key in every store, I suggest you create a master key that you put in all the key stores when you install the application. Then you use the master key to sign all the client and server keys you create. This way, when the applications send their keys, the keys will be signed by a trusted key in the key store. This is essentially what Verisign does and why HTTPS works between your web browser and servers you've never contacted before. Verisign has signed the server's key and Verisign's public key is in your browser's key store. You don't actually want to have Verisign sign all your keys, because then anyone with a Verisign-signed key could interact with your client.

Certificate Authority

In a nutshell, what I'm suggesting is that you create your own Certificate Authority (CA) to sign your keys. This gets complicated because nothing in the Java Development Kit or JSSE lets you set up a CA and sign keys. You have to go elsewhere for tools to do this. I chose to go with the OpenSSL toolkit (http://www.openssl.org/) running on Linux. There are toolsets available from other vendors and platforms, however. If you choose to use a different toolset, you will just have to substitute the appropriate commands; the theory is the same no matter what.

First, you need to generate your CA's key. That key is used to sign all the other application keys. The OpenSSL toolkit comes configured to setup a CA from whatever directory you start it in. This means that you need to use all the CA commands from the same directory. In the sample code, you'll find the CA directory that I used to generate the CA key and sign all the application keys:

1.Generate the CA key

$ openssl genrsa -rand -des -out ca.key 1024

2.Create a self signed certificate

$ openssl req -new -x509 -days 365 -key ca.key -out ca.crt

You are prompted for location information for the certificate. Enter whatever you want, but make sure you enter something for each field:

3.Setup the OpenSSL CA tools

$ mkdir demoCA

$ mkdir demoCA/newcerts

$ touch demoCA/index.txt

$ cp ca.crt demoCA/

$ echo "01" > demoCA/serial

You now can create the client application's key store and export its public key so your CA can sign it. You can enter whatever you want for all the location information, but again make sure you enter something — standard alphanumeric characters and spaces, but no underscores or other special characters — for every field:

4.Create a new key store for the client application

$ keytool -keystore testkeys -genkey - alias client

When prompted, enter passphrase for the password to use this keystore with the sample applications.

5.Export the client's public key

$ keytool -keystore testkeys -certreq -alias client -file client.crs

6.Sign the client's key with our CA key

$ openssl ca -config /etc/openssl.cnf -in client.crs -out client.crs.pem -keyfile ca.key

At this point, you should have a file called "client.crs.pem," which is the signed public key. It needs to be converted to a format suitable for the JDK's keytool command, and then imported into the testkeys keystore:

7.Convert to DER format

$ openssl x509 -in client.crs.pem -out client.crs.der -outform DER

8.Import CA certificate into client's key store

$ keytool -keystore testkeys -alias jsse_article_ca -import -file ca.crt

9.Import signed key into client's key store

$ keytool -keystore testkeys -alias client -import -file client.crs.der

Step 8 must be completed so that the keytool command agrees to import the signed key. While importing the signed key, keytool checks the signatories to ensure that their signatures can be validated. They can be validated if their public keys are in the key store.

Once you have completed all of these steps, move the testkeys key store to the client directory. Start over with step 4 and create a key store for the server process. Just substitute "server" everywhere you see "client." Make sure you enter something different in one of the location fields (organizational unit would be a good choice).

Going Into Production

For each new install of your application, just go through steps 4-9. You'll likely perform steps 4-5 on the computer where the application is installed, then take the product (client.crs) to the computer you have designated as your CA and walk through steps 6-7. Finally, take the product of those steps, client.crs.der, and the CA's certificate back to the new application. Perform steps 8-9 and you will be ready to communicate.

Each time you move a key from one computer to another, check its fingerprint to ensure it has not been modified. All of the major key commands will display the key fingerprint, which can be used to make sure nothing naughty has gone on behind your back. This is a necessity if you are not hand carrying the keys. If e-mail or some other transport is in use, the fingerprint is an effective guard against tampering. In a public-key system such as SSL with this key management scenario, not much could be more disastrous than accidentally signing a cracker's key.

Conclusion

The code to convert a regular socket-based application to an SSL socket is pretty minimal. However, the issues involved in key management are more complex. There are some things to watch out for if you implement this system. The computer used as the CA is the weak link. If unauthorized access is gained to this computer, the CA key could be stolen or, at the very least, used to sign bogus keys. Once this happens, crackers would have easy access to your systems. They likely wouldn't be able to intercept the communications between client and server, but would be able to masquerade as a valid client or server. If this does happen, you will need to create a new CA key and generate new key stores for all of your installs. This is really no different than what would happen if the keys for any of the commercial CAs were stolen. If possible, put this computer in a secure room, do not connect it to a network, and carry the keys in and out on floppy disks.

DDJ

Listing One

static void sendData( Socket server, String cc, String amnt ) {
    try { 
        BufferedWriter out = new BufferedWriter( 
            new OutputStreamWriter( server.getOutputStream() ) );
        BufferedReader in = new BufferedReader( 
            new InputStreamReader( server.getInputStream() ) );
        boolean success = false;
        String s = in.readLine();
        if ( s.equals( "HELO CC-SERVER" ) ) {
            System.out.println( "Connected to server." );
            println( out, "HELO CC-CLIENT" );
            s = in.readLine();
            if ( s.equals( "READY" ) ) {
                System.out.println( "Server ready." );
                println( out, cc + "\t" + amnt );
                s = in.readLine();
                if ( s.equals( "OK" ) ) {
                    success = true;
                } // if
            } // if
        } // if
        if ( success ) 
            System.out.println( "Data sent." );
        else
            System.out.println( "Protocal error. -- " + s );
        server.close();
    } catch ( Exception e ) {
            System.out.println( "Error: " + e.getMessage() );
            e.printStackTrace();
    } // catch
} // send data

Back to Article

Listing Two

class ClientProcessor implements Runnable {
    Socket socket = null;
    ClientProcessor( Socket client ) {
        socket = client;
    } // constructor
    public void run() {
        try {
            BufferedWriter out = new BufferedWriter( 
                new OutputStreamWriter( socket.getOutputStream() ) );
            BufferedReader in = new BufferedReader( 
                new InputStreamReader( socket.getInputStream() ) );
            println( out, "HELO CC-SERVER" );
            String s = in.readLine();
            if ( s.equals( "HELO CC-CLIENT" ) ) {
                System.out.println( "Client connected." );
                println( out, "READY" );
                s = in.readLine();
                writeTransaction( s );
                System.out.println( "Credit card authorized." );
                println( out, "OK" );
            } else {
                println( out, "IHATEYOU" );
            } // else
        } catch ( Exception e ) {
            System.out.println("Error: " + e.getMessage());
            e.printStackTrace();
        } // catch
        try {
            socket.close();
        } catch ( Exception ex ) {}
    } // run
   void writeTransaction( String s ) throws IOException {
        FileWriter fw = new FileWriter( "transactions.log", true );
        PrintWriter out = new PrintWriter( fw, true );
        out.println( s );
        out.flush();
        out.close();
        fw.close();
    } // writeTransaction
    static void println( BufferedWriter bw, String s ) throws IOException {
        bw.write( s );
        bw.newLine();
        bw.flush();
    } // println
} // ClientProcessor

Back to Article

Listing Three

public static void main( String[] args ) throws Exception {
    String type = args[0];
    String host = args[1];
    String cc = args[2];
    String amnt = args[3];
    Socket socket = null;
1:  if ( type.equals( "TLS" ) ) {
        SSLSocketFactory factory = null;
2:      SSLContext ctx = SSLContext.getInstance( "TLS" );
3:      KeyManagerFactory kmf = KeyManagerFactory.getInstance( "SunX509" );
4:      KeyStore ks = KeyStore.getInstance( "JKS") ;
5:      char[] passphrase = "passphrase".toCharArray();
6:      ks.load(new FileInputStream("testkeys"), passphrase);
7:      kmf.init(ks, passphrase);
8:      TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
9:      tmf.init( ks );
10:     ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
11:     factory = ctx.getSocketFactory();
12:     socket = factory.createSocket(host, DEFAULT_PORT);
    } else {
13:     socket = new Socket( host, DEFAULT_PORT );
    } // else
14: sendData( socket, cc, amnt );
} // main

Back to Article

Listing Four

public static void main(String args[]) {
   String type = "plain";
    if ( args.length > 0 ) {
        type = args[0];
    } // if
    try {
1:      ServerSocketFactory ssf = server.createServerSocketFactory( type );
2:      ServerSocket ss = ssf.createServerSocket( DEFAULT_PORT );
3:      if ( type.equals( "TLS" ) ) 
4:          ((SSLServerSocket)ss).setNeedClientAuth( true );
5:      acceptConnections( ss );
    } catch (IOException e) {
        System.out.println( "Error: " + e.getMessage() );
        e.printStackTrace();
    } // try
} // main
private static ServerSocketFactory createServerSocketFactory( String type ) {
    if ( type.equals( "TLS" ) ) {
        SSLServerSocketFactory ssf = null;
        try {
            // set up key manager to do server authentication
6:          KeyManagerFactory kmf = KeyManagerFactory.getInstance( "SunX509" );
7:          KeyStore ks = KeyStore.getInstance( "JKS" );
8:          char[] passphrase = "passphrase".toCharArray();
9:          ks.load(new FileInputStream("testkeys"), passphrase);
10:         kmf.init(ks, passphrase);
11:         TrustManagerFactory tmf = 
                 TrustManagerFactory.getInstance("SunX509");
12:         tmf.init( ks );
13:         SSLContext ctx = SSLContext.getInstance( "TLS" );
14:         ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
15:         ssf = ctx.getServerSocketFactory();
16:         return ssf;
        } catch (Exception e) {
            System.out.println( "Error: " + e.getMessage() );
            e.printStackTrace();
        } // try
    } else {
        return ServerSocketFactory.getDefault();
    } // if
    return null;
} // createServerSocketFactory
static void acceptConnections( ServerSocket server ) {
17: Socket socket = null;
    while( true ) {
        // accept a connection
        try {
18:         socket = server.accept();
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
            e.printStackTrace();
           return;
        } // catch
        // create a new thread to accept the next connection
19:     ClientProcessor cp = new ClientProcessor( socket );
20:     (new Thread( cp )).start();
    } // while
} // run

Back to Article

Feb01: The Java Secure Socket Extensions

Figure 1: Cleartext.

Feb01: The Java Secure Socket Extensions

Figure 2: Ciphertext.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.