Secure Internet Programming with the Java 2 Platform, Standard Edition (J2SE) 1.4 Part II: The Client Side

By Qusay H. Mahmoud December 2002

The Java Secure Socket Extension (JSSE), which is a set of Java packages that enable secure Internet communications, is a framework and implementation of the Secure Socket Layer (SSL) version 3.0 and the Transport Layer Security (TLS) version 1.0, which is an improvement on SSL 3.0. These packages enable you, the Java developer, to develop secure network applications that feature the secure passage of data between a client and a server running any application protocol, such as HTTP, FTP, Telnet, NTTP, over TCP/IP.

In Part I (the Server Side) of this article, I presented a detailed overview of SSL and JSSE, and described how to develop server-side SSL-enabled applications. One useful application described in Part I, which will be used in this part as well, is the HTTPS server.

This article is concerned with the client-side. It provides a brief overview of JSSE, then shows you how to:

  • Use the JSSE APIs on the client-side
  • Approach developing client-side SSL-enabled applications
  • Develop simple SSL-enabled client applications
  • Export server-side certificates and import them to the client-side
  • Develop an SSL-enabled Web browser

JSSE

JSSE, which provides a framework and an implementation of the SSL 3.0 and TLS 1.0 protocols, abstracts the complex underlying cryptographic algorithms and thus minimizes the risk of creating subtle and dangerous security vulnerabilities. As you will see in this article, it makes the development of secure SSL-enabled applications quite simple by allowing you to seamlessly integrate SSL into your client applications. The JSSE framework is capable of supporting many different secure communication protocols such as SSL 2.0 and 3.0 and TLS 1.0, but J2SE v1.4 implements SSL 3.0 and TLS 1.0.

Programming Client Applications with JSSE

The JSSE APIs supplement the java.security and java.net packages by providing extended networking socket classes, trust and key managers, and a socket factory framework for encapsulating socket creation behavior. These classes are included in the packages javax.net and javax.net.ssl.

The javax.net.ssl.SSLSocketFactory class is an object factory for creating secure sockets. An instance of SSLSocketFactory can be obtained in two ways:

  1. Get the default factory by calling SSLSocketFactory.getDefault. The default factory is configured to enable server authentication only (no client authentication). Note that most e-commerce web sites do not require client authentication.
  2. Construct a new factory with specified configured behavior (this is beyond the scope of this article).

Once an instance of SSLSocketFactory has been created, you can create an instance of SSLSocket by invoking a createSocket method on the SSLSocketFactory instance. Here is an example that creates a socket connection to Sun's WWW server through the SSL port 443, which is the default port number for HTTPS.


// Get a Socket factory 
SocketFactory factory = SSLSocketFactory.getDefault(); 
 
// Get Socket from factory 
Socket socket = factory.createSocket("www.sun.com", 443); 

Working with Low-level SSL Sockets

Now, let's see a complete example of opening an SSL socket connection to an HTTPS server using low-level sockets. In this example, an SSL socket connection will be opened to an HTTPS server, and then we read the content of the default document. Code Sample 1 shows this application. The instructions that open the SSL socket are highlighted in bold. As you can see, the rest of the application is regular Java code for input/output streams.

Code Sample 1: ReadHttpsURL1


import java.net.*;
import javax.net.*;
import javax.net.ssl.*;

public class ReadHttpsURL1 {
   static final int HTTPS_PORT = 443; 

   public static void main(String argv[]) throws Exception { 
      if (argv.length != 1) { 
         System.out.println("Usage: java ReadHttpsURL1 <url>");
         System.exit(0);
      }  
       
	   
                     
		// Get a Socket factory        SocketFactory factory = SSLSocketFactory.getDefault();      

		// Get Socket from factory        Socket socket = factory.createSocket(argv[0], HTTPS_PORT);       
      BufferedWriter out = new BufferedWriter(new 
                OutputStreamWriter(socket.getOutputStream()));
      BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
      out.write("GET / HTTP/1.0\n\n");
      out.flush();

      String line;
      StringBuffer sb = new StringBuffer();
      while((line = in.readLine()) != null) {
         sb.append(line);
      }
      out.close();
      in.close();
      System.out.println(sb.toString());
   }
}
               

To experiment with this application:

  1. Copy and paste the code of the ReadHttpsURL1 class into a file named ReadHttpsURL1.java, and save this file in a directory of your choice.
  2. Compile the ReadHttpsURL1.java using javac.
  3. Run the ReadHttpsURL1 and provide the domain of an HTTPS url. Here is an example:

    Prompt> java ReadHttpsURL1 www.sun.com

    After a few seconds, you will note that tons of HTML code is displayed on your screen. Note that even though we are providing the domain www.sun.com, we are opening a connection to https://www.sun.com. This is because the port number we are using, 443, is the default port number for HTTPS.

