A Very Lightweight HTTPS Server and Client

An HTTPS server needs two stores for digital certificates:

keystore
A keystore contains digital certificates, including the certificates that an HTTPS server sends to clients during the peer authentication phase of the HTTPS handshake. When the server is challenged to establish its identity, the server can send one or more certificates for its keystore to the challenger. If peer authentication is truly mutual, then a client needs a keystore with the client’s own digital certificates, which can be sent to the server for verification.
truststore
A truststore is a keystore with a specified function: the truststore stores trusted certificates used to verify other certificates. When a host, server or client, receives a certificate to be verified, this received certificate can be compared against truststore entries. If the truststore does not contain such a certificate, the truststore may contain at least a certificate from a CA such as VeriSign, whose digital signature is on the received certificate.

Although the keystore and the truststore differ in core purpose (see Figure 6-6), one and the same file can function as both keystore and truststore, and, in development, this option is attractively simple.

A depiction of how a keystore and a truststore function

Figure 6-6. A depiction of how a keystore and a truststore function

In fleshing out these and related details about HTTPS security, the HttpsPublisher (see Example 6-3) code may be useful.

Example 6-3. The lightweight HttpsPublisher

import java.net.InetSocketAddress;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import java.security.KeyStore;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import javax.xml.ws.http.HTTPException;
import java.io.OutputStream;
import java.io.InputStream;
import java.security.SecureRandom;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpsServer;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpsParameters;
import service.IService;

public class HttpsPublisher {
    private static final int defaultPort = 3443;
    private static final int backlog = 12;
    private static final String keystore = "test.keystore";
    private IService serviceInstance;                                           1

    public static void main(String[ ] args) {
        if (args.length < 2) {
            System.err.println("Usage: java HttpsPublisher <service> <path>");
            return;
        }
        // % java HttpsPublisher myService.Service /service
        new HttpsPublisher().init(args[0],  // service name
                                  args[1]); // URI
    }
    private void init(String serviceName, String uri) {
        try {
            Class serviceClass = Class.forName(serviceName);                    2
            serviceInstance = (IService) serviceClass.newInstance();            3
        }
        catch(Exception e) { throw new RuntimeException(e); }
        HttpsServer server = getHttpsServer(uri, defaultPort);
        if (server != null) {
            server.createContext(uri);                                          4
            System.out.println("Server listening on port " + defaultPort);
            server.start();                                                     5
        }
        else
            throw new RuntimeException("Cannot create server instance.");
    }
    private HttpsServer getHttpsServer(String uri, int port) {
        HttpsServer server = null;
        try {
            InetSocketAddress inet = new InetSocketAddress(port);
            server = HttpsServer.create(inet, backlog);
            SSLContext sslCtx = SSLContext.getInstance("TLS");
            // password for keystore
            char[ ] password = "qubits".toCharArray();
            KeyStore ks = KeyStore.getInstance("JKS");
            FileInputStream fis = new FileInputStream(keystore);
            ks.load(fis, password);
            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            kmf.init(ks, password);
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
            tmf.init(ks); // same as keystore
            sslCtx.init(kmf.getKeyManagers(),
                        tmf.getTrustManagers(),
                        new SecureRandom());
            // Create SSL engine and configure HTTPS to use it.
            final SSLEngine eng = sslCtx.createSSLEngine();
            server.setHttpsConfigurator(new HttpsConfigurator(sslCtx) {
                    public void configure(HttpsParameters parms) {
                        parms.setCipherSuites(eng.getEnabledCipherSuites());
                        parms.setProtocols(eng.getEnabledProtocols());
                    }
                });
            server.setExecutor(null); // use default, hence single-threaded
            server.createContext(uri, new MyHttpsHandler(this.serviceInstance));
        }
        catch(Exception e) { throw new RuntimeException(e); }
        return server;
    }
}
// The handle method is called on a particular request context,
// in this case on any request to the server that ends with /<uri>.
class MyHttpsHandler implements HttpHandler {
    private IService service;

