The JRE comes preloaded with a set of trusted root authorities, but if you are working with self-signed certificates, or SAN server certificates that were signed using your own Certificate Authority then you are going to need to add these certificates to your trusted keystore.
If your Java application attempts to communicate via TLS to a remote host that does not have a trusted chain of security you will get the all too famous “SSLHandshakeException: PKIX path building failed” exception. At this point you have a couple of options:
- Bypass all security checks by injecting a custom X509TrustManager that allows all communication
- Add the certificates you want to trust into the TrustManager keystore
Bypassing security at this level is not a good idea. It’s like telling your browser that you will never care about HTTPS certificates, so from now on just show the green icon in the address bar no matter what. It is best to address security from day one, and not as a future feature.
In this article I will lead you through installing a self-signed as well as CA signed certificate into the Trust Manager keystore so that TLS communication to remote sites is handled correctly and securely.
Overview
When the JVM communicates to a secure site via TLS, the certificates to that remote site are evaluated by the TrustManager. It consults its trust keystore which is by default found in the file, “$JAVA_HOME/jre/lib/security/cacerts”.
It does not inherit any trusted root certificates from the OS level, this “cacerts” file is the global system of record for this JVM. You can view the certs for this preexisting keystore at the console using the command:
$ cd $JAVA_HOME/jre/lib/security $ keytool -list -v -keystore cacerts -storepass changeit
Much like the set of OS level root certificates, this list is fairly extensive (100+). It represents all the global Certificate authorities that are trusted by the Java packagers.
Unless a Java developer or user goes out of their way to set a new trust keystore using System properties or custom code, this is the keystore that will be used to evaluate remote certificate trust.
But there is a way to override this behavior at runtime by setting a JVM System property with the value of a file path to use:
System.setProperty("javax.net.ssl.trustStore","/tmp/keystore.jks"); System.setProperty("javax.net.ssl.trustStorePassword","changeit");
This will then use the keystore file found at “/tmp/keystore.jks” instead of the standard “jre/lib/security/cacerts” file. In this way, you can have your JVM process use a custom keystore in case you don’t want to modify it globally for any other applications.
My github project “javahttpstest” that I will be describing later in this article has another mechanism for loading a custom TrustManager where the keystore can be loaded from the classpath.
Regardless of the TrustManager’s backing store (JRE keystore, file path, or classpath), in the end we need to get our untrusted certs into this keystore file. This is done using the same “keytool” utility used earlier to list the certs.
Using commands with syntax like below, we can add any CA, intermediate, or self-signed certs that need to be trusted into the keystore file. You must start by importing the root, then move up the intermediate chain toward the server certificate.
$ keytool -import -trustcacerts -keystore <keystorefile> -alias <myalias> -file <mycert> -storepass <password>
Prerequisites
What we need to do is generate a SAN certificate for “mydomain.com” as well as its subdomains “*.mydomain.com”, and then have HAProxy deliver these certificates when requests are made.
In a previous article, I describe step-by-step how to use OpenSSL to create your own CA, and then use it to sign a SAN server certificate. Follow the instructions which include generating a .pem compatible with HAProxy for mydomain.com and *.mydomain.com. This “mydomain-ca-full.pem” file should be placed into the “/etc/pki/tls/certs” directory.
In a previous article I described step-by-step how to install HAProxy on Ubuntu for TLS communication. Follow those instructions, replacing “FQDN.pem” in haproxy.cfg for “mydomain-ca-full.pem”.
Finally, modify the local “hosts” file so that clients pointing to https://test.mydomain.com or https://mydomain.com go to the local HAProxy instance. On Windows, this is “c:\windows\system32\drivers\etc\hosts” and on Linux, “/etc/hosts”.
<IPADDRESS> test.mydomain.com mydomain.com
In order to move on you must meet the following criteria.
Browser access to test.mydomain.com
You should be able to point your browser at https://test.mydomain.com. It will show a warning because it is untrusted, but once you accept these warnings you should get the simple “OK” page from HAProxy coming from file ‘200.http’
curl access to test.mydomain.com
You should be able to get back the same “OK” html page from curl at the console as long as you specify “–insecure” to ignore the untrusted certs.
$ curl --insecure https://test.mydomain.com
openssl access to test.mydomain.com
You should be able to get an SSL handshake and the expected non-trusted certificates when pulling the site via openssl:
$ echo QUIT | openssl s_client -connect test.mydomain.com:443 -showcerts
When these criteria are met, you are ready to following along with the following sections.
Test Project
On github, I’ve created a Java project called javahttpstest that can assist with our exploration of certs and Java keystores. Let’s build it, and run a quick test that should be successful.
$ sudo apt-get install git maven -y # For Ubuntu 14.04 $ sudo apt-get install openjdk-7-jdk # For Ubuntu 16.04 $ sudo apt-get install openjdk-8-jdk $ git clone https://github.com/fabianlee/javahttpstest.git $ cd javahttpstest $ mvn test
The output will contain lines at the top saying:
Trust keystore: JVM default Default URL: https://www.google.com Setting up SSLContext using default JVM implementation with no overrides
This test retrieves “https://www.google.com” by default, and uses the global JRE’s cacerts trust store. You should see output listing Google’s root certificate authority, intermediate certs, and server certificate. And at the end, a truncated version of the web page content is displayed.
Now, run the same test again but try to pull from “https://test.mydomain.com”.
$ mvn test -DURL=https://test.mydomain.com
As expected, you will get the following error:
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Adding certs to a custom Trust keystore
In order to get a successful test for “https://test.mydomain.com”, we have to add our CA to the trust keystore. For any server certificate with a possible chain of intermediate certs and a root CA cert, its is the root and the intermediate certs that all need to be trusted (not the server cert itself).
For a self-signed certificate, we have to import the server certificate because it essentially is the root.
But instead of modifying the JVM’s global trust keystore in “jre/lib/security/cacerts”, we will instead create a local copy of the trust keystore in the current directory called “keystore.jks”. We will make our modifications there, and point our JVM instance to that keystore using standard Java system properties.
There are multiple ways to accomplish this. We could manually copy over “cacerts” to the local directory as “keystore.jks”, and then copy over the “ca.pem” that we created during our cert creation. Then running keytool to import:
$ keytool -import -trustcacerts -keystore keystore.jks -alias mycaroot -file ca.pem -storepass changeit
We could also use OpenSSL to grab the certificates, copying and pasting the CA section that has boundaries on “–BEGIN CERTIFICATE–” and “–END CERTIFICATE–” and saving it as ca.pem. Then it would just be a matter of running the ‘keytool -import’ command above.
$ echo QUIT | openssl s_client -connect mydomain.com:443 -showcerts
Finally, I have also provided a slightly modified version of InstallCert.java, a program written a long time ago by Sun. Running this program will grab the certs from a remote host, ask which you want to install, and then create a local keystore.jks file that contains the entry.
I’m going to recommend this method since it shortens our manual steps and I’ve already given you the long explanation above in case you want to manually perform the operation.
$ mvn exec:java -Dexec.mainClass=javahttpstest.InstallCert -Dexec.args="test.mydomain.com:443"
You will see output similar to:
Server sent 2 certificate(s): 1 Subject CN=mydomain Issuer CN=myca sha1 37 44 69 4a 1f f9 b8 5c df 22 e0 cb 9a d5 74 0c 02 e4 a7 f7 md5 43 47 1e 1b ad 54 1e cc d4 a0 a5 b4 e9 a7 8c ff 2 Subject CN=myca Issuer CN=myca sha1 af f0 81 b8 9e 23 58 bc f4 09 c3 91 8e 10 14 a7 3b 10 8c a1 md5 3f 18 6a fc ec 3c a5 06 c5 60 d9 e6 1d b6 16 d4 Enter certificate to add to trusted keystore or 'q' to quit: [1]
Press “2” beside the “myca” certificate which is the root. The application should say on the last line that it added the certificate to the keystore ‘keystore.jks’. In our case, we only have one cert that needs to be trusted, the root. If this was a self-signed certificate we would need to trust the single certificate presented.
If you have a root and multiple intermediates, it is important that you import the root first, then import up the chain of intermediates. The server cert does not need to be imported. The reason why the server certificate is imported for simple self-signed certs is because it is also the root.
Notice you now have a “keystore.jks” file in the current directory with all the certs from the global JVM plus ours appended. Let’s validate that our CA cert is there:
$ keytool -list -v -keystore keystore.jks -storepass changeit | grep myca -A 5
Now let’s run our test again:
$ mvn test -DURL=https://test.mydomain.com
It failed again! The reason why is that by default it is still using the global trust keystore. We need to use a Java System property to tell it to use ‘keystore.jks’ in the current directory.
$ mvn test -DURL=https://test.mydomain.com -Djavax.net.ssl.trustStore=keystore.jks
You should now see a successful run. It will show the JVM protocols/ciphers, then the test.mydomain.com certificate and our root certificate, then the simple html OK page returned from HAProxy.
Trust keystore from classpath
There are many deployment scenarios where it is difficult to know the absolute path where the keystore lives (inside jar, deployed in container). For these cases, I have written a TrustManager that initializes from a file on the classpath.
Run the below commands and the output should be exactly the same as our successful ‘mvn test’ run above.
$ mvn package -DskipTests=true $ java -classpath .:target/javahttpstest-0.0.1-SNAPSHOT.jar -DURL=https://test.mydomain.com -Dclasspath.trustStore=/keystore.jks javahttpstest.TestHTTPS
It may not appear much different, but by specifying the “-Dclasspath.trustStore” property, we can now place the file anywhere in the external classpath or jar’s classpath and it will be picked up. The absolute file path is not necessary.
REFERENCES
https://docs.oracle.com/javase/7/docs/technotes/tools/windows/keytool.html (Java7 doc)
https://www.sslshopper.com/article-most-common-java-keytool-keystore-commands.html (common commands)
https://info.ssl.com/how-to-install-a-certificate-on-java-based-web-servers/
https://blogs.oracle.com/jtc/installing-trusted-certificates-into-a-java-keystore
https://www.sslsupportdesk.com/java-keytool-commands/
https://github.com/robinhowlett/everything-ssl/blob/master/src/test/java/com/robinhowlett/ssl/EverythingSSLTest.java (custom keystore with SSLContext)
https://stackoverflow.com/questions/30121510/java-httpsurlconnection-and-tls-1-2 (Java, setting keystore from classpath and setting trustmanager)
http://olafsblog.sysbsb.de/testing-ssl-https-clients-with-junit-and-jetty/ (testing keystore with jetty and junit)
https://jonathanjwright.wordpress.com/2009/12/04/unit-testing-https-clients-with-a-self-signed-certificate/ (System properties ‘javax.net.ssl.trustStore’ and ‘javax.net.ssl.trustStorePassword’)
https://stackoverflow.com/questions/9210514/unable-to-find-valid-certification-path-to-requested-target-error-even-after-c (source of InstallCert.java)
NOTES
default jks password is “changeit”
default trust keystore found at: $JAVA_HOME/jre/lib/security/cacerts
keytool -list -v -keystore keystore.jks -storepass changeit
keytool -delete -alias mydomain -keystore keystore.jks -storepass changeit
keytool -import -trustcacerts -keystore keystore.jks -alias mycaroot -file ca.pem -storepass changeit
keytool -import -trustcacerts -keystore keystore.jks -alias myservercert -file mydomain.crt -storepass changeit