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:
- 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. - 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:
- Copy and paste the code of the
ReadHttpsURL1
class into a file namedReadHttpsURL1.java
, and save this file in a directory of your choice. - Compile the
ReadHttpsURL1.java
usingjavac
. - 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 tohttps://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:
- Run the
ReadHttpsURL1
:java ReadHttpsURL1 localhost
. You will receive the same exception as the one described above. - Export the server's certificate using the following
keytool
command that says:- Export the server's certificate from the file
serverkeys
, whose alias isqusay
- Save the exported certificate in a file called
server.cert
, which will be created by thekeytool
server.cert
.Prompt> keytool -export -keystore serverkeys -alias qusay -file server.cert
Enter keystore password: hellothere Certificate stored in file <server.cert>
- Export the server's certificate from the file
- Copy the
server.cert
file to the directory whereReadHttpsURL1
is located. Use thekeytool
to create a new keystore and import the server'sserver.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 thekeytool
. 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
- 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:
- 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 aCertificateException
will be thrown. - 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 thelib/security
subdirectory of yourjava.home
directory, it will be used. - If
jssecacerts
doesn't exist, butcacerts
does (which is shipped with the J2SDK with a limited number of trusted root certificates), it will be used.
- If a trust store named
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:
- The user enters a URL and the browser retrieves it.
- The browser opens a connection to the domain name of the URL.
- The browser sends HTTP commands.
- The browser waits for an answer from the HTTP/HTTPS server.
- The browser receives an HTML reply.
- 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:
- Request
http://www.javacourses.com
, and you will see something similar to Figure 1.Figure 1: http://www.javacourses.com
- 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
- Request
https://localhost
where theHttpsServer
from Part I is running. Note, if you runQBrowser
using the commandjava QBrowser
, and the server's certificate was exported and imported to the default filejssecacerts
, it should be copied to thelib/security
subdirectory of yourjava.home
directory as explained above. If the certificates are in a different file, you can use thetrustStore
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 theHostnameVerifier
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.