    public MyHttpsHandler(IService service) {
        this.service = service;
    }
    public void handle(HttpExchange ex) {
        // Implement a simple routing table.
        String verb = ex.getRequestMethod().toUpperCase();
        if (verb.equals("GET"))         service.doGet(ex);
        else if (verb.equals("POST"))   service.doPost(ex);
        else if (verb.equals("PUT"))    service.doPut(ex);
        else if (verb.equals("DELETE")) service.doDelete(ex);
        else throw new HTTPException(405);
    }
}

The HttpsPublisher can be started with a command such as:

% java HttpsPublisher service.TestService /test

The first command-line argument, service.TestService (see Example 6-4), is the fully qualified name of a RESTful service’s implementation class; the second command-line argument, in this case /test, is the URI that identifies the service. Any service deployed with the HttpsPublisher must implement the IService interface (see Example 6-5), which means that the four CRUD methods doGet, doPost, doPut, and doDelete must be defined. The HttpsPublisher declares a reference of data type IService (line 1 in the listing) and then uses the Class.forName utility to load a service class such as TestService from the filesystem (line 2) and create an instance (line 3). The IService interface thus allows the HttpPublisher to publish any service that implements the RESTful CRUD operations with the designated method names.

The HttpsPublisher sets the context for requests (line 4), which in this example means that a client must use the URI /test to hit the TestService. The publisher maintains a queue of up to backlog requests, currently set to 16, on the specified HTTPS port 3443. Finally, the start method is invoked on the HttpsServer instance (line 5), which starts the server for an indefinitely long run.

Example 6-4. The TestService published with the HttpsPublisher

package service;

import java.io.OutputStream;
import com.sun.net.httpserver.HttpExchange;

public class TestService implements IService {
    public void doGet(HttpExchange e) { respond2Client(e, "doGet"); }
    public void doPost(HttpExchange e) { respond2Client(e, "doPost"); }
    public void doPut(HttpExchange e) { respond2Client(e, "doPut"); }
    public void doDelete(HttpExchange e) { respond2Client(e, "doDelete"); }

    private void respond2Client(HttpExchange ex, String response) {
        try {
            ex.sendResponseHeaders(200, 0); // 0 == as many bytes as there are
            OutputStream out = ex.getResponseBody();
            out.write(response.getBytes());
            out.close(); // effectively ends session
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
}

Example 6-5. The IService interface

package service;
import com.sun.net.httpserver.HttpExchange;

public interface IService {
    public void doGet(HttpExchange e);
    public void doPost(HttpExchange e);
    public void doPut(HttpExchange e);
    public void doDelete(HttpExchange e);
}

Once the RESTful service class has been loaded, the HttpsServer begins the tricky security configuration. A client that opens an HTTPS connection to the HttpsServer is going to challenge this server to verify its identity, and the HttpsServer responds with a digital certificate. The core Java JDK comes with a keytool utility that can be used to create a self-signed rather than a CA-certified digital certificate. For development, the self-signed digital certificate is good enough; for production, a CA-certified digital certificate would be needed. The command:

% keytool -genkey -keyalg RSA -keystore test.keystore

eventually creates the file test.keystore, which contains an X.509 digital certificate generated using the RSA algorithm. This file is the keystore. The keytool utility is interactive, prompting the user with questions that must be answered. The first such question is about a password to protect access to the keystore. In this case, the password is qubits. For the current example, the keystore file test.keystore (the name is arbitrary) performs various functions:

  • The file acts as a regular keystore that contains the digital certificate that the HttpsPublisher sends to any challenger, in this case the HttpsClient, which is introduced shortly.
  • The file doubles as the HttpsPublisher truststore and as the HttpsClient truststore. Accordingly, the fix is in. When the HttpsPublisher sends the one and only digital certificate in the keystore test.keystore, the HttpsClient verifies this digital certificate by checking it against the very same keystore—but a keystore now functioning as a truststore. Were the HttpsPublisher to challenge the HttpsClient, the client would send the same digital certificate as its identity voucher—and the HttpsPublisher would verify this digital certificate against itself, in effect.

Using the keystore for multiple purposes simplifies the setup and keeps the focus on the technical coding details. In a production environment, of course, there would be four keystores involved in this scenario: the HttpsPublisher would have a keystore with its certificates and a truststore with the certificates that it trusts; the same would hold for the HttpsClient.

The HttpsPublisher reads into memory the digital certificate stored in the file named test.keystore. Here is a block of initialization code:

SSLContext sslCtx = SSLContext.getInstance("TLS");                      1
char[ ] password = "qubits".toCharArray();                              2
KeyStore ks = KeyStore.getInstance("JKS");
FileInputStream fis = new FileInputStream(keystore);
ks.load(fis, password);                                                 3
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, password);                                                 4
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ks); // same as keystore
sslCtx.init(kmf.getKeyManagers(),                                       5
            tmf.getTrustManagers(),
            new SecureRandom());