Try another example, such as:

Prompt> java ReadHttpsURL1 www.jam.ca

This will throw the following exception. Can you guess why?


Exception in thread "main" javax.net.ssl.SSLHandshakeException:
 java.security.ce               
rt.CertificateException:
 Couldn't find trusted certificate              
at com.sun.net.ssl.internal.ssl.BaseSSLSocketImpl.a(DashoA6275)            

It didn't work for a good reason. This was caused by the remote server sending a certificate that is unknown to the client. As I mentioned in the first part of the article, when a client connects to a server, the server sends its certificate to the client for authentication. Well, in the first example, where you entered www.sun.com, that server did send its certificate, but Java checks the default certificate store and realized that the certificate was generated by one of the trusted Certificate Authorities that Java trusts by default. In the second example where you entered www.jam.ca, the certificate for that site was either self-generated or generated by a Certification Authority unknown to Java, and therefore it wasn't trusted.

Note: If the system clock is not set correctly, the perceived time may be outside the validity period of the certificate, the server will assume the certificate is invalid and therefore a CertificateException will be thrown.

In order to make that example work, you'd need to import the certificate of the Web site www.jam.ca into the store of certificates trusted by Java.

Exporting and Importing Certificates

To explain how to export and import certificates, I will use my own HTTPS server which is discussed in the first part of the article. To get started, do the following:

  1. Run the ReadHttpsURL1: java ReadHttpsURL1 localhost. You will receive the same exception as the one described above.
  2. Export the server's certificate using the following keytool command that says:
    • Export the server's certificate from the file serverkeys, whose alias is qusay
    • Save the exported certificate in a file called server.cert, which will be created by the keytool
    As you can see, I was asked to enter the password. Upon successful entry of the password, the server's certificate got exported and saved in the file server.cert.

    Prompt> keytool -export -keystore serverkeys -alias qusay -file server.cert

    
        Enter keystore password:       
    	 hellothere Certificate stored in file 
    	 <server.cert>      
                 
  3. Copy the server.cert file to the directory where ReadHttpsURL1 is located. Use the keytool to create a new keystore and import the server's server.cert certificate into it. Here is a sample command:

    Prompt> keytool -import -keystore trustedcerts -alias qusay -file server.cert

    This command produces the following output. I was asked to enter a password. This is a new password for the trustedcerts keystore. This keystore is created by the keytool. At the end of the output, I got asked if I wish to trust this certificate. My answer was yes.

    
    
    	Enter keystore password: 
    	clientpass                
        Owner: CN=localhost, OU=Training and Consulting,
    	O=javacourses.com, L=Toronto, ST=Ontario, C=CA                 
        Issuer: CN=localhost, OU=Training and Consulting,
    	O=javacourses.com, L=Toronto, ST=Ontario, C=CA                  
        Serial number: 3dcf988a               
        Valid from: Mon Nov 11 06:46:18 EST 2002 until:
    	Sun Feb 09 06:46:18 EST 2003                   
        Certificate fingerprints:                   
        MD5:37:35:4D:3A:2B:7E:B5:09:A5:41:B3:FA:E4:3C:1D:C4                   
        SHA1:CB:7C:77:36:79:A2:37:26:E2:98:61:C2:9D:10:50:69:                  
        99:F9:B9:1B                  
        Trust this certificate? [no]:        yes              
        Certificate was added to keystore       
  4. Now run the ReadHttpsURL1 and inform it where to look for certificates using the following command:

    Prompt> java -Djavax.net.ssl.trustStore=trustedcerts ReadHttpsURL1 localhost

    This will contact your HTTPS server, verify its certificate and if it is valid, will download the default page index.html.

Note: A trust manager is responsible for determining if the remote authentication credentials should be trusted. The following rules are used:

  1. If a truststore is specified by the javax.net.ssl.trustStore system property, then the trust manager will use the file provided to check for credentials. If, however, the system property exists but the file specified doesn't exist, then no truststore is utilized and a CertificateException will be thrown.
  2. If the javax.net.ssl.trustStore system property is not defined, then a default trust store is searched for:
    • If a trust store named jssecacerts exists in the lib/security subdirectory of your java.home directory, it will be used.
    • If jssecacerts doesn't exist, but cacerts does (which is shipped with the J2SDK with a limited number of trusted root certificates), it will be used.

On my Windows 2000 client machine, the java.home directory is c:\Program File\java\jre1.4.1\lib\security. In the above example, if you change the name of trustStore to jssecacerts and move it to the lib/security subdirectory, then you no longer need to specify the javax.net.ssl.trustStore property on the command line.

If you do not know your java.home, here is a simple application that allows you to find it:


public class FindJavaHome {
  public static void main(String argv[]) {
    System.out.println(System.getProperty("java.home"));
  }
}

The URL Class

The ReadHttpsURL1 in Code Sample 1 uses low-level sockets to open a connection to an SSL-enabled server. One of the disadvantages of this is that we couldn't write the URL https://www.jam.ca explicitly on the command line without having to do a lot of parsing. There is an easier way of exploiting SSL and JSSE in your client-side applications.

The java.net.URL class is capable of supporting HTTPS URLs. For example, the following snippet of code creates an HTTPS url and establishes an input stream reader:


 URL url = new URL("https://www.sun.com"); 
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream())); 
         

Isn't that easier? Well, I hope you do realize and appreciate the beauty of Java as you learn new things about it!

The ReadHttpsURL1 in Code Sample 1 can be rewritten using the URL class as shown in Code Sample 2.

Code Sample 2: ReadHttpsURL2.java


import java.net.*;
import java.io.*;

public class ReadHttpsURL2 {
   public static void main(String argv[]) throws Exception {
      if(argv.length != 1) {
         System.out.println("Usage: java ReadHttpsURL2 <https:>");
         System.exit(0);
      }

      URL url = new URL(argv[0]);
      BufferedReader in = new BufferedReader(new InputStreamReader(
                                url.openStream()));

      String line;
      StringBuffer sb = new StringBuffer();
      while ((line = in.readLine()) != null) {
         sb.append(line);
      }
      in.close();
      System.out.println(sb.toString());

   }
}

If you want to experiment with ReadHttpsURL2, the execution instructions are similar to those discussed above. Note, however, since we are using the URL class, you can specify URLs on the command line along with the name of the protocol. Here is an example:

Prompt> java ReadHttpsURL2 https://localhost

Developing an SSL-enabled Web Browser

As a complete example, let's develop an SSL-enabled web browser. A web browser works as follows:

  1. The user enters a URL and the browser retrieves it.
  2. The browser opens a connection to the domain name of the URL.
  3. The browser sends HTTP commands.
  4. The browser waits for an answer from the HTTP/HTTPS server.
  5. The browser receives an HTML reply.
  6. The browser parses the HTML and displays the rendered page.

The browser we create will support and handle any URLs such as HTTP, HTTPS, ftp, etc. Note that I parse the HTML using the kit in the class javax.swing.text.html.HTMLEditorKit, which provides support for HTML 3.2

The code for the browser, QBrowser, is shown in Code Sample 3. Note that the QBrowser is implementing the Runnable interface. I did this because this browser doesn't provide a STOP button. Therefore, if a request freezes for some reason, multiple downloads can still be instantiated concurrently as you can see from the run method.

Code Sample 3: QBrowser.java


