JRE’s cacerts might not have the Certification Authority (CA) certificate to validate the CA that issued the certificate of the host you are calling. If so, add it.
I was trying to access an external API endpoint and unfortunately, it failed with the following error message.
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 at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296) at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1509) at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216) at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979) at sun.security.ssl.Handshaker.process_record(Handshaker.java:914) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062) at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:436) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:384) at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376)
Since it says “unable to find valid certification path to requested target” I understood that the server’s SSL certificate’s issuer seemed untrusted to the Java app.
As per the context of the development task, I was not sure whether the SSL certificate of the server is issued from a private CA. So I tried importing the public certificate of the server into cacerts assuming it will make Java trust this server. But no luck. I added this certificate to mac’s keychain and tried various things. However, I had no clue.
The real issue
After a little more reading I came across the real issue. The certificate of the server is actually issued by Let’s Encrypt (You may try it, it’s a free CA which anybody can get a free SSL certificate to your own website). My Java version is 1.8.0_73. As per the compatibility list of Let’s Enrypt following Java versions support Let’s Encrypt
- Java 7 >= 7u111
- Java 8 >= 8u101
So my Java version does not support Let’s Encrypt. Which means it does not have the knowledge to trust Let’s Encrypt.
Similarly in your setup as well, Java might not know the Root Certification Authority who issued the certificate for your server. It can be a private CA. Or it can be a self-signed one. Or you can be having the same issue as I had.
If you can load your URL in a browser, you can find out the Root CA if you press the padlock symbol on the left side of the URL bar. For chrome that’s a padlock symbol and for may other browsers as well that’s similar.
So as you see in the above image, google.com has got the certificate from “GTS CA 101” immediate CA who got it’s certificate from GlobalSign. So here the root CA is GlobalSign.
If you want to get information about a certificate in command line, if you have a .crt certificate, you can try the following which will show you the issuer, etc.:
openssl x509 -in ca.crt -text -noout
If you do not have the certificate with you, you can try the following
openssl s_client -connect <host>:<port> -showcerts
Following is a portion of the output for google.com:443
openssl s_client -connect google.com:443 -showcerts CONNECTED(00000005) depth=2 OU = GlobalSign Root CA - R2, O = GlobalSign, CN = GlobalSign verify return:1 depth=1 C = US, O = Google Trust Services, CN = GTS CA 1O1 verify return:1 depth=0 C = US, ST = California, L = Mountain View, O = Google LLC, CN = *.google.com verify return:1 --- Certificate chain 0 s:/C=US/ST=California/L=Mountain View/O=Google LLC/CN=*.google.com i:/C=US/O=Google Trust Services/CN=GTS CA 1O1
Download the .der certificate file from the Root CA (for me that is Let’s Encrypt) and add that to the cacerts file. You can usually find cacerts file in path jre/lib/security. In my mac the path was: /Library/Java/JavaVirtualMachines/jdk1.8.0_73.jdk/Contents/Home/jre/lib/security.
Download the certificate (for Let’s Encrypt)
Add it to cacerts
sudo keytool -import -alias omnivore -keystore /Library/Java/JavaVirtualMachines/jdk1.8.0_73.jdk/Contents/Home/jre/lib/security/cacerts -file lets-encrypt-x3-cross-signed.der
This will ask for a keystore password. The default password is changeit
All good. After this, I could run my application without any issues.
How to do a curl with a manually supplied CA certificate
Do it like the following.
curl -v --cacert ca-cert.pem <host>
If you need to use local private and public keys where server requires mutual authentication, do it as follows:
curl -v \ --cacert ca-cert.pem \ --key client-pvt-pem.pem \ --cert client-public.pem \ <host>
How to see the content of the cacerts file
You may need to check what CAs are already available in the cacerts file. Do it like this.
keytool -list -v -keystore /path/to/cacerts/file >> /file/path/you/need/to/export/to
Change /path/to/cacerts/file to the path of the cacerts file and give a path to a file name for /file/path/you/need/to/export/to where you need to export the CAs.
Here as well, you’ll be asked for a password which is by default changeit.