An HTTPS server needs two stores for digital certificates:
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.
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
;
![]()
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
);
![]()
serviceInstance
=
(
IService
)
serviceClass
.
newInstance
();
![]()
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
HttpsServer
server
=
getHttpsServer
(
uri
,
defaultPort
);
if
(
server
!=
null
)
{
server
.
createContext
(
uri
);
![]()
System
.
out
.
println
(
"Server listening on port "
+
defaultPort
);
server
.
start
();
![]()
}
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:
HttpsPublisher
sends to any challenger, in this
case the HttpsClient
, which is introduced shortly.
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"
);
![]()
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
());
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
));
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:
SSLContext
data structure is populated with security information, in particular the
keystore and truststore contents that the HttpsPublisher
needs to conduct the handshake with
a prospective client.
HttpsPublisher
also uses the SSLContext
to create an SSLEngine
, which manages
the cipher suites and related security protocols.
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 My
HttpsHandler
:
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.
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
)
{
![]()
try
{
conn
.
connect
();
readResponse
(
"GET request: "
,
conn
);
conn
.
disconnect
();
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
private
void
postTest
(
HttpsURLConnection
conn
)
{
![]()
try
{
conn
.
connect
();
writeBody
(
conn
);
readResponse
(
"POST request: "
,
conn
);
conn
.
disconnect
();
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
private
void
putTest
(
HttpsURLConnection
conn
)
{
![]()
try
{
conn
.
connect
();
writeBody
(
conn
);
readResponse
(
"PUT request: "
,
conn
);
conn
.
disconnect
();
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
private
void
deleteTest
(
HttpsURLConnection
conn
)
{
![]()
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
![]()
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
'
);
"Type/value: $type\n"
;
"Start line: $start_line\n"
;
"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.