I've been working on a very simple but secure web application. We're using the simplest of all Java web servers, Jetty, and configuring it for SSL programmatically. It looks something like this:
import org.mortbay.jetty.Server;
import org.mortbay.jetty.security.SslSocketConnector;
.
.
SslSocketConnector sslSocketConnector = new SslSocketConnector();
sslSocketConnector.setPort(SSL_PORT_NUMBER);
sslSocketConnector.setKeystoreType("JKS");
sslSocketConnector.setSslKeyManagerFactoryAlgorithm("SunX509");
sslSocketConnector.setNeedClientAuth(true); // this is what makes the server only talk to certain clients
sslSocketConnector.setKeystore( JKS_FILE_NAME );
sslSocketConnector.setKeyPassword(KEY_PASSWORD);
sslSocketConnector.setPassword(PASSWORD);
Server server = new Server();
server.addConnector(sslSocketConnector);
.
.
This makes Jetty use SSL. SSL means that communication is encrypted (so nobody can eavesdrop) but does not stop somebody calling the service who shouldn't.
What's more, SSL is susceptible to a Man-in-the-Middle attack. "When an encrypted connection between the two parties is established, a secret key is generated and transmitted using an asymmetric cipher... So when A negotiates an encrypted connection with B, A is actually opening an encrypted connection with the attacker, which means the attacker securely communicates with an asymmetric cipher and learns the secret key" [1]. Erickson then goes on to demonstrate such an ARP poisoning/spoofing.
To avoid this, the client needs to be given (securely) the server's certificate in advance. To prevent the world and his wife from connecting to the server, it in turn needs to be given the client's certificate in advance. Once this is done, the important thing to note here is the setNeedClientAuth method call.
Let's start by creating the server key thus:
keytool -genkey -alias server_full -keypass KEY_PASSWORD -keystore server.jks -storepass PASSWORD
And enter the relevant details.
From the generated server key file, extract the certificate:
keytool -export -alias server_full -file server_pub.crt -keystore server.jks -storepass PASSWORD
We'll use this in a moment.
Create the client key thus:
keytool -genkey -alias client_full -keypass CLIENT_KEY_PASSWORD -keystore client.jks -storepass CLIENT_PASSWORD
and enter the relevant details.
Now, from the client key file, extract the certificate:
keytool -export -alias client_full -file client_pub.crt -keystore client.jks -storepass CLIENT_PASSWORD
"A digital certificate is basically a wrapper around a public key, which includes identifying information for the party owning that key." [2]
Now, tell the server's keystore about this client's certificate with:
keytool -import -alias client_pub -file client_pub.crt -keystore server.jks -storepass PASSWORD
And, similarly tell the client's keystore about the server's certificate with:
keytool -import -alias cerver_pub -file server_pub.crt -keystore client.jks -storepass CLIENT_PASSWORD
Now, only the client can talk to the server.
What's a keystore?
"The client's store will contain the client's private and public key pair. It is called a keystore.
The server's store will contain the client's public key. It is called a truststore.
The separation of truststore and keystore is not mandatory but recommended. They can be the same physical file."[3]
One odd thing about keystores is that if keys are generated with different passwords but stored in the same key store file, you'll see this exception:
java.security.UnrecoverableKeyException: Cannot recover key
at sun.security.provider.KeyProtector.recover(KeyProtector.java:311)
at sun.security.provider.JavaKeyStore.engineGetKey(JavaKeyStore.java:121)
at sun.security.provider.JavaKeyStore$JKS.engineGetKey(JavaKeyStore.java:38)
at java.security.KeyStore.getKey(KeyStore.java:763)
at com.sun.net.ssl.internal.ssl.SunX509KeyManagerImpl.(SunX509KeyManagerImpl.java:113)
at com.sun.net.ssl.internal.ssl.KeyManagerFactoryImpl$SunX509.engineInit(KeyManagerFactoryImpl.java:48)
at javax.net.ssl.KeyManagerFactory.init(KeyManagerFactory.java:239)
.
.
This really seems impossible to work around to me. Using JAD, I decompiled Sun's class and saw:
final class SunX509KeyManagerImpl extends X509ExtendedKeyManager
{
/* member class not found */
class X509Credentials {}
SunX509KeyManagerImpl(KeyStore keystore, char ac[])
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException
{
/* 102*/ credentialsMap = new HashMap();
/* 103*/ serverAliasCache = new HashMap();
/* 104*/ if(keystore == null)
/* 105*/ return;
/* 108*/ Enumeration enumeration = keystore.aliases();
/* 108*/ do
{
/* 108*/ if(!enumeration.hasMoreElements())
/* 109*/ break;
/* 109*/ String s = (String)enumeration.nextElement();
/* 110*/ if(keystore.isKeyEntry(s))
{
/* 113*/ java.security.Key key = keystore.getKey(s, ac);
Where I have used the -lnc switch to generate line numbers. This seems to confirm my suspicions. The code iterates over all the aliases and tries to get the key but with the same password for all of them (ac[]).
The simple solution is to use a different key store file.
Miscellaneous
1. Sometimes, you need some extra information why things are going wrong. To make the Sun/Oracle classes to tell you what is going on, turn on debugging with:
System.setProperty("javax.net.debug", "ssl");
2. Use the -validity flag of keytool to set the number of days the certificate is valid for.
References
- [1] Hacking: The Art of Exploitation- Jon Erickson
- [2] http://www.ibm.com/developerworks/java/library/j-jws5/index.
- [3] http://stackoverflow.com/questions/1666052/java-https-client-certificate-authentication
- I found the blog of Dr Herong Yang extremely useful and well worth reading.