Using two-way SSL authentication it is not only the client verifying the server's identity, but the server also authenticates the client. In this article we will configure Tomcat to use two-way authentication by executing the following steps:
- Generate a self-signed server-certificate on the server
- Download the server-certificate to the client
- Import the server-certificate to the client's Java-keystore
- Generate a self-signed client-certificate on the client
- Upload it to the server
- Import the client-certificate to the server's Java-keystore
- Configure Tomcat to use this keystore, and configure to require two-way authentication.
Both the server and the client will use self-signed certificates in our example; the purpose is only the mutual authentication and the encryption over the wire. This way we can secure Tomcat's Manager application, and we can even configure Maven to deploy to this server at the end of the build process automatically.
Let's get started!
On the server: generate the self-signed server-certificate
Let's summarize the steps we will take:
• Delete the possibly existing key-store from previous runs (reproducibility is important ;-))
• Generate a certificate
• Export it
• Move it to an appropriate location where it can be downloaded
• Generate a certificate
• Export it
• Move it to an appropriate location where it can be downloaded
The script below accomplishes all the tasks above. Don't forget to update the SERVER and PASSWORD environment variables at the beginning.
cd ~ SERVER=ec2-79-125-40-221.eu-west-1.compute.amazonaws.com PASSWORD=password sudo rm -f /usr/share/tomcat6/server.keystore sudo rm -f /usr/share/tomcat6/client.cer sudo rm -f /home/ec2-user/client.cer sudo su -c "keytool -genkeypair -alias serverkey -keyalg RSA -dname \"CN=$SERVER,OU=Techsoft Centre,O=Sony,L=Zaventem,ST=Vlaams-Brabant,C=BE\" -keystore /usr/share/tomcat6/server.keystore -keypass $PASSWORD -storepass $PASSWORD" tomcat sudo su -c "keytool -exportcert -alias serverkey -file /usr/share/tomcat6/server.cer -keystore /usr/share/tomcat6/server.keystore -storepass $PASSWORD" tomcat sudo mv /usr/share/tomcat6/server.cer /home/ec2-user
To check the contents of the keystore, you can use
sudo su -c "keytool -list -keystore /usr/share/tomcat6/server.keystore -storepass $PASSWORD" tomcat
The output should be similar to this:
Keystore type: JKS Keystore provider: SUN Your keystore contains 1 entry serverkey, 19-Dec-2010, PrivateKeyEntry, Certificate fingerprint (MD5): B5:B5:B1:BA:B3:B0:BE:B2:B7:BF:B9:B9:B3:B6:BB:B2
On Windows client: generate a certificate for the server
Let's continue our journey on the client. The steps to take:
1. Download the server’s public key generated in the preceding step
2. Install it
3. Generate a key-pair (this will authenticate the client) for the server
4. Export the public key to a certificate
5. Upload it
6. Export the client's private key to a file
7. Upload to the server since we have OpenSSL there to process the key
2. Install it
3. Generate a key-pair (this will authenticate the client) for the server
4. Export the public key to a certificate
5. Upload it
6. Export the client's private key to a file
7. Upload to the server since we have OpenSSL there to process the key
Steps 6 and 7 may require some more explanation. Since the certificate generated by keytool will be used by browsers to connect to Tomcat, it has to be exported in a way browsers can understand it. The OpenSSL tool can help us to accomplish this goal (it can generate .P12 files), but unfortunately it is not installed on Windows by default. But we have it on the Amazon Linux image; to avoid the hassle of downloading and installing OpenSSL on Windows, we can upload the private key to the server to process it with OpenSSL. But before uploading, we have to acquire it somehow from the java keystore - an official procedure only exists to export the public key part, but not the private key.
To export the private key, we'll need a small java application, DumpPrivateKey.java:
public class DumpPrivateKey { public static void main(String[] args) throws Exception { final String keystoreName = args[0]; final String keystorePassword = args[1]; final String alias = args[2]; java.security.KeyStore ks = java.security.KeyStore.getInstance("jks"); ks.load(new java.io.FileInputStream(keystoreName), keystorePassword.toCharArray()); System.out.println("-----BEGIN PRIVATE KEY-----"); System.out.println(new sun.misc.BASE64Encoder().encode(ks.getKey(alias, keystorePassword.toCharArray()).getEncoded())); System.out.println("-----END PRIVATE KEY-----"); } }
A compilation step will be also be needed with javac so that we can execute the code. Then we'll upload the exported private-key to the server for processing.
The script to execute all the steps (including generating and compiling the small java-code above) above:
set SERVER=ec2-79-125-40-221.eu-west-1.compute.amazonaws.com set PASSWORD=password if exist client.keystore ( del client.keystore ) if exist server.cer ( del server.cer ) if exist client.cer ( del client.cer ) "C:\Program Files (x86)\PuTTY\pscp.exe" ec2-user@%SERVER%:server.cer . keytool -importcert -alias serverkey -noprompt -file server.cer -keystore client.keystore -storepass %PASSWORD% keytool -genkeypair -alias clientkey -keyalg RSA -dname "CN=philatelistclient,OU=Techsoft Centre,O=Sony,L=Zaventem,ST=Vlaams-Brabant,C=BE" -keypass %PASSWORD% -keystore client.keystore -storepass %PASSWORD% keytool -list -keystore client.keystore -storepass %PASSWORD% keytool -rfc -exportcert -alias clientkey -file client.cer -keypass %PASSWORD% -keystore client.keystore -storepass %PASSWORD% "C:\Program Files (x86)\PuTTY\pscp.exe" client.cer ec2-user@%SERVER%: echo public class DumpPrivateKey { > DumpPrivateKey.java echo public static void main(String[] args) throws Exception { >> DumpPrivateKey.java echo final String keystoreName = args[0]; >> DumpPrivateKey.java echo final String keystorePassword = args[1]; >> DumpPrivateKey.java echo final String alias = args[2]; >> DumpPrivateKey.java echo java.security.KeyStore ks = java.security.KeyStore.getInstance("jks"); >> DumpPrivateKey.java echo ks.load(new java.io.FileInputStream(keystoreName), keystorePassword.toCharArray()); >> DumpPrivateKey.java echo System.out.println("-----BEGIN PRIVATE KEY-----"); >> DumpPrivateKey.java echo System.out.println(new sun.misc.BASE64Encoder().encode(ks.getKey(alias, keystorePassword.toCharArray()).getEncoded())); >> DumpPrivateKey.java echo System.out.println("-----END PRIVATE KEY-----"); >> DumpPrivateKey.java echo } >> DumpPrivateKey.java echo } >> DumpPrivateKey.java "%JAVA_HOME%\bin\javac" -classpath . DumpPrivateKey.java "%JAVA_HOME%\bin\java" -classpath . DumpPrivateKey client.keystore %PASSWORD% clientkey > clientkey.pkcs8 "C:\Program Files (x86)\PuTTY\pscp.exe" clientkey.pkcs8 ec2-user@%SERVER%: del client.cer del clientkey.pkcs8 del DumpPrivateKey.class del DumpPrivateKey.java del server.cer
Windows does not have scp on its own, so we are using PuTTY's pscp here to do the secure copy. If cygwin is installed (and its bin folder is in the PATH), the "C:\Program Files (x86)\PuTTY\pscp.exe" can be replaced simply by scp.
On OSX/Linux client:
If you happen to use Mac OSX or Linux on the client, the following script is the bash-equivalent of the one above:
SERVER=ec2-79-125-40-221.eu-west-1.compute.amazonaws.com PASSWORD=password rm -f client.keystore rm -f server.cer rm -f client.cer scp ec2-user@$SERVER:server.cer . keytool -importcert -alias serverkey -noprompt -file server.cer -keystore client.keystore -storepass $PASSWORD keytool -genkeypair -alias clientkey -keyalg RSA -dname "CN=client,OU=Java Notes,O=Java Notes,L=Zaventem,ST=Vlaams-Brabant,C=BE" -keypass $PASSWORD -keystore client.keystore -storepass $PASSWORD keytool -list -keystore client.keystore -storepass $PASSWORD keytool -exportcert -rfc -alias clientkey -file client.cer -keypass $PASSWORD -keystore client.keystore -storepass $PASSWORD scp client.cer ec2-user@$SERVER: rm -f DumpPrivateKey.java echo 'public class DumpPrivateKey {' >> DumpPrivateKey.java echo ' public static void main(String[] args) throws Exception {' >> DumpPrivateKey.java echo ' final String keystoreName = args[0];' >> DumpPrivateKey.java echo ' final String keystorePassword = args[1];' >> DumpPrivateKey.java echo ' final String alias = args[2];' >> DumpPrivateKey.java echo ' java.security.KeyStore ks = java.security.KeyStore.getInstance("jks");' >> DumpPrivateKey.java echo ' ks.load(new java.io.FileInputStream(keystoreName), keystorePassword.toCharArray());' >> DumpPrivateKey.java echo ' System.out.println("-----BEGIN PRIVATE KEY-----");' >> DumpPrivateKey.java echo ' System.out.println(new sun.misc.BASE64Encoder().encode(ks.getKey(alias, keystorePassword.toCharArray()).getEncoded()));' >> DumpPrivateKey.java echo ' System.out.println("-----END PRIVATE KEY-----");' >> DumpPrivateKey.java echo ' }' >> DumpPrivateKey.java echo '}' >> DumpPrivateKey.java javac DumpPrivateKey.java java DumpPrivateKey client.keystore $PASSWORD clientkey > clientkey.pkcs8 scp clientkey.pkcs8 ec2-user@$SERVER:
On the server: import the client's certificate
Let's continue on the server with the following steps:
- Generate the PKCS12 certificate for the browser from the private key uploaded by the client in the preceding step
- Fix permissions & move the client’s certificate to a place where the tomcat user can reach it easily
- Delete the possibly existing client-key from the server's keystore
- Install the client’s certificate to the server's java keystore
- Clean up: remove the temporary files used during the process.
The script:
openssl pkcs12 -export -in client.cer -inkey clientkey.pkcs8 -password pass:$PASSWORD -out client.p12 sudo chown tomcat:tomcat ~/client.cer sudo mv ~/client.cer /usr/share/tomcat6 sudo su -c "keytool -delete -alias clientkey -keystore /usr/share/tomcat6/server.keystore -storepass $PASSWORD -noprompt" tomcat sudo su -c "keytool -importcert -alias clientkey -file /usr/share/tomcat6/client.cer -keystore /usr/share/tomcat6/server.keystore -storepass $PASSWORD -noprompt" tomcat sudo rm /usr/share/tomcat6/client.cer
Since we were using the tomcat user to execute, if we wouldn’t have specified a keystore path, it would have been generated in tomcat's home folder, /usr/share/tomcat.
On the client: import the certificate to the browser
Back to the client, we’ll have to import the PKCS12 file client.p12 into the browser so that it can authenticate itself to Tomcat. To download the file, you can use
scp ec2-user@java-notes.com:client.p12 .
Start Firefox and import the generated PKCS12 file as a Personal key via Options->Advanced->Encryption->View Certificates->Your Certificates->Import... To use Chrome or Internet Explorer, just right click the .P12 file and install the certificate to Windows' own certificate store.
Don't lose this .P12 file, and don't disclose it: this is your key to your server, after the next step Tomcat will not allow HTTPS connections from browsers where this key is not imported into the certificate store.
On the server: configure Tomcat's HTTPS connector
Now we have insert the following line in server.xml, inside the section :
From a script, this can be done with the help of sed:
sudo sed -i 's// \n \n/' /usr/share/tomcat6/conf/server.xml
Please note: since we didn't specify a path for the keystore-file, it will be loaded from tomcat user's home directory, /usr/share/tomcat6. If on your server tomcat complains at startup not being able to reach this file, please specify an absolute path.
To require HTTPS (and thus our .P12 file to be installed) for clients, the web-applications' web.xml file has to be updated by including the following XML-snippet:
CONFIDENTIAL
In our next article, we'll update Tomcat's Manager application to mandate the client to have our key for administration/deployment.