Thursday, February 16, 2012

Configuring two-way SSL authentication on Tomcat Configuring two-way SSL authentication on Tomcat


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
      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
      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.