import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class QBrowser implements ActionListener, Runnable {
   private JFrame frame;
   private JButton go;
   private JEditorPane content;
   private JTextField url;
   private JLabel statusLine;

   // default constructor
   public QBrowser () {
      buildBrowserInterface();
   }

   private void buildBrowserInterface() {
      frame = new JFrame("Q's Browser");
      // on close, exit the application using System.exit(0);
      frame.setDefaultCloseOperation (3);

      url = new JTextField("", 25);
      go = new JButton("Go Get It");
      go.addActionListener(this);

JPanel controls = new JPanel(new FlowLayout ());            
controls.add(new JLabel("URL:"));            
controls.add(url);            
controls.add(go);            
content = new JEditorPane();             
content.setEditable(false);           
// HTML text. Use the kit in the class javax.swing.text.html.HTMLEditorKit, which             
// provides support for HTML 3.2             
content.setContentType("text/html");           
content.setText("<center><h1>Q's Browser</h1><p>
 Copyright (c) 2002 Qusay H. Mahmoud</center>");    

     statusLine = new JLabel("Initialization Complete");
    
      JPanel panel = new JPanel(new BorderLayout (0, 2));
      frame.setContentPane(panel);

      panel.add(controls, "North");
      panel.add(new JScrollPane (content), "Center");
      panel.add(statusLine, "South");
      frame.pack();
      frame.setVisible(true);
   }


/**             
* You cannot stop a download with QBrowser             
* The thread allows multiple downloads to start concurrently in case a download freezes           
*/         
public void actionPerformed (ActionEvent event) {            
Thread thread = new Thread(this);             
thread.start();            
}          

   // this is the Thread's run method
   public void run () {
      try {
         String str = url.getText();
         URL url = new URL(str);
         readURL(url);
      } catch (IOException ioe) {
        statusLine.setText("Error: "+ioe.getMessage());
        showException(ioe);
      } 
   }

  private void showException(Exception ex) {
    StringWriter trace = new StringWriter ();
    ex.printStackTrace (new PrintWriter (trace));
    content.setContentType ("text/html");
    content.setText ("<h1>" + ex + "</h1><pre>" + trace + "</pre>");
  }

  /**
   * The URL class is capable of handling http:// and https:// URLs
   */
   private void readURL(URL url) throws IOException {
      statusLine.setText("Opening " + url.toExternalForm());
      URLConnection connection = url.openConnection();
      StringBuffer buffer = new StringBuffer();
      BufferedReader in=null;
      try {
        in = new BufferedReader(new 
                   InputStreamReader(connection.getInputStream()));
        String line;
        while ((line = in.readLine()) != null) {
          buffer.append(line).append('\n');
          statusLine.setText("Read " + buffer.length () + " bytes...");
        }
      } finally {
        if(in != null) in.close();
      }
      String type = connection.getContentType();
      if(type == null) type = "text/plain";
      statusLine.setText("Content type " + type);
      content.setContentType(type);
      content.setText(buffer.toString());
      statusLine.setText("Done");
   }
  
  public static void main (String[] args) {
    QBrowser browser = new QBrowser();
  }
}

The QBrowser handles both HTTP and HTTPS requests since it uses the URL class. You can test the QBrowser using both HTTP and HTTPS URLs. Here are some sample tests:

  1. Request http://www.javacourses.com, and you will see something similar to Figure 1.

    Figure 1: http://www.javacourses.com

  2. Requesting https://www.jam.ca will result in an exception being thrown. Since the certificate for this Web server is not trusted and cannot be found in the default files, it will throw an exception as shown in Figure 2.

    Figure 2: https://www.jam.ca

  3. Request https://localhost where the HttpsServer from Part I is running. Note, if you run QBrowser using the command java QBrowser, and the server's certificate was exported and imported to the default file jssecacerts, it should be copied to the lib/security subdirectory of your java.home directory as explained above. If the certificates are in a different file, you can use the trustStore option as in: java -Djavax.net.ssl.trustStore=file QBrowser. Either way, it will work and you will see the default file (index.html) as shown in Figure 3.

    Figure 3: https://localhost

The HttpsURLConnection Class

This class, which is part of the javax.net.ssl package, extends the java.net.HttpURLConnection by adding support for HTTPS-specific features. It can be used to establish secure channels through SSL/TLS sockets before requesting/receiving data. Code Sample 4 shows a mini-client downloading documents from HTTPS servers using the HttpsURLConnection class.

Code Sample 4: ReadHttpsURL3.java


 import java.io.*; import java.net.*; import javax.net.ssl.*;
         
public class ReadHttpsURL3 {            
public static void main(String[] argv) throws Exception {             
URL url = new URL(argv[0]);             
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();            
connection.setDoOutput(true);             
BufferedReader in = new BufferedReader(             
new InputStreamReader(connection.getInputStream()));            
String line;             
while ((line = in.readLine()) != null) {            
System.out.println(line);             
}             
in.close();             
}            
}          

To experiment with ReadHttpsURL3, execute as discussed above. Note, however, since you are using the URL class, you can specify URLs on the command line along with the name of the protocol. Here is an example:

Prompt> java ReadHttpsURL3 https://www.sun.com

One of the interesting features about the HttpsURLCOnnection class is that once a connection is obtained, you can configure some useful parameters, such as the HostnameVerifier, before initiating the network connection. The HostnameVerifier is an interface that declares one method public boolean verify (String hostname, SSLSession session) and it works as follows:

  • If the SSL/TLS standard hostname verification logic fails, the implementation will call the verify method of the callback class that implements the HostnameVerifier interface.
  • If the callback class determines that the hostname is acceptable, the connection should be allowed, otherwise it causes the connection to be terminated.

The policies to be followed by the callback class can either be certification-based or may depend on other authentication schemes. Here is how it can be implemented:


public class MyVerified implements HostnameVerifier {
   public boolean verify(String hostname, SSLSession session) {
      // pop up a dialog box
      // ...
      // return either true or false
   }
}

Now, this can be used as:



HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();   
 connection.setHostnameVerifier(new MyVerifier());            

Trust Managers

When an SSL client, such as a Web browser, connects to an SSL server (such as an HTTPS server), the HTTPS server presents its certificate chain to the client for authentication. The SSL specification states that the client should terminate the connection if an invalid certificate in the chain is discovered. Web browsers, such as Netscape Communicator and Microsoft Internet Explorer, ask the user whether to ignore the invalid certificate and continue up the chain to check if it is possible to authenticate the HTTPS server. This inconsistent practice is eliminated through the use of javax.net.ssl.TrustManager, which is the base interface for JSSE trust managers who are responsible for managing the trust material that is used when marking trust decisions, and for deciding whether credentials presented by a peer should be accepted. Typical trust managers support authentication based on X.509, which is a common certificate format that can be managed by J2DK's keytool, public key certificates,

The X509TrustManager Interface

The javax.net.ssl.X509TrustManager interface, which extends the general TrustManager interface, must be implemented by a trust manager when using an authentication scheme based on X.509 public key certificates. Creating your own trust manager can be done by implementing the X509TrustManager. Here is an empty implementation:


 public class MyTrustManager implements X509TrustManager
 {    MyTrustManager() { // constructor       // create/load keystore    } 
         
public void checkClientTrusted(X509Certificate chain[], 
String authType) throws CertificatException {          
}             
public void checkServerTrusted(X509Certificate chain[],
 String authType) throws CertificationException {            
// special handling such as poping dialog boxes          
}          
    public X509Certificate[] getAcceptedIssuers() {    } } 
         

In order to support X.509 authentication of remote socket peers through JSSE, an instance of the class implementing the X509TrustManager interface must be passed on to the init method of an SSLContext object, which acts as a factory for SSL socket factories. In other words, once a trust manager is created and assigned to an SSLSocket through the init method, future SocketFactories created from this SSLContext will use the new trust manager when making trust decisions. The following snippet of code demonstrates this:


X509TrustManager xtm = new MyTrustManager()
TrustManager mytm[] = {xtm};
SSLContext ctx = SSLContext.getInstance("SSL");
ctx.init(null,mytm, null );
SSLSocketFactory sf = ctx.getSocketFactory();

JSSE Debugging Utility

Sun's JSSE implementation provides dynamic debug tracing support that can be accessed using the system property javax.net.debug. This utility, which is not an officially supported feature of JSSE, allows you to see what goes on behind the scenes during the SSL communications. This utility can be used from the command line as follows:

Prompt> java -Djavax.net.debug=option[debugSpecifiers] MySSLApp

If you use the help option, it will display a list of the debug options. The current options in J2SE 1.4.1 are:


all            turn on all debugging
ssl            turn on ssl debugging
The following can be used with ssl:
        record       enable per-record tracing
        handshake    print each handshake message
        keygen       print key generation data
        session      print session activity
        defaultctx   print default SSL initialization
        sslctx       print SSLContext tracing
        sessioncache print session cache tracing
        keymanager   print key manager tracing
        trustmanager print trust manager tracing

        handshake debugging can be widened with:
        data         hex dump of each handshake message
        verbose      verbose handshake message printing

        record debugging can be widened with:
        plaintext    hex dump of record plaintext

You must specify the option ssl or all, optionally followed by debug specifiers. One or more debug specifiers can be used, and a separator such as ":" or "," is not needed but can help with readability. Here are some examples:


            Prompt> java -Djavax.net.debug=all MyApp  
   Prompt> java -Djavax.net.debug=ssl MyApp                
 Prompt> java -Djavax.net.debug=ssl:handshake:trustmanager MyApp  

         

Conclusion

This article showed how to develop secure client-side applications using the JSSE, which is a framework and implementation of the SSL protocol. The examples presented in this article show how easy it is to seamlessly integrate SSL into your client/server applications. This article presented a Web browser, the QBrowser, capable of handling both HTTP and HTTPS requests.

In QBrowser, if the server corresponding to the entered HTTPS URL doesn't have a valid certificate recognized by the Java runtime system, an exception will be thrown. As an exercise, you may want to modify the QBrowser so that it handles this exception and pops up a window asking the user whether s/he would like the certificate to be downloaded and installed. It is worth noting that the Java Plugin in 1.4.x uses the JSSE; it has its own trust manager which pops up a window if it cannot find the certificate in its trusted store.

For more information

Acknowledgments

Special thanks to Brad Wetmore and Andreas Sterbenz of Sun Microsystems whose feedback helped me improve the article.