TL;DR

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.

Problem

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.

Certificate for google.com

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

Solution

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)

wget https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.der

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.

Additional Notes

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.

References


0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *