SSL and TLS
Overview
Having created discrete secured messages using CMS and S/MIME, it is now time to turn your attention to the issue of creating secure links over which you can transfer data and exchange messages.
This chapter looks at SSL and its offspring, TLS. This family of protocols is very different from protocols like CMS. The main reason is that they are socket-based and designed to allow the creation of secured links between two end points for data transmission and exchange. You are probably already familiar with SSL at some level; you probably encountered URLs that begin with "https" while buying items over the Internet. HTTPS connections are created using SSL, but as you will see, the possible uses for the protocol go much further than that.
The SSL API for Java is referred to as the JSSE (Java Secure Socket Extension). By the end of this chapter, using the JSSE, you should
- Understand the basics of the SSL protocol
- Know how to create a basic SSL client and server to secure communications
- Know how to use client-side authentication
- Know how to access SSL session information
Finally, you will be able to write client-side programs that can use and configure URL connections based on HTTPS.
The SSL and TLS Protocols
SSL (Secure Sockets Layer) was originally developed by Netscape as a means of securing communications across the Internet, primarily for electronic commerce. Atotal of three versions of the protocol were developed, with the last one, SSL version 3.0, being released in 1996. Since then, the development of the original idea has continued with TLS (Transport Layer Security), with the first version being published in RFC 2246 in January 1999. At this point, virtually all financial institutions have endorsed the use of SSL for electronic commerce.
The protocol is an interesting one, as it designed to be extensible. The initial handshake done between two end points when they are setting up an SSL channel includes negotiation for both the algorithms to be used and the key size . The algorithm set used for an SSL channel normally includes at least one public key mechanism, a symmetric cipher, a MAC algorithm, as well as their associated key sizes. This collection of algorithms is normally referred to as a cipher suite , and as you may well imagine, some cipher suites are better than others from a security point of view. The presence of weak cipher suites is largely for historical reasons, but you do need to be aware that they exist, as when you are using SSL, you should to take care to configure the link creation with specific cipher suites. You should do this so that a possibly malicious server or client cannot dumb down the cryptography being used on the link by using the negotiation phase to its advantage and subsequently fool your end of the link into transmitting the data across a link with much lower security than you wanted.
Normally you would have seen SSL being used where only one end point is actually authenticated(the server end. This is the common practice for Internet shopping; however, the protocol does allow for both ends of the link to authenticate to the other one. This type of link is usually described as one with clientside authentication, because it is assumed the server side is always authenticated.
Since RFC 2246 came out, other RFCs have also been published that add the use of Kerberos cipher suites (RFC 2712), extensions (RFC 3546), the AES algorithm (RFC 3268), and compression methods (RFC 3749). The RFCs read quite differently than the normal ones you run into in security, as, unlike the PKIX RFCs on the use of public key certificates, the TLS- related RFCs have their message formats documented in a C-like language rather than ASN.1. Currently, TLS is still at version 1.0, but at this writing, there is a TLS version 1.1 RFC under development, which will probably be released at the end of 2005. You can find out more about the development of TLS by looking at the Web site for the TLS Working Group, which is listed in Appendix D.
Having said all that, as SSL and TLS are both low-level protocols that operate just above the socket layer, you do not really need to be familiar with the RFCs to be able to use it in Java. The Java API that deals with SSL and TLS is the Java Secure Socket Extension, or JSSE for short. At its simplest, it can be summed up as providing two additional socket factory types and two additional socket types that can be used in the same way as regular socket factories and sockets defined in the java.net package. The JSSE goes a lot further than that, but that should give you an idea.
Like SSL itself, the JSSE has now gone through several versions. It was previously a standard extension to the JDK, versions 1.2 and 1.3. As of JDK 1.4, the JSSE was integrated into the JDK. This did result in some changes to the API, as prior to its integration with the JDK, some of the classes now found in the javax.net.ssl package were originally found in com.sun.net.ssl. So, if you are trying to use the examples with older versions of the JSSE, you might need to change the imports used in the examples accordingly and make a few other minor adjustments.
As of JDK 1.5, the JSSE can also support the Kerberos cipher suites outlined in RFC 2712. I won't discuss Kerberos support here in any detail, because I want to concentrate on using SSL with X.509, but it is a fact worth being aware of.
Getting Started
The Utils class for this chapter is quite simple; all it does is define some constants that will be used by the examples. You also need some keystores to use for the server and client sides of the links you'll be creating in the examples. By way of further reading, although it won't stop you from running the examples, or probably even understanding them, being familiar with some of the classes in the java.net package will not hurt.
I'll start with the Utils class and then move onto the keystore creation utility. Here is the new Utils class:
package chapter10; /** * Chapter 10 Utils */ public class Utils extends chapter9.Utils { /** * Host name for our examples to use. */ static final String HOST = "localhost"; /** * Port number for our examples to use. */ static final int PORT_NO = 9020; /** * Names and passwords for the keystore entries we need. */ public static final String SERVER_NAME = "server"; public static final char[] SERVER_PASSWORD = "serverPassword".toCharArray(); public static final String CLIENT_NAME = "client"; public static final char[] CLIENT_PASSWORD = "clientPassword".toCharArray(); public static final String TRUST_STORE_NAME = "trustStore"; public static final char[] TRUST_STORE_PASSWORD = "trustPassword".toCharArray(); }
and here is the keystore creation utility:
package chapter10; import java.io.FileOutputStream; import java.security.KeyStore; import java.security.cert.Certificate; import javax.security.auth.x500.X500PrivateCredential; /** * Create the various credentials for an SSL session */ public class CreateKeyStores { public static void main(String[] args) throws Exception { X500PrivateCredential rootCredential = Utils.createRootCredential(); X500PrivateCredential interCredential = Utils.createIntermediateCredential( rootCredential.getPrivateKey(), rootCredential.getCertificate()); X500PrivateCredential endCredential = Utils.createEndEntityCredential( interCredential.getPrivateKey(), interCredential.getCertificate()); // client credentials KeyStore keyStore = KeyStore.getInstance("PKCS12", "BC"); keyStore.load(null, null); keyStore.setKeyEntry( Utils.CLIENT_NAME, endCredential.getPrivateKey(), Utils.CLIENT_PASSWORD, new Certificate[] { endCredential.getCertificate(), interCredential.getCertificate(), rootCredential.getCertificate() }); keyStore.store( new FileOutputStream(Utils.CLIENT_NAME + ".p12"), Utils.CLIENT_PASSWORD); // trust store for both ends keyStore = KeyStore.getInstance("JKS"); keyStore.load(null, null); keyStore.setCertificateEntry( Utils.SERVER_NAME, rootCredential.getCertificate()); keyStore.store( new FileOutputStream(Utils.TRUST_STORE_NAME + ".jks"), Utils.TRUST_STORE_PASSWORD); // server credentials keyStore = KeyStore.getInstance("JKS"); keyStore.load(null, null); keyStore.setKeyEntry( Utils.SERVER_NAME, rootCredential.getPrivateKey(), Utils.SERVER_PASSWORD, new Certificate[] { rootCredential.getCertificate() }); keyStore.store( new FileOutputStream(Utils.SERVER_NAME + ".jks"), Utils.SERVER_PASSWORD); } }
As you can see, the keystore creation utility, CreateKeyStores , creates three keystores, using methods on the Utils class that were defined in previous chapters. The first file is for client credentials, the second file contains a single trusted certificate that will be used as the trust anchor for (eventually) both ends to validate the X.509 credentials passed to them, and the third keystore contains the server credentials.
Important |
For the rest of chapter, I refer to SSL to cover both SSL and TLS. This convention is used in the Java API, and my explanations will become rather labored if I'm constantly writing "SSL and TLS." When a distinction needs to be drawn between the two protocols, I mention the protocol explicitly; otherwise , wherever you see "SSL" think "SSL and TLS." |
A Basic SSL Client and Server
In its most common form, SSL is used between an authenticated server and a client. What takes place is described in Figure 10-1.
Figure 10-1
In the case of an X.509-based cipher suite, this is done by the server presenting the client with a certificate chain (the Certificate message) after the client initially connects. If the protocol is using RSA, the client then uses the end entity certificate to encrypt an appropriate pre-master secret, which it sends back to the server(the ClientKeyExchange message. The pre-master secret is then converted by both ends into a master secret, which provides the bits required for a symmetric key required for doing encryption on the data and the MACs used for verifying the integrity of the data. Both ends start encrypting and communication on the link then proceeds.
You might be wondering what the optional ServerKeyExchange message is for. It's there because SSL includes key agreement in the mechanisms that can be used for the client and the server to arrive at an agreed secret. If the server is using key agreement, this message will be used to pass the server's parameters to the client so the necessary calculations can be done on the client side.
Raised up to the level of the JSSE API, you need to be able to use only four JSSE classes to get this to work. Two of them are factory classes, and the other two are socket classes used to represent the end points of the SSL connection.
Let's look at the factory classes first.
The SSLSocketFactory Class
The javax.net.ssl.SSLSocketFactory is used to create SSLSocket socket objects.
The class is an extension of the java.net.SocketFactory class and, in the same way as its parent class, is not created directly but normally by the getDefault() method. Like the SSLServerSocketFactory class, it is also possible to create SSLSocketFactory objects using an SSLContext object. I will cover this further on. Other than that, this class is quite straightforward, as you can see from the method descriptions that follow.
SSLSocketFactory.create Socket()
The createSocket() method returns a Socket object, which can be cast to SSLSocket if required.
Most of the createSocket() methods on the SSLSocketFactory class are inherited from the SocketFactory class and take the equivalent parameters.
The SSLSocketFactory also provides one additional createSocket() method, which takes an existing Socket , the host and port it refers to as a String and an int , respectively, and a boolean , which determines whether the Socket parameter passed to the method should close automatically when the SSLSocket returned by the method does. You can use this method if you want to create an SSL "tunnel" over the top of an existing connection.
SSLSocketFactory.get Default()
The static getDefault() method returns an object providing a default implementation of the SSLSocketFactory class.
The default implementation can be changed by setting the value of the ssl.SocketFactory.provider security property to the desired class. The property should be set in the java.security file if you want to change it. This is the same file you configured the Bouncy Castle provider in.
SSLSocketFactory.get DefaultCipherSuites()
The getDefaultCipherSuites() method on both classes returns an array of String objects representing the cipher suites that are enabled by default. The default list excludes null ciphers.
SSLSocketFactory.get SupportedCipherSuites()
The getSupportedCipherSuites() method on both classes returns an array of String objects representing the cipher suites that are available to be used for SSL.
The SSLServerSocketFactory Class
The javax.net.ssl.SSLServerSocketFactory is used to create SSLServerSocket socket objects.
It is an extension of the java.net.ServerSocketFactory class and, like its parent, is not created directly but is created instead by other factory methods. Generally, this is from SSLServerSocketFactory.getDefault(), but you will also see later that it is possible to create them using SSLContext objects.
As you can see from the method descriptions that follow, this class is also quite straightforward and only adds two extra methods to the class signature inherited from ServerSocketFactory.
SSLServerSocketFactory.create ServerSocket()
The createServerSocket() methods on SSLServerSocketFactory are all inherited from its parent class and are the same, other than the fact the ServerSocket objects they return can be safely cast to SSLServerSocket .
SSLServerSocketFactory.get Default()
The static getDefault() method returns an object providing a default implementation of the SSLServerSocketFactory class.
The default implementation can be changed by setting the value of the security property ssl.ServerSocketFactory.provider to the desired class. The property should be set in the java.security file if you want to change it. This is the same file you configured the Bouncy Castle provider in.
SSLServerSocketFactory.get DefaultCipherSuites() and SSLServerSocketFactory.getSupportedCipherSuites()
Both the getDefaultCipherSuites() method and the getSupportedCipherSuites() method provide the same information as the methods of the same name on the SSLSocketFactory class.
The SSLSocket Class
Like the class it is an extension of, java.net.Socket , objects of the type javax.net.ssl.SSLSocket are not created directly but are created using a factory methods on SSLSocketFactory objects or as a result of an SSLServerSocket.accept() returning.
The extra methods added by the SSLSocket class can be divided into three categories: methods for basic configuration, adding listeners, and configuring client-side authentication. Initially I will just discuss the basic configuration messages and discuss the other two categories as appropriate later in the chapter.
The following methods are for basic configuration.
SSLSocket.set EnabledCipherSuites()
The setEnabledCipherSuites() method takes an array of String objects representing the cipher suites that can be used with sockets that result from this server socket doing an accept. The method has a corresponding get() method, getEnabledCipherSuites() , which returns a String array representing what cipher suites are currently enabled.
The method throws an IllegalArgumentException when one or more of the ciphers named in its parameter do not appear in the array returned by getSupportedCipherSuites() , or when the parameter is null .
In general you should always call this method, or its equivalent on SSLServerSocket, at least once. If you look at the list of supported cipher suites, you will realize that the security offered by them varies, and by explicitly setting the enabled cipher suites, using setEnabledCipherSuites() , you can ensure that any SSL connections that take place are done with the security level you expected.
SSLSocket.set EnabledProtocols()
The setEnabledProtocols() method takes an array of String objects representing the variations on the SSL protocol that can be used with this socket. The method has a corresponding get() method, getEnabledProtocols(), which returns a String array representing what protocols are currently enabled.
The method throws an IllegalArgumentException when one or more of the ciphers named in its parameter do not appear in the array returned by getSupportedProtocols() , or when the parameter is null .
As with setEnabledCipherSuites() , in many situations it is also a good idea to explicitly specify which version of the protocol you want to use rather than leaving it to chance.
SSLSocket.set EnableSessionCreation()
The setEnableSessionCreation() method takes a single boolean parameter, which if true , allows the SSLSocket object to create new SSL sessions. If the parameter value is false , then the socket can only be used to resume existing sessions.
The method has a corresponding get() method, getEnableSessionCreation() , that returns true if new SSL sessions may be established by this socket. A return value of false indicates the socket can only be used to resume an existing session.
SSLSocket.set UseClientMode()
In general, you will never need to use this; however, the setUseClientMode() method takes a single boolean as a parameter, which if true , specifies that when a handshake is done on this socket this end of the link will process in client mode. The method needs to be called before any initial handshaking has been done on the socket and will throw an IllegalArgumentException if this is not the case.
The method has a corresponding get() method, getUseClientMode() , that returns true if connections on the socket will be in SSL client mode, false otherwise .
SSLSocket.startHandshake()
The startHandshake() method starts an SSL handshake on the socket on which it is called.
The method will throw an IOException if there is a problem performing the handshake. If a handshake fails, the SSLSocket is closed and no further communication can be done.
Strictly speaking, a handshake will be done for you the moment you try writing to the socket, but you may want to use this method so that any exceptions thrown due to handshake failure are explicit. You may want do this so you can handle the exception yourself or to make sure you get the original exception rather than one that may have been wrapped and thrown again by another object that is wrapping one of the socket's streams. Other reasons for using the method include the need to use new encryption keys or to initiate a new session. If the current session has been invalidated, a call to startHandshake() will cause a complete reauthentication to take place.
The SSLServerSocket Class
The javax.net.ssl.SSLServerSocket class is an extension of java.net.ServerSocket , and objects of the SSLServerSocket type are created using factory objects of the type SSLServerSocketFactory . They are used to accept incoming connection requests using the accept() method, which returns Socket objects that can be cast to SSLSocket .
The extra methods added by the SSLServerSocket class can be divided into two categories: methods for basic configuration and for the configuration of client-side authentication.
The following methods are for basic configuration.
SSLServerSocket.set EnabledCipherSuites()
Other than the fact the method applies the setting to any sockets produced as a result of an accept() on this SSLServerSocket object, the setEnabledcipherSuites() method has the same meaning and definition as its equivalent on the SSLSocket class. As with SSLSocket , there is also a getEnabledCipherSuites() method.
Remember that this is a method you should probably be calling if it is important to you to guarantee a certain level of security. As mentioned earlier, the strength of an SSL connection is based on negotiation and there is no guarantee a connecting client will always be interested in making sure the connection is running at its maximum level of security.
SSLServerSocket.set EnabledProtocols()
The setEnabledProtocols() method has the same meaning and definition as its equivalent on the SSLSocket class, but applies the settings passed in to any sockets produced as a result of an accept() method returning. As with SSLSocket , there is also a getEnabledProtocols() method.
SSLServerSocket.set EnableSessionCreation()
The setEnableSessionCreation() method has the same meaning and definition as its equivalent on the SSLSocket class, but applies the settings passed in to any sockets produced as a result of an accept() method returning. As with SSLSocket , there is also a getEnableSessionCreation() method.
SSLServerSocket.set UseClientMode()
The setUseClientMode() method has the same meaning and definition as its equivalent on the SSLSocket class, but applies the settings passed in to any sockets produced as a result of an accept() method returning. As with SSLSocket, there is also a getUseClientMode() method.
This concludes basic configuration. Now take a look at an example.
Try It Out: A Basic SSL Client and Server
This example is written to use a simple protocol that just terminates its messages with a "!" character, and, unlike most examples you have done before, this one is in two parts . (You need different programs to run on the server and client ends.) Both programs also require the use of some command-line arguments. You will also need to use the CreateKeyStores utility that was listed in the section called "Getting Started," so make sure you have that class compiled before you go any further.
First up, here is the source for the client end of the SSL link you will create. As you can see, the SSL- related work is done in the main driver.
package chapter10; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; /** * Basic SSL Client - using the '!' protocol. */ public class SSLClientExample { /** * Carry out the '!' protocol - client side. */ static void doProtocol(Socket cSock) throws IOException { OutputStream out = cSock.getOutputStream(); InputStream in = cSock.getInputStream(); out.write(Utils.toByteArray("World")); out.write('!'); int ch = 0; while ((ch = in.read()) != '!') { System.out.print((char)ch); } System.out.println((char)ch); } public static void main(String[] args) throws Exception { SSLSocketFactory fact = (SSLSocketFactory)SSLSocketFactory.getDefault(); SSLSocket cSock = (SSLSocket)fact.createSocket( Utils.HOST, Utils.PORT_NO); doProtocol(cSock); } }
This program will run on the server side of the SSL link. Once again, notice that all the SSL-related work is done in the main driver.
package chapter10; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocket; /** * Basic SSL Server - using the '!' protocol. */ public class SSLServerExample { /** * Carry out the '!' protocol - server side. */ static void doProtocol(Socket sSock) throws IOException { System.out.println("session started."); InputStream in = sSock.getInputStream(); OutputStream out = sSock.getOutputStream(); out.write(Utils.toByteArray("Hello ")); int ch = 0; while ((ch = in.read()) != '!') { out.write(ch); } out.write('!'); sSock.close(); System.out.println("session closed."); } public static void main(String[] args) throws Exception { SSLServerSocketFactory fact = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SSLServerSocket sSock = (SSLServerSocket)fact.createServerSocket(Utils.PORT_NO); SSLSocket sslSock = (SSLSocket)sSock.accept(); doProtocol(sslSock); } }
The first step to running the example is to create a temporary directory you can work in. Once you have done that, you should change directory to it and run the following command:
java chapter10.CreateKeyStores
If this doesn't work, make sure you have your copy of the chapter10 package in your Java class path and then try again. If it does work, you should find you have three files in your directory: client.p12, server.jks , and trustStore.jks . You won't use the client.p12 file just yet, but you will need the other two.
Now you can start the server using the following command that should be entered on one line:
java -Djavax.net.ssl.keyStore=server.jks -Djavax.net.ssl.keyStorePassword=serverPassword chapter10.SSLServerExample
If you're running under Windows, your firewall might prompt you for authorization to continue. You'll need to grant this so the example can run.
The next step is to create another window, change its directory to the temporary working directory you have set up, and run the client using the following command:
java -Djavax.net.ssl.trustStore=trustStore.jks chapter10.SSLClientExample
You will then see this in the server window:
session started. session closed.
and the server will exit.
In the client window, you will see
Hello World!
and the client will exit.
How It Works
There's obviously a bit of "magic" going on here. In this case, it is possible because the defaults for the JSSE are set up so that the common use of SSL is also the default one. This example takes full advantage of that fact.
Looking at the server code first, you can see that only the following lines were required to create the SSL server end:
SSLServerSocketFactory fact = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SSLServerSocket sSock = (SSLServerSocket)fact.createServerSocket(Utils.PORT_NO); SSLSocket sslSock = (SSLSocket)sSock.accept(); doProtocol(sslSock);
The code creates a default server socket factory, creates a server socket on the port you want, does an accept() , and then performs the protocol. The "magic" is the default factory that picks up the values of the system properties you have passed to the command line configuring a keystore and the password to be used with it. The keystore is used by the default factory implementation to provide the necessary credentials to be used to identify the server to the other end of the link.
The client is even simpler, with only the following lines of code required to handle the setting up of an SSL connection:
SSLSocketFactory fact = (SSLSocketFactory)SSLSocketFactory.getDefault(); SSLSocket cSock = (SSLSocket)fact.createSocket( Utils.HOST, Utils.PORT_NO); doProtocol(cSock);
Here, you create a default socket factory, create the socket, and perform the protocol. Once again, the magic is performed by the default factory. It uses the value of the system property you passed to the JVM to find a keystore that contains trusted certificates. These trusted certificates allow the default factory to validate any credentials, in this case a certificate chain from the server, presented to it.
The default factories also have a default place to look for the trust store if the command-line setting of the javax.net.ssl.trustStore property is not specified. I have already mentioned in Chapter 8 that the JVM's security directory contains a file called cacerts that contains a keystore. It is also possible to create a keystore in the same directory called jssecacerts, and if the javax.net.ssl.trustStore property is not set, the JVM will first look for the jssecacerts file. If the jssecacerts file does not exist, it will use the cacerts file.
Other properties that can be set for the trust store are javax.net.ssl.trustStorePassword , which represents the password to be used with the trust store keystore; javax.net.ssl.trustStoreProvider , which is the provider used to create it; and javax.net.ssl.trustStoreType , which is the type of keystore being used as a trust store if it isn't the JVM's default one. In production, you should use the javax.net.ssl.trustStorePassword property, because it will force an integrity check on the file. If the trust store you are using is actually a hardware device rather than a file, the keystore type you use for the trust store is NONE .
The properties javax.net.ssl.keyStoreProvider and javax.net.ssl.keyStoreType can also be used in a similar fashion for specifying a source of identifying credentials for a server or a client.
The HandshakeCompletedListener Interface
It is also possible to attach a listener to an SSLSocket so that you can be informed as to when the handshake calculation has finally been done between the two ends of the link.
This is done by SSLSocket.addHandshakeCompletedListener() , which takes a single object implementing the javax.net.ssl.HandshakeCompletedListener interface as a parameter and registers it to receive notifications that an SSL handshake has completed on this socket. There is also a corresponding SSLSocket.removeHandshakeCompletedListener() method that can be used to remove the listener once it is no longer required.
The HandshakeCompletedListener interface has a single method, handshakeCompleted() , which is invoked by the SSLSocket that the listener is attached to when an SSL handshake completes. The handshakeCompleted() method is passed a javax.net.ssl.HandshakeCompletedEvent object. If you look at the JavaDoc for this class, you will see that it contains the full details of the session that has just been established. You can use this information for doing connection logging and other processing.
Both the addHandshakeCompletedListener() and the removeHandshakeCompletedListener() methods will throw an IllegalArgumentException if the parameter passed to them is null . The remove() method will also throw an IllegalArgumentException if the parameter passed to it does not represent a current listener on the SSLSocket concerned .
Client Side Authentication
So far, the connection you have created is authenticated only on the server side. Although this is fine for most applications related to, say, Internet shopping, a lot of business-to-business and corporate applications require that both sides of the link are authenticated. Additional messages are used in the handshake leading to a process like the one described in Figure 10-2.
Figure 10-2
As you can see, the SSL protocol enables this by allowing the client to also send back a certificate chain in the Certificate message along with the pre-master secret and then sending a signature, the CertificateVerify message, which can be validated using the end entity certificate in the client's certificate chain. The signature is calculated on the data stream that represents all the messages that have been exchanged starting from the initial client message. Doing it this way is quite useful, as it does not require the client to have a public key that can be used for encryption. Instead, the client can also use a range of digital signature specific algorithms as well to authenticate itself.
As client-side authentication is an option with SSL, introducing it requires you to make use of some additional configuration methods . Normally you will just use the ones on the SSLServerSocket class; however, it is also possible to customize an SSLSocket object in server mode to use client-side authentication. The next section starts with the configuration methods on the SSLServerSocket class.
SSLServerSocket Configuration
Configuring an SSLServerSocket to request client configuration is very straightforward, with two methods now being provided for the purpose; the older setNeedClientAuth() method and a newer method added in JDK 1.4, setWantClientAuth() .
SSLServerSocket.set NeedClientAuth()
The setNeedClientAuth() method takes a single boolean parameter. If the parameter has the value true , a server-mode socket created as a result of an accept() will enforce client-side authentication. If the client is unable to provide authentication, the connection will be dropped. Calling this method overrides any previous use of setWantClientAuth() . The method has a corresponding get() method called getNeedClientAuth() , which returns a boolean indicating whether optional client-side authentication will be requested .
SSLServerSocket.set WantClientAuth()
The setWantClientAuth() method takes a single boolean parameter. If the parameter has the value true , a server-mode socket created as a result of an accept() will initially request client-side authentication. If the client is unable to provide authentication, the connection will proceed regardless. Calling this method overrides any previous use of setNeedClientAuth() . The method has a corresponding get() method called getWantClientAuth() , which returns a boolean indicating whether optional client-side authentication will be requested.
The methods setWantClientAuth() and getWantClientAuth() are only available in JDK 1.4 and later.
Server Mode SSLSocket Configuration
There are also some methods on SSLSocket that allow you to customize the settings for client-side authentication for an individual socket being used in server mode. As you would expect, they are the same as the ones available on the SSLServerSocket class and need to be invoked before the SSLSocket.startHandshake() method has been called.
The setNeedClientAuth() Method
The setNeedClientAuth() method takes a single boolean value. Its meaning is the same as the method with the same name on the SSLServerSocket class, except it only applies to the SSLSocket object it is called on. Likewise, a corresponding get() method, getNeedClientAuth() , is also available.
The setWantClientAuth() Method
The setWantClientAuth() method takes a single boolean value. Its meaning is the same as the method with the same name on the SSLServerSocket class, except it only applies to the SSLSocket object it is called on. Likewise, a corresponding get() method, getWantClientAuth() , is also available, and the get() and set() methods are only available in JDK 1.4 and later.
The SSLContext Class
The javax.net.ssl.SSLContext class is used as a carrier for the credentials and other information associated with particular SSL links. You have already used this class indirectly, because the default factory objects are created using an SSLContext object, which uses the system properties you were setting to locate the credentials that were used to identity the server and the trust store that was used by the client to validate them.
You can also create your own SSLContext objects using the getInstance() factory pattern that you saw in the JCE and the JCA. All the getInstance() methods take a String as a parameter that represents the protocol being requested. In addition to the method that just takes a single String parameter and uses a default implementation, there are two additional versions of the method that take the name of the provider required or a class representing the provider required if you want to specify which provider should be used to provide the underlying implementation of the SSLContext object.
Legitimate values for the protocol String parameter are SSL or TLS , where SSL is an alias for SSLv3 and TLS is an alias for TLSv1 . If the protocol you are requesting is not available, the getInstance() method will throw a NoSuchAlgorithmException. If you use the alternate methods that allow you to specify a provider, the getInstance() method may throw an IllegalArgumentException if the parameter representing the provider is null , and if you use the getInstance() method that accepts a String representing the provider name and the provider cannot be found, a NoSuchProviderException will be thrown.
I'll only cover the commonly used methods on the class here. If you look at the JavaDoc for JDK 1.5, you'll find that a few extra methods have been added, such as those allowing you to create your own javax.net.ssl.SSLEngine objects if you require. These can be useful if you want to work with the non-blocking I/O library. I won't go into details on non-blocking I/O here, but you can find details on using it with SSL in the "JSSE Reference Guide" that forms part of the Java documentation set.
SSLContext.init()
The init() method is used to initialize the context. It takes three parameters: an array of KeyManager objects, an array of TrustManager objects, and a SecureRandom . If any of the parameter values are null , a default implementation of the required parameter value will be used. You will look at how you create KeyManager and TrustManager objects over the next two examples, but as you have probably guessed, the KeyManager objects are used to provide credentials needed to identify the local end of the SSL connection to the peer at the other end, and the TrustManager objects are used to provide the resources required to validate the identity the peer is presenting.
The method will throw a KeyManagementException if the operation fails.
SSLContext.get ClientSessionContext()
The getClientSessionContext() method returns a javax.net.ssl.SSLSessionContext object associated with the handshake phase of client-side connections. In the case where the session context is not available, the method returns null .
If an SSLSessionContext object is returned, you can use it for finding out and setting the session timeout, finding out and setting the number of sessions that can be cached in the context, and finding out the IDs of all the sessions associated with the context.
SSLContext.getProtocol()
The getProtocol() method returns a String representing the name of the protocol associated with this context. It will be the same name that was specified in the getInstance() call that created this SSLContext object.
SSLContext.get ServerSessionContext()
The getServerSessionContext() method returns an SSLSessionContext object associated with the handshake phase of server-side connections. In the case where the session context is not available, the method returns null .
SSLContext.get ServerSocketFactory()
The getServerSocketFactory() returns an SSLServerSocketFactory that will create SSLSocket objects on accepted connections that will use the protocol, key managers, and trust managers associated with this context.
SSLContext.get SocketFactory()
The getSocketFactory() returns an SSLSocketFactory that will create SSLSocket objects that will use the protocol, key managers, and trust managers associated with this context.
The KeyManagerFactory Class
The javax.net.ssl.KeyManagerFactory class is used to create objects that implement the javax.net.ssl.KeyManager interface. Like other security-related classes, it is created using the getInstance() pattern and follows the usual rules in regards to provider precedence when getInstance() is called and the provider is not explicitly specified along with the algorithm requested. The standard algorithm to use with the getInstance() method is SunX509 .
Aprovider can be specified to getInstance() explicitly as either a String representing the provider's name or as an object representing the provider's implementation. If the algorithm requested is not available, the method throws a NoSuchAlgorithmException. In the event the provider is specified explicitly, the method throws an IllegalArgumentException if the provider parameter is null or if the getInstance() method that takes a provider name is used and the provider cannot be found, the method throws a NoSuchProviderException .
The class has only a few methods on it, and its use as a factory is straightforward. As you will see in the example following the method descriptions, use of the factory is a simple two-step process.
KeyManagerFactory.init()
There are two init() methods on the KeyManagerFactory class. Both methods serve the purpose of providing the KeyManagerFactory with a source for the private keys required for allowing the SSLContext holding KeyManager objects produced by this factory to create an authenticated connection.
The first init() method takes a KeyStore object and a char array representing a key password. If there is more than one private key in the keystore, the KeyManagerFactory object will assume that all keys in the keystore passed in have the same password. The method can throw any of KeyStoreException, NoSuchAlgorithmException , or UnrecoverableKeyException , depending on which problems occur when processing the passed in keystore.
The second init() method takes an object implementing the javax.net.ssl.ManagerFactory Parameters interface. The method will throw an InvalidAlgorithmParameterException if a problem occurs. ManagerFactoryParameters is a simple marker interface with no methods on it, and how the parameter object that implements it is interpreted is up to the underlying provider implementing the KeyManagerFactory object you are using. One use of this interface worth looking at is the KeyStoreBuilderParameters class, which is also in the javax.net.ssl package. The class is an interesting one because it will show you an application of the KeyStore.Builder class that was mentioned in Chapter 8.
KeyManagerFactory.get Algorithm()
The getAlgorithm() method returns a String representing the algorithm name for this KeyManagerFactory object. This will be the same as the algorithm parameter passed to KeyManagerFactory.getInstance() .
KeyManagerFactory.get DefaultAlgorithm()
The static getDefaultAlgorithm() returns a String representing the default algorithm name for KeyManagerFactory objects created using the JVM.
The default algorithm can be changed setting the value of the ssl.KeyManagerFactory.algorithm security property in the java.security file. Alternately, it can be changed at runtime by calling the Security.setProperty() method with the property name and the desired algorithm.
KeyManagerFactory.get KeyManagers()
The getKeyManagers() method returns an array of objects implementing the KeyManager interface, one for each type of key material the KeyManagerFactory object was initialized with.
The KeyManager interface is a marker interface with no methods on it, and what actual implementations are returned in the array generated by getKeyManagers() depends on the underlying provider. In the case of SSL based on X.509, the standard JSSE KeyManagerFactory will return objects implementing the javax.net.ssl.X509KeyManager interface or javax.net.ssl.X509ExtendedKeyManager.
Fortunately, you don't have to worry so much about what is returned by the KeyManagerFactory as long as the array contains objects that are compatible with the SSLContext object you are initializing. Having a basic understanding of the return value of the getKeyManagers() method is enough to allow you to create a client that has client-side authentication, without the need to make use of Java system properties as you did with the server in the last example. You will look at how that is done now.
Try It Out: Introducing Client-Side Authentication
This example introduces client-side authentication and the use of the KeyManagerFactory class. As before, it is in two parts. Both parts extend from the classes you created for the last example, so that it isn't necessary to reproduce the code that supports the "!" protocol that is used to test the connections.
The first thing you will notice is that most of the changes are on the client class and that they relate to the creation of an SSLContext object, which can be used to create the SSLSocket object that the client uses to connect to the server process.
Here is the source for the new client:
package chapter10; import java.io.FileInputStream; import java.security.KeyStore; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; /** * SSL Client with client-side authentication. */ public class SSLClientWithClientAuthExample extends SSLClientExample { /** * Create an SSL context with a KeyManager providing our identity */ static SSLContext createSSLContext() throws Exception { // set up a key manager for our local credentials KeyManagerFactory mgrFact = KeyManagerFactory.getInstance("SunX509"); KeyStore clientStore = KeyStore.getInstance("PKCS12"); clientStore.load(new FileInputStream("client.p12"), Utils.CLIENT_PASSWORD); mgrFact.init(clientStore, Utils.CLIENT_PASSWORD); // create a context and set up a socket factory SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(mgrFact.getKeyManagers(), null, null); return sslContext; } public static void main(String[] args) throws Exception { SSLContext sslContext = createSSLContext(); SSLSocketFactory fact = sslContext.getSocketFactory(); SSLSocket cSock = (SSLSocket)fact.createSocket( Utils.HOST, Utils.PORT_NO); doProtocol(cSock); } }
The new server class is very similar to the one in the previous example. All you need to do is call the configuration method that specifies that you only want to accept authenticated clients .
Here is the source for the new server:
package chapter10; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocket; /** * Basic SSL Server with client authentication. */ public class SSLServerWithClientAuthExample extends SSLServerExample { public static void main( String[] args) throws Exception { SSLServerSocketFactory fact = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SSLServerSocket sSock = (SSLServerSocket)fact.createServerSocket(Utils.PORT_NO); sSock.setNeedClientAuth(true); SSLSocket sslSock = (SSLSocket)sSock.accept(); doProtocol(sslSock); } }
As with the last example, you should run this example using two windows in the same temporary directory you set up before. You'll need to do this because this example requires all the keystores that you created previously. Note that each command should be entered on one line as before. (Any line breaks you see here are due to the formatting requirements of this book.)
The first thing to do is to start the new server using the following command:
java -Djavax.net.ssl.keyStore=server.jks -Djavax.net.ssl.keyStorePassword=serverPassword -Djavax.net.ssl.trustStore=trustStore.jks chapter10.SSLServerWithClientAuthExample
As you can see, the client is now authenticating itself. You need to provide the server with a trust store.
Now that the server is running, you can run the new client as well:
java -Djavax.net.ssl.trustStore=trustStore.jks chapter10.SSLClientWithClientAuthExample
In this case, you haven't had to set the javax.net.ssl.keyStore properties as you did for the server, because the client is providing its own KeyManager objects.
You will then see this in the server window:
session started. session closed.
and the server will exit.
In the client window, you will see
Hello World!
and the client will exit.
How It Works
In this example, the server end of the link is still largely taking advantage of the default SSLServerSocketFactory that the JSSE makes available. There are only two changes you had to make to create a server that expects authenticated clients.
The first change is the addition of the line
sSock.setNeedClientAuth(true);
which forces the creation of SSLSocket objects that will work only for authenticated clients each time an accept() occurs.
The second change is the setting of the javax.net.ssl.trustStore parameter on the command line. This needed to be done because, otherwise , the server would have tried using one of the default trust store locations and would have been unable to validate the client's certificate chain.
On the client side, because you are now creating your own SSLContext , there is considerably more code than before, but in some ways it works nearly the same way. Most of the changes are restricted to the addition of the createSSLContext() method. The use of the SSLSocketFactory is still the same; it's just the underlying context that it is created with that is different.
Initially, the createSSLContext() method creates a KeyManagerFactory of the SunX509 type using the default provider and initializes it using a keystore created from the client.p12 file. Next, an SSLContext object, sslContext , is created for the TLS version 1 protocol using the default provider, and the init() method is called on sslContext , passing it a KeyManager array generated by the KeyManagerFactory object mgrFact and two null parameters.
As the other two parameters passed to sslContext.init() are null , you have created an SSLContext object that uses your provided KeyManager objects and uses the system default TrustManager objects and a system default SecureRandom object. This is the reason why the javax.net.ssl.trustStore system property still needs to be set on the command line(the SSLContext object that is being created still needs to use the default TrustManager object and the default TrustManager object requires the property to be set. Otherwise, it will be retrieving the trust anchor used to validate the certificate path from the wrong keystore.
The answer to avoiding use of the system property is to create your own TrustManager objects. To do this, you need to use the TrustManagerFactory class.
The TrustManagerFactory Class
The javax.net.ssl.TrustManagerFactory class is used to create objects that implement the javax.net.ssl.TrustManager interface. Like other security-related classes, it is created using the getInstance() pattern and follows the usual rules with regard to provider precedence when getInstance() is called, and the provider is not explicitly specified along with the algorithm requested. The standard algorithms to use with the getInstance() method are SunX509 or SunPKIX .
Aprovider can be specified to getInstance() explicitly as either a String representing the provider's name or an object representing the provider's implementation. If the algorithm requested is not available, the method will throw a NoSuchAlgorithmException. In the event the provider is specified explicitly, the method will throw an IllegalArgumentException . If the provider parameter is null or if the getInstance() method that takes a provider name is used and the provider cannot be found, the method will throw a NoSuchProviderException .
The class has only a few methods on it, and as you will see in the example following the method descriptions, the use of the factory object is the same as for the KeyManagerFactory .
TrustManagerFactory.init()
There are two init() methods on the TrustManagerFactory class. Both methods serve the purpose of providing the TrustManagerFactory with a source for the trust anchors required for allowing the SSLContext holding TrustManager objects produced by this factory to create and authenticate the entity at the other end of the connection it is attached to.
The first init() method simply takes a KeyStore object and uses any trusted certificates stored in it as trust anchors that it will recognize when the other party in the connection presents their credentials. The method will throw a KeyStoreException if there is a problem extracting the certificates from the passed-in keystore.
The second init() method takes an object implementing the ManagerFactoryParameters interface. As mentioned earlier, this is a simple marker interface, but in the case of the TrustManagerFactory class, there is a parameters object defined called CertPathTrustManagerParameters that is also in the javax.net.ssl package. The CertPathTrustManagerParameters class allows you to pass CertPathParameters for use by the TrustManager that will be created by the TrustManagerFactory object. This can be quite useful if you want to rely on something other then the default certificate path validation performed by the TrustManager either because you want to use OCSP or your certificates are carrying locally defined extensions, which are marked as critical.
TrustManagerFactory.get Algorithm()
The getAlgorithm() method returns a String representing the algorithm name for this TrustManagerFactory object. This will be the same as the algorithm parameter passed to TrustManagerFactory.getInstance() .
TrustManagerFactory.get DefaultAlgorithm()
The static getDefaultAlgorithm() returns a String representing the default algorithm name for TrustManagerFactory objects created using the JVM.
The default algorithm can be changed by setting the value of the ssl.TrustManagerFactory.algorithm security property in the java.security file. Alternately, it can be changed at runtime by calling the Security.setProperty() method with the property name and the desired algorithm.
TrustManagerFactory.get TrustManagers()
The getTrustManagers() method returns an array of objects implementing the TrustManager interface, one for each type of trust information the TrustManagerFactory object was initialized with.
The TrustManager interface is a marker interface with no methods on it, and what actual implementations are returned in the array generated by getTrustManagers() depends on the underlying provider. In the case of SSL based on X.509, the standard JSSE TrustManagerFactory will return objects implementing the javax.net.ssl.X509TrustManager interface. As you will see in the example that follows, the situation with TrustManager objects is the same as with KeyManager objects ”the important thing about the return value from the getTrustManagers() method is that it is compatible with the SSLContext object being used.
Try It Out: Using the TrustManagerFactory
This example includes the use of a TrustManagerFactory object in the initialization of the SSLContext object. As you can see from the code, all the functional changes are in the createSSLContext() method.
package chapter10; import java.io.FileInputStream; import java.security.KeyStore; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManagerFactory; /** * SSL Client with client-side authentication. */ public class SSLClientWithClientAuthTrustExample extends SSLClientExample { /** * Create an SSL context with both identity and trust store */ static SSLContext createSSLContext() throws Exception { // set up a key manager for our local credentials KeyManagerFactory mgrFact = KeyManagerFactory.getInstance("SunX509"); KeyStore clientStore = KeyStore.getInstance("PKCS12"); clientStore.load( new FileInputStream("client.p12"), Utils.CLIENT_PASSWORD); mgrFact.init(clientStore, Utils.CLIENT_PASSWORD); // set up a trust manager so we can recognize the server TrustManagerFactory trustFact = TrustManagerFactory.getInstance("SunX509"); KeyStore trustStore = KeyStore.getInstance("JKS"); trustStore.load( new FileInputStream("trustStore.jks"), Utils.TRUST_STORE_PASSWORD); trustFact.init(trustStore); // create a context and set up a socket factory SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init( mgrFact.getKeyManagers(), trustFact.getTrustManagers(), null); return sslContext; } public static void main(String[] args) throws Exception { SSLContext sslContext = createSSLContext(); SSLSocketFactory fact = sslContext.getSocketFactory(); SSLSocket cSock = (SSLSocket)fact.createSocket( Utils.HOST, Utils.PORT_NO); doProtocol(cSock); } }
As with the last example, you should run this example using two windows in the same temporary directory you set up before. Once again, each command should be entered on one line; any line breaks are due to the formatting requirements of this book.
The first thing to do is to start the server as before, using the following command:
java -Djavax.net.ssl.keyStore=server.jks -Djavax.net.ssl.keyStorePassword=serverPassword -Djavax.net.ssl.trustStore=trustStore.jks chapter10.SSLServerWithClientAuthExample
This is the same as before, which should not be any surprise, because you have only needed to change the client.
Next, you should run the client in the other window using
java chapter10.SSLClientWithClientAuthTrustExample
Because the SSLContext is now initialized with a TrustManager object as well as a KeyManager object, you no longer have to set the JVM properties on the command line.
You will then see this in the server window:
session started. session closed.
and the server will exit.
In the client window, you will see
Hello World!
and the client will exit.
This indicates that the client has successfully connected, identified itself, and validated the server.
How It Works
As you can see from the highlighted code in createSSLContext() , creating and using the TrustManagerFactory object is virtually the same as for the KeyManager . The main difference is that there is no need to provide a password to the init() method, because only trusted certificate entries are extracted from the KeyStore parameter passed in.
The introduction of the TrustManagerFactory object on the client side now means that the only system default the SSLContext is relying on is the SecureRandom implementation it is using. This frees you from having to set any system properties on the client side to get the client to run, because the SSLContext now has all the information it needs to provide identification information for its end of the link and to identify other entities connecting to it.
The SSLContext provides an environment in which an SSL connection can be established, but when a connection is in use, it doesn't provide any information as to who connected or what cipher suite is being used. This information is associated with the SSL session.
Managing SSL Session Information
In addition to connections having a context associated with them, the JSSE also makes it possible to get access to and manage session information that is related to the connection taking place. This is done via objects that implement the SSLSession interface.
The SSLSession Interface
Objects implementing the javax.net.ssl.SSLSession interface are not created directly, but are returned by the getSession() method on SSLSocket . As you can see from the method descriptions that follow, an object implementing SSLSession acts not only as a carrier of connection information but also allows for the session to be invalidated and for other objects to be associated with the session if need be.
SSLSession.get CipherSuite()
The getCipherSuite() method returns a String representing the name of the SSL cipher suite, which is in use with all connections in the session.
SSLSession.get CreationTime()
The getCreationTime() method returns a long representing the time at when this SSLSession object was created. The time value is in milliseconds since midnight, January 1, 1970 UTC.
SSLSession.getId()
The getId() method returns a byte array representing the identifier that has been assigned to the session this SSLSession object represents.
SSLSession.get LastAccessedTime()
The getLastAccessedTime() method returns a long representing the last time this SSLSession object was accessed by the session-level infrastructure. As with getCreationTime() , the value is in milliseconds since midnight, January 1, 1970 UTC.
SSLSession.get LocalCertificates()
The getLocalCertificates() method returns an array of Certificate objects representing the certificate chain sent to the peer during handshaking. If no certificate chain was sent the method returns null .
SSLSession.get LocalPrincipal()
The getLocalPrincipal() method returns a Principal representing the principal that was used to identify this end of the connection to the peer. If no principal was provided, this method returns null . If the cipher suite in use is X.509-certificate based, the return value can be cast to X500Principal .
SSLSession.get PeerCertificates()
The getPeerCertificates() method returns an array of Certificate objects representing the certificate chain that you received from the peer during handshaking.
The method will throw an SSLPeerUnverifiedException if the peer did not authenticate itself during handshaking or if the cipher suite being used is not based on X.509 certificates but is instead based on another mechanism, such as that used by Kerberos.
SSLSession.get PeerHost()
The getPeerHost() method returns a String representing the hostname or the Internet address of the peer. The method will return null if the information is not available.
The return value of this method is not authenticated, so it should be considered to be a hint as to the identity of the peer's host.
SSLSession.get PeerPort()
The getPeerPort() method returns the port number being used on the peer. The method will return ˆ’ 1 if the information is not available. The method is only available in JDK 1.5 or later.
Like getPeerHost() , the return value of this method is not authenticated, so it should be considered to be a hint.
SSLSession.get PeerPrincipal()
The getPeerPrincipal() method returns a Principal object representing the principal that was used to identify the peer during handshaking. If the cipher suite used is based on the use of X.509 certificates, the object returned can be cast to X500Principal . The method is only available in JDK 1.5 and later.
The method will throw an SSLPeerUnverifiedException if the peer did not authenticate itself during handshaking.
SSLSession.get Protocol()
The getProtocol() method returns a String representing the standard name of the protocol that will be used for all connections associated with this SSLSession object.
SSLSession.get SessionContext()
The getSessionContext() method returns a javax.net.ssl.SSLSessionContext object that provides additional context information specific to this session. In the case where the environment does not have the session context available, the method will return null .
If an SSLSessionContext object is returned, you can use it for finding out and setting the session timeout, finding out and setting the number of sessions that can be cached in the context, and finding out the IDs of all the sessions associated with the context.
SSLSession.invalidate()
The invalidate() method invalidates the session. In this case that means that future connections will not be able to resume or join the session; however, any existing connection using the session will be able to continue until the connection is closed.
SSLSession.isValid()
The isValid() method returns a boolean value that will be true if it is possible to resume or join a connection to the session, false otherwise . The method is only available in JDK 1.5 or later.
SSLSession.putValue()
The putValue() method takes two parameters, a String and an Object , with the String representing a name that the Object parameter is to be stored under. The object that has been stored can be retrieved later using the getValue() method. The getValue() method takes a single String as a parameter representing an object name previously stored by putValue() and returns an Object that is associated with that name, or null if there isn't one. Objects can be removed using the removeValue() method.
A String array representing the names of all the objects that have been stored in the session can be retrieved from the SSLSession object using the getValueNames() method. If no Objects are stored in the session, the method will return a zero length array.
The putValue(), getValue() , and removeValue() methods will all throw an IllegalArgumentException if any of the parameters passed to them are null .
As you can see, an object implementing SSLSession can carry any information that you want to associate with a session, as well as the basic information about the parties involved in the SSL connection the session object represents. The next example shows how a session object can be used to further check the identity of a client that has connected to a server.
Try It Out: Using SSLSession
This example uses the information about a client's principal in an SSLSession object to restrict connections that can be made to the server to only those where the principal the client is using is the one belonging to the example end entity certificate. The example also incorporates the use of the TrustManagerFactory and KeyManagerFactory classes, which you have seen in the client-side programs.
package chapter10; import java.io.FileInputStream; import java.security.KeyStore; import java.security.Principal; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManagerFactory; import javax.security.auth.x500.X500Principal; /** * Basic SSL Server with client authentication and id checking. */ public class SSLServerWithClientAuthIdExample extends SSLServerExample { /** * Check that the principal we have been given is for the end entity. */ static boolean isEndEntity(SSLSession session) throws SSLPeerUnverifiedException { Principal id = session.getPeerPrincipal(); if (id instanceof X500Principal) { X500Principal x500 = (X500Principal)id; return x500.getName().equals("CN=Test End Certificate"); } return false; } /** * Create an SSL context with the identity and trust stores in place */ static SSLContext createSSLContext() throws Exception { // set up a key manager for our local credentials KeyManagerFactory mgrFact = KeyManagerFactory.getInstance("SunX509"); KeyStore serverStore = KeyStore.getInstance("JKS"); serverStore.load( new FileInputStream("server.jks"), Utils.SERVER_PASSWORD); mgrFact.init(serverStore, Utils.SERVER_PASSWORD); // set up a trust manager so we can recognize the server TrustManagerFactory trustFact = TrustManagerFactory.getInstance("SunX509"); KeyStore trustStore = KeyStore.getInstance("JKS"); trustStore.load( new FileInputStream("trustStore.jks"), Utils.TRUST_STORE_PASSWORD); trustFact.init(trustStore); // create a context and set up a socket factory SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init( mgrFact.getKeyManagers(), trustFact.getTrustManagers(), null); return sslContext; } public static void main(String[] args) throws Exception { // create a context and set up a socket factory SSLContext sslContext = createSSLContext(); SSLServerSocketFactory fact = sslContext.getServerSocketFactory(); SSLServerSocket sSock = (SSLServerSocket)fact.createServerSocket(Utils.PORT_NO); sSock.setNeedClientAuth(true); SSLSocket sslSock = (SSLSocket)sSock.accept(); sslSock.startHandshake(); // process if principal checks out if (isEndEntity(sslSock.getSession())) { doProtocol(sslSock); } } }
Now that both ends of the link are setting up their own SSLContext objects, you no longer need to use system properties. You will still need to run the example in the same directory as you have with the previous ones, preferably using two windows , but in this case you can start the server with just
java chapter10.SSLServerWithClientAuthIdExample
and run the last client you used with
java chapter10.SSLClientWithClientAuthTrustExample
You will then see this in the server window:
session started. session closed.
and the server will exit.
In the client window, you will see
Hello World!
and the client will exit.
Both of these indicate that everything has worked successfully.
How It Works
You are already familiar with the contents createSSLContext() method from the client examples. The only change from its design on the server side is that it is now using server.jks rather than the client.p12 file, because you now need the server's credentials, not the client's. As with the client, creating your own SSLContext in the server frees you from having to use command-line property setting to provide local identity information and a trust store.
Ordinarily, the SSL handshaking is done when an attempt is made to use a stream off an SSLSocket . In this case, you want to establish the session before anything is sent to the other end so that you can check the principal being used to identify the client. The call to the startHandshake() method forces this to take place.
When startHandshake() returns, you are able to get a session object for the connection. This is obtained using SSLSocket.getSession() , and the SSLSession object is passed to the isEndEntity() method. This checks the principal of the peer to see that it is the principal for the certificate you expected(the example end entity certificate. If the principal is correct, the method returns true and the protocol proceeds; if the method returns false , the server quietly exits. If you've run the correct client, the method will return true and the client will receive the "Hello World!" message. It is also worth running the client from the first example ("Try It Out: A Basic SSL Client and Server") to see how the server reacts.
Dealing with HTTPS
The most common experience most users of the Internet have with SSL is via HTTPS. HTTPS stands for Hypertext Transport Protocol (Secure) and is simply HTTP done over an SSL connection.
Just as Java provides the HttpURLConnection class in java.net for dealing with regular HTTP connections, the JSSE provides a class for dealing with HTTPS connections ” HttpsURLConnection .
The HttpsURLConnection Class
The javax.net.ssl.HttpsURLConnection class is an extension of HttpURLConnection and has some extra methods to make retrieving some of the SSL- related information about the connection more accessible, as well as providing for the use of a specific socket factory and doing hostname verification against the host the URL points to. Like its parent class, HttpsURLConnection objects are not created directly but are created using the openConnection() method on java.net.URL .
I'll describe the SSL specific methods here; however, it would also be worth having a look at the JavaDoc for the parent class HttpURLConnection if you are not already familiar with it. You will see in the next example that, as it was with the SSLSocket class compared to the Socket class, the actual mechanics of doing the connection are the same as for the parent class.
HttpsURLConnection.get CipherSuite()
The getCipherSuite() method returns a String representing the name of the cipher suite that is in use for this connection.
This method will throw an IllegalStateException if the method is called before a connection has been established.
HttpsURLConnection.get HostnameVerifier()
The getHostnameVerifier() method is used to return the HostnameVerifier that will be used to verify the hostname of the server against the credentials it provides when the connect() method on the object is invoked. You'll look at HostnameVerifier in more detail in the next section.
The method has a corresponding set() method, setHostnameVerifier() , which can be used to pass your own HostnameVerifier . After the set() method is called, any calls to connect() will verify the hostname of the server using the new HostnameVerifier object.
HttpsURLConnection.get LocalCertificates()
The getLocalCertificates() method returns an array of Certificate objects representing the certificate chain, if there was one, that was sent to the server when the connection was established. In the event no certificate chain was sent, a null is returned. As you have already seen from the discussion of SSLContext , it is possible to have more than one possible certificate chain that can be used when connections are established. This method will return the chain that was actually used.
This method will throw an IllegalStateException if the method is called before a connection has been established.
HttpsURLConnection.get LocalPrincipal()
The getLocalPrincipal() method returns the principal that was sent to the server when the connection was set up. In the case of JDK 1.4 and later, you can normally cast this value to an X500Principal for an X.509-based cipher suite.
This method will throw an IllegalStateException if the method is called before a connection has been established.
HttpsURLConnection.get PeerPrincipal()
The getPeerPrincipal() method returns the principal that the server sent back to you when the connection was set up. In the case of JDK 1.4 and later, you can normally cast this value to an X500Principal for an X.509-based cipher suite.
The method will throw an SSLPeerUnverifiedException if no principal for the server has been correctly established and an IllegalStateException if the method is called before a connection has been established.
HttpsURLConnection.get ServerCertificates()
The getServerCertificates() method returns an array of Certificate objects representing the certificate chain for the server taking part in this session.
The method will throw an SSLPeerUnverifiedException if no principal for the server has been correctly established and an IllegalStateException if the method is called before a connection has been established.
HttpsURLConnection.get SSLSocketFactory()
The getSSLSocketFactory() method is used to return the SSLSocketFactory that will be used to create a connection when the connect() method on the object is invoked.
The method has a corresponding set() method, setSSLSocketFactory() , which can be used to pass your own SSLSocketFactory for the SSLContext you want to use. After the set() method is called, any calls to connect() will use the new SSLSocketFactory object.
HttpsURLConnection.set DefaultSSLSocketFactory()
Initially an HttpsURLConnection is created with a default SSLSocketFactory . The setDefaultSSLSocketFactory() method is a static method on the HttpsURLConnection class that allows you to provide your own SSLSocketFactory that will be used to create connections rather than the default one. After this method is called, new instances of HttpsURLConnection will be created using the SSLSocketFactory object that was passed in. There is also a static method, getDefaultSSLSocketFactory() , which returns the current SSLSocketFactory being used.
HttpsURLConnection.set DefaultHostnameVerifier()
The setDefaultHostnameVerifier() method is a static method on the HttpsURLConnection class that allows you to specify your own HostnameVerifier implementation that overrides the default one. After this method is called, new instances of HttpsURLConnection objects will be created using the passed-in HostnameVerifier . There is also a static method, getDefaultHostnameVerifier() , which returns the current verifier object.
The HostnameVerifier Interface
The first thing you need to be aware of if you are dealing with the HttpsURLConnection class and using your own certificates is the javax.net.ssl.HostnameVerifier interface.
The javax.net.ssl.HostnameVerifier interface has a single boolean method on it called verify() , which is passed a String representing the hostname the URL connection handler found and an SSLSession object representing the session that is being created. If the method returns true , the URL connection handler will assume everything is okay; if the method returns false , an exception will be thrown warning that the HTTPS hostname is wrong.
When a connection is established, the default HostnameVerifier implementation verifies the name of the host against the common name field (CN) in the subject DN for the end entity certificate the host presented. If the common name value and the hostname differ , the connection is terminated and an exception is thrown. In some cases, especially in intranet environments, this behavior is too restrictive , so the answer is to provide your own implementation of HostnameVerifier and pass it to the setHostnameVerifier() method on the HttpsURLConnection class. You will see an example of this next.
Try It Out: Using HttpsURLConnection and HostnameVerifier
Because you are now using HTTP as the protocol, you need different client and server programs. These can still take advantage of the methods you have already developed for creating SSLContext objects, but you need to provide extra code to support the different protocol.
The first step is to provide a client program that will also deal with the fact the common name in the certificate is not what is expected. As mentioned earlier, in a real situation, it would be the name of the host the server process is running on. Here is the code for the client, including its HostnameVerifier implementation:
package chapter10; import java.io.InputStream; import java.net.URL; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.security.auth.x500.X500Principal; /** * SSL Client with client-side authentication. */ public class HTTPSClientExample extends SSLClientWithClientAuthTrustExample { /** * Verifier to check host has identified itself using "Test CA Certificate". */ private static class Validator implements HostnameVerifier { public boolean verify(String hostName, SSLSession session) { try { X500Principal hostID = (X500Principal)session.getPeerPrincipal(); return hostID.getName().equals("CN=Test CA Certificate"); } catch (Exception e) { return false; } } } public static void main(String[] args) throws Exception { SSLContext sslContext = createSSLContext(); SSLSocketFactory fact = sslContext.getSocketFactory(); // specify the URL and connection attributes URL url = new URL("books/1/274/1/html/2/https://"+ Utils.HOST + ":" + Utils.PORT_NO); HttpsURLConnection connection = (HttpsURLConnection)url.openConnection(); connection.setSSLSocketFactory(fact); connection.setHostnameVerifier(new Validator()); connection.connect(); // read the response InputStream in = connection.getInputStream(); int ch = 0; while ((ch = in.read()) >= 0) { System.out.print((char)ch); } } }
Next, you need to provide a simple HTTPS server. Here's a simple one that accepts any request it gets and always responds with a "Hello World!" page:
package chapter10; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.security.Principal; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; /** * Basic SSL Server with optional client authentication. */ public class HTTPSServerExample extends SSLServerWithClientAuthIdExample { /** * Read a HTTP request */ private static void readRequest(InputStream in) throws IOException { System.out.print("Request: "); int ch = 0; int lastCh = 0; while ((ch = in.read()) >= 0 && (ch != ' ' && lastCh != ' ')) { System.out.print((char)ch); if (ch != ' ') lastCh = ch; } System.out.println(); } /** * Send a response */ private static void sendResponse(OutputStream out) { PrintWriter pWrt = new PrintWriter(new OutputStreamWriter(out)); pWrt.print("HTTP/1.1 200 OK "); pWrt.print("Content-Type: text/html "); pWrt.print(" "); pWrt.print("
"); pWrt.print("
"); pWrt.print("Hello World! "); pWrt.print("
"); pWrt.print(" "); pWrt.flush(); } public static void main(String[] args) throws Exception { SSLContext sslContext = createSSLContext(); SSLServerSocketFactory fact = sslContext.getServerSocketFactory(); SSLServerSocket sSock = (SSLServerSocket)fact.createServerSocket(Utils.PORT_NO); // client authenticate where possible sSock.setWantClientAuth(true); for (;;) { SSLSocket sslSock = (SSLSocket)sSock.accept(); try { sslSock.startHandshake(); } catch (IOException e) { continue; } readRequest(sslSock.getInputStream()); SSLSession session = sslSock.getSession(); try { Principal clientID = session.getPeerPrincipal(); System.out.println("client identified as: " + clientID); } catch (SSLPeerUnverifiedException e) { System.out.println("client not authenticated"); } sendResponse(sslSock.getOutputStream()); sslSock.close(); } } }
Starting the server is a simple matter of running:
java chapter10.HTTPSServerExample
Once the server is running, you can try running the client program as follows :
java chapter10.HTTPSClientExample
and you will see the following output in the server window:
Request: GET / HTTP/1.1 client identified as: CN=Test End Certificate
and the following in the client window:
Hello World!
showing that the "Hello World!" page has been served correctly to an authenticated client.
Next, try using your favorite browser, pointing it at the URL https://localhost:9020 . If the browser supports SSL, it will prompt you to see whether you accept the certificate being offered by the server program. Respond in the affirmative and your browser should respond by displaying "Hello World!" in a page, showing that the server program is also accepting connections from clients that are not offering client-side authentication.
How It Works
There is quite a bit of new code here; in the case of the server, most of it is related to the mechanics of dealing with HTTP, so I'll start by looking at how the SSL-related code in the server works first.
The main driver in the server commences by creating an SSLContext object as you have done previously and using it to create an SSLServerSocketFactory , which is then used to create an SSLServerSocket . The server socket is then configured to produce sockets that will authenticate the client by calling SSLServerSocket.setWantClientAuth() with the value true . After that the program loops , accepting connections, attempting a handshake, reading the incoming request, printing whether the client is validated , and then sending back the "Hello World!" page.
On the client side, the main driver also begins by creating an appropriate SSLContext object and then a socket factory from it. A URL object is then created with the URL pointing at the server process and specifying HTTPS as the protocol. Next, a HttpsURLConnection object is created by calling URL.openConnection() , and it is set up to use the locally created socket factory and a custom object implementing the HostnameVerifier interface. Finally, the connect() method is called on the HttpsURLConnection object and the response is read in from the server and printed along the way.
The SSLSocketFactory you pass in and the HostnameVerifier implementation are both used when the connect() method is called. As you can probably guess, the SSLSocketFactory is simply used to provide a socket to use for the connection. The verifier is called once the name of the host has been resolved and, providing the method returns true , the call to connect() will finish without an exception being thrown. In the same way you checked the client identity in the last example, the Validator class simply checks the peer principal to see whether it is the one you expect using the SSLSession object that is made available to it.
A useful exercise would be to comment-out the use of the Validator class and see how the client reacts. You should see that it rejects the server, because the common name in the certificate and the name of the server host do not match.
Summary
In this chapter, you looked at how to use the JSSE to create SSL and TLS clients and servers in Java. You should now have a good understanding of the API and how to use it, as well as some idea of what is actually taking place when the SSL/TLS protocol handshake is taking place.
Over the course of this chapter, you learned
- How the basics of the SSL protocol work
- How to create a basic SSL client and server to secure communications
- How to use client-side authentication with SSL
- How to use an SSL session information
Finally, you can make use of HTTPS, configuring your own SSL contexts using the KeyManagerFactory and TrustManagerFactory classes, as well as using the HostnameVerifier interface if you need to.
This brings the book to an end. At this point you should have a good grounding in many of the Java APIs related to cryptography and secure communications, as well as some ideas as to how to use them. Cryptography and security are constantly evolving areas, however, so always remember the title of this book started with the word "beginning," and good luck and best wishes with the rest of your journey.
Exercises
1. |
What system properties are available to configure the key and trust managers for default SSL socket and server socket factories? Which of these properties has default values and what are they? |
|
2. |
Most Java keystore types allow access to trusted certificate entries without the need to specify a password for loading the keystore file. Why is it important to use a password when loading a keystore with trusted certificate entries? |
|
3. |
What class do you use to provide your own KeyManager and TrustManager objects and create your own SSLServerSocketFactory and SSLSocketFactory objects that will be configured using them? |
|
4. |
Usually, an SSL handshake is done when one of the streams involved in an SSL link is written to. This might mean that exceptions thrown during handshaking are hidden beneath other exceptions. What method do you need to use to see explicit handshake exceptions when an SSL connection is initiated? |
|
Answers
1. |
What system properties are available to configure the key and trust managers for default SSL socket and server socket factories? Which of these properties has default values and what are they? The key manager properties are javax.net.ssl.keyStore, javax.net.ssl.keyStorePassword, javax.net.ssl.keyStoreProvider, and javax.net.ssl.keyStoreType . The trust manager properties are javax.net.ssl.trustStore, javax.net.ssl.trustStorePassword, javax.net.ssl.trustStoreProvider, and javax.net.ssl.trustStoreType . If the javax.net.ssl.trustStore property is not set, the system will look in the JVM's security directory for the file jssecacerts . If jssecacerts does not exist, the system will use the file cacerts . If the javax.net.ssl.keyStoreType or javax.net.ssl.trustStoreType properties are not set, they default to the return value of KeyStore.getDefaultType() . |
2. |
Most Java keystore types allow access to trusted certificate entries without the need to specify a password for loading the keystore file. Why is it important to use a password when loading a keystore with trusted certificate entries? Providing the password will force an integrity check on the file and help detect any tampering that might have occurred. |
3. |
What class do you use to provide your own KeyManager and TrustManager objects and create your own SSLServerSocketFactory and SSLSocketFactory objects that will be configured using them? The javax.net.ssl.SSLContext class. |
4. |
Usually, an SSL handshake is done when one of the streams involved in an SSL link is written to. This might mean that exceptions thrown during handshaking are hidden beneath other exceptions. What method do you need to use to see explicit handshake exceptions when an SSL connection is initiated? The SSLSocket.startHandshake() method. |