Background
The most common scenario when using SSL/TLS is the basic handshake where the server is the only party that is authenticated with its certificate - the client remains unauthenticated. We may then connect to the server just knowing its address:
1
|
|
In this post I’m going to deal with a less popular scenario - the client-authenticated handshake - in which the client is required to present its certificate as well and use its private key.
Let’s assume our secure server is secure.server.com:443
and we already have the client’s certificate in client.crt
and the client’s private key in client.key
, both of them in the PEM format. We can again use s_client
to test the connection, but this time we need to present the certificate and private key:
1
|
|
However, things get a little bit less straightforward on the JVM. Any secure HTTP connection on the JVM, no matter which library you use, boils down to using the javax.net.ssl.HttpsURLConnection
, which is a part of the Java Secure Socket Extension (JSSE).
JSSE, key stores and trust stores
Among other stuff, JSSE has a concept of key stores and trust stores. The former are containers for keys/certificates presented to the server, the latter let the JVM know whether a given server certificate is signed by a trusted Certificate Authority (CA). The default format for both stores is JKS (which stands for Java keystore), but JSSE is also capable of reading the PKCS #12 format.
Custom key store
As you may already have guessed, in order to use the aforementioned client’s certificate and key, we need to store them in a keystore. We’ll go for the PKCS #12 format and use openssl
to do the necessary conversions:
1
|
|
Please make sure not to provide an empty password when openssl
prompts you - not only is it unreasonable from the security point of view, but it will also make mysterious NullPointerException
s fly around when you attempt to use a key store which has an empty password.
In order for the JVM to use the custom key store, you need to set the following system properties:
-Djavax.net.ssl.keyStore=keystore.p12
-Djavax.net.ssl.keyStoreType=pkcs12
-Djavax.net.ssl.keyStorePassword=<password>
where <password>
is the key store password you chose when prompted by openssl
. You may of course set those properties at runtime by calling System.getProperties().put(key, value)
(in Java) or sys.props += key -> value
(in Scala).
Provided that the certificate of secure.server.com
is signed by a trusted CA, the steps so far are enough to get up and running. However, if the server’s certificate is a self-signed one, you need an additional step, which is telling JSSE to trust the self-signed certificate.
Custom trust store
We’re going to achieve this by creating a trust store containing the certificate of the CA (the untrusted one) which signed the server’s certificate. But where do we take the CA’s certificate from? Once again openssl
comes to the rescue. After executing
1
|
|
you’re going to see - among other output - a number of certificates in the PEM format, i.e. something like:
-----BEGIN CERTIFICATE-----
(some Base64 content)
-----END CERTIFICATE-----
You’re interested in the last certificate in the sequence, which is going to be the CA’s certificate - you need to save it (including the BEGIN/END CERTIFICATE
lines) into a file, e.g. ca.crt
.
Now it’s time to decide whether you want to import the CA’s certificate into the global JSSE trust store or just to create a local trust store with a single certificate. The global trust store contains certificates of trusted CAs like VeriSign/Symantec, so it’s necessary if you want to connect to most of the well-known servers like google.com
. The tricky part is that when you tell JSSE to use a custom trust store, it won’t be using the global one anymore, so you will only be able to connect to servers whose certificates are signed by the CA in the custom trust store.
Therefore, you have three options to choose from:
Extend the global trust store by importing the untrusted CA’s certificate into it. This is the easiest solution, but you need to remember that it will affect all applications running on the given JVM, i.e. all of them will trust certificates signed by the CA in question.
Make a copy of the global trust store and import the CA’s certificate into the copy, then use the copy as a custom trust store in your application. In this case your application will be able to connect both to the well-known servers and to
secure.server.com
.Create a custom trust store with only the certificate of the untrusted CA. Your application is then only going to trust certificated signed by the selected CA and it won’t be able to make a secure connection to a well-known server like
google.com
.
Let’s now explore the above options in more detail.
1. Extending the global trust store
The global trust store is located in $JAVA_HOME/jre/lib/security/cacerts
. To import the ca.crt
into the trust store, we’re going to use JDK’s keytool
utility (if you have java
in the PATH
, you should have keytool
as well):
1 2 |
|
Note: the default password for the global trust store is changeit
(yes, not the most secure one).
Since the global trust store is used by default in a JVM application, no further configuration is needed.
2. Using an extended copy of the global trust store
First simply create a copy of the global trust store:
1
|
|
Then import ca.crt
like in the previous case (again, the default password is changeit
):
1
|
|
Finally, you need to tell the JVM to use the custom trust store by setting the following system properties:
-Djavax.net.ssl.trustStore=my-cacerts.jks
-Djavax.net.ssl.trustStoreType=JKS
-Djavax.net.ssl.trustStorePassword=changeit
3. Using a single-certificate trust store
The first step here is to create a new key store (yes, a trust store is a actually a key store):
1
|
|
The cn
value in the dname
parameter is an arbitrary name and doesn’t really matter. The same applies to the alias
parameter. And again, please remember not to set an empty password.
Then you import ca.crt
into the newly created trust store:
1
|
|
Finally, you need to tell the JVM to use the custom trust store by setting the following system properties:
-Djavax.net.ssl.trustStore=truststore.jks
-Djavax.net.ssl.trustStoreType=JKS
-Djavax.net.ssl.trustStorePassword=<password>
where <password>
is the password you chose when creating your custom trust store.
Summary
Hopefully, this post has shed some light on the not-so-common scenario of a secure JVM client authenticating itself with a certificate and private key. You should now be able to seamlessly implement this kind of authentication in your JVM application.