final SSLEngine eng = sslCtx.createSSLEngine();
server.setHttpsConfigurator(new HttpsConfigurator(sslCtx) {
   public void configure(HttpsParameters parms) {
      parms.setCipherSuites(eng.getEnabledCipherSuites());              6
      parms.setProtocols(eng.getEnabledProtocols());                    7
   }});
server.setExecutor(null); // use default, hence single-threaded         8
server.createContext(uri, new MyHttpsHandler(this.serviceInstance));    9

The SSLContext (line 1) is the central data structure, and this context supports secure (that is, TLS-based) communications from clients to the HttpsPublisher. After the password bytes are stored in an array (line 2), the contents of the keystore file, test.keystore, are loaded into memory (line 3) and a KeyManagerFactory is initialized with the contents of this keystore file (line 4). There is now an in-memory version of the file test.keystore. The TrustStoreFactory (line 5) is initialized with the contents of the very same keystore file. At this point, the SSLContext is initialized with the key managers of the keystore file and the trust managers of the truststore file, which are the same file.

The next security initialization steps involve setting the appropriate cipher suites (line 6), which are used during the handshake negotiations with a client. The security protocols that are available to the server are likewise part of the initialization (line 7). For demonstration purposes, the HttpsPublisher remains single-threaded, which explains the null in line 8. To make the publisher multithreaded, a non-null value for the Executor (that is, the thread manager) would be used. Finally, the HTTPS server specifies a handler to handle requests against the URI, in this case /test (line 9).

The setup code is tricky, but its gist can be summarized as follows:

When the setup is complete, the HttpsPublisher is ready to accept HTTPS connections from potential clients. Client requests are dispatched to an instance of the class MyHttpsHandler:

server.createContext(uri, new MyHttpsHandler(this.serviceInstance));

The constructor call to MyHttpsHandler has, as its single argument, a reference to the IService instance so that GET requests can be forwarded to the serviceInstance method doGet, POST requests to doPost, and so on. The MyHttpsHandler class implements the HttpHandler interface, which has a single method: handle. Here is the implementation:

public void handle(HttpExchange ex) {
    String verb = ex.getRequestMethod().toUpperCase();
    if (verb.equals("GET"))         service.doGet(ex);
    else if (verb.equals("POST"))   service.doPost(ex);
    else if (verb.equals("PUT"))    service.doPut(ex);
    else if (verb.equals("DELETE")) service.doDelete(ex);
    else throw new HTTPException(405); // bad verb
}

The flow of control (see Figure 6-7) is straightforward: a request targeted at the URI /test goes to the MyHttpsHandler instance, which implements the handle method. The handle method dispatches GET requests to the service’s doGet method, POST requests to the service’s doPost method, and so on. The critical point is that the communications between the HttpsPublisher and the client are over a secure HTTPS channel.

The routing in the HttpsPublisher

Figure 6-7. The routing in the HttpsPublisher

In the current implementation, the TestService instance encapsulates minimalist versions of doGet, doPost, doPut, and doDelete operations. The point of interest is the security configuration, not the actual behavior of the RESTful service. Each CRUD operation returns a string confirming that the operation has been invoked. For HTTPS requests with bodies (that is, POST and PUT), the HttpsClient (see Example 6-6) sends a short string that the service operations ignore.

Example 6-6. The sample HttpsClient against the TestService

import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import java.security.KeyStore;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import java.security.cert.X509Certificate;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.SecureRandom;

public class HttpsClient {
    private static final String endpoint = "https://localhost:3443/test/";
    private static final String truststore = "test.keystore";

    public static void main(String[ ] args) {
        new HttpsClient().runTests();
    }
    private void runTests() {
        try {
            SSLContext sslCtx = SSLContext.getInstance("TLS");
            // password for truststore (same as server's keystore)
            char[ ] password = "qubits".toCharArray();
            KeyStore ks = KeyStore.getInstance("JKS");
            FileInputStream fis = new FileInputStream(truststore);
            ks.load(fis, password);
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
            tmf.init(ks); // same as keystore
            sslCtx.init(null,                   // not needed, not challenged
                        tmf.getTrustManagers(),
                        null);                  // use default: SecureRandom
            HttpsURLConnection.setDefaultSSLSocketFactory(sslCtx.getSocketFactory());
            URL url = new URL(endpoint);
            HttpsURLConnection conn = getConnection(url, "GET");
            getTest(conn);
            conn = getConnection(url, "POST");
            postTest(conn);
            conn = getConnection(url, "PUT");
            putTest(conn);
            conn = getConnection(url, "DELETE");
            deleteTest(conn);
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private HttpsURLConnection getConnection(URL url, String verb) {
        try {
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setDoInput(true);
            conn.setDoOutput(true);
            conn.setRequestMethod(verb);
            // Guard against "bad hostname" errors during handshake.
            conn.setHostnameVerifier(new HostnameVerifier() {
                    public boolean verify(String host, SSLSession session) {
                        return host.equals("localhost"); // for development
                    }
                });
            return conn;
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void getTest(HttpsURLConnection conn) {                1
        try {
            conn.connect();
            readResponse("GET request: ", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void postTest(HttpsURLConnection conn) {               2
        try {
            conn.connect();
            writeBody(conn);
            readResponse("POST request: ", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void putTest(HttpsURLConnection conn) {                3
        try {
            conn.connect();
            writeBody(conn);
            readResponse("PUT request: ", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void deleteTest(HttpsURLConnection conn) {             4
        try {
            conn.connect();
            readResponse("PUT request: ", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void writeBody(HttpsURLConnection conn) {
        try {
            OutputStream out = conn.getOutputStream();
            out.write("foo bar baz".getBytes()); // anything will do
            out.flush();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void readResponse(String msg, HttpsURLConnection conn) {
        try {
            byte[ ] buffer = new byte[512]; // plenty for testing
            InputStream in = conn.getInputStream();
            in.read(buffer);
            System.out.println(msg + new String(buffer));
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
}

The HttpsClient (see Example 6-6) makes requests against the four CRUD operations (lines 1, 2, 3, and 4) in the TestService published with the HttpsPublisher. This client is similar in structure to the GoogleTrustingClient examined earlier except that the HttpsClient does demand a digital certificate from the HttpsPublisher and does verify this certificate against a truststore. As noted earlier, the client’s truststore is the same file, test.keystore, as the server’s keystore; hence, the verification is guaranteed to succeed. The HttpsClient reads the truststore data into memory and uses these data to initialize the all-important SSLContext. Here is the relevant code:

FileInputStream fis = new FileInputStream(truststore);
ks.load(fis, password);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ks); // same as keystore
sslCtx.init(null, // not needed, not challenged   1
            tmf.getTrustManagers(),
            new SecureRandom());

In the call to init (line 1), the first argument is null, which represents the keystore managers. The assumption is that the HttpsPublisher will not challenge the HttpsClient, which therefore does not need a keystore for the handshake. Were mutual challenge in operation, then the HttpsClient setup would be the same, with respect to the keystore and the truststore, as in the HttpsPublisher.

Of course, a non-Java client also can connect over HTTPS to the HttpsPublisher. As proof of concept, here is a very short Perl client that connects but does not bother to verify the digital certificates that the HttpsPublisher sends to the client:

use Net::SSLeay qw(get_https);
my ($type, $start_line, $misc, $extra) = get_https('localhost', 3443, '/test');
print "Type/value:  $type\n";
print "Start line:  $start_line\n";
print "Misc:        $misc => $extra\n";

The output from a sample run was:

Type/value:  doGet
Start line:  HTTP/1.1 200 OK
Misc:        CONNECTION => close

Changing from HTTP to HTTPS transport does not imperil the language neutrality and interoperability of web services—assuming, of course, that the languages used on client side and the service side include HTTPS libraries, as modern languages usually do.