7
votes

I'm getting the following error

sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

when connecting to google maps geocoding API. I am able to reproduce the error in a simple Main program. Here's how to reproduce it with this test program:

import javax.net.ssl.*;
import java.net.*;
import java.io.*;

public class Main {

    public static void main(String[] args) {
        try {
            String httpsURL = "https://maps.googleapis.com/maps/api/geocode/json?address=49+874%2Cla+plata%2Cbuenos+aires%2Cargentina&sensor=false&key=AIzaSyAJ1QS0C6KjiWajwxx4jUb_Jz0b8lBZyyE";
            URL myurl = new URL(httpsURL);
            HttpsURLConnection con = (HttpsURLConnection) myurl.openConnection();
            InputStream ins = con.getInputStream();
            InputStreamReader isr = new InputStreamReader(ins);
            BufferedReader in = new BufferedReader(isr);
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                System.out.println(inputLine);
            }
            in.close();
        } catch (IOException ex) {
            System.err.println(ex.getMessage());
        }
    }
}

Saved as Main.java Compile it

javac Main.java

Run it

java Main

I get the normal result (json response is printed).

But if I create a TrustStore with a certificate from here: https://www.clic.gob.ar/ I downloaded the SSL certificate and saved it as an X.509 PEM file named clic.gob.ar

Create a new TrustStore named keystorefede.jks

keytool -import -file clic.gob.ar -alias clicCert -keystore keystorefede.jks

I gave it password tompass. I can list it

keytool -list -keystore keystorefede.jks -storepass tompass

Tipo de Almacén de Claves: JKS Proveedor de Almacén de Claves: SUN

Su almacén de claves contiene 1 entrada

cliccert, 01/08/2014, trustedCertEntry, Huella Digital de Certificado (SHA1): 15:3B:67:EE:51:C9:F2:CF:68:7C:24:51:A4:B6:6E:AE:EA:61:D5:B5

Now run the same program using the trustStore

java -Djavax.net.ssl.trustStore=/home/fede/keystorefede.jks -Djavax.net.ssl.trustStorePassword=tompass Main sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

This happens with Java 8 and also Java 7.

java version "1.8.0_11"

Java(TM) SE Runtime Environment (build 1.8.0_11-b12)

Java HotSpot(TM) 64-Bit Server VM (build 25.11-b03, mixed mode)

The problem was first discovered inside a web application running inside Tomcat. The certificate has to be in a TrustStore in Tomcat's command line for another request; the trust store has nothing google related, just one certificate. If I add Google's certificate to the trust store then the problem is solved, but this is not a proper solution. Google does not accept geocoding requests over http using an API key. It the -Djavax.net.ssl.trustStore overriding all the root CA's Java knows?

2

2 Answers

15
votes

The javax.net.ssl.trustStore property, indeed, overrides all known certificates of the JVM with the one you provide.

By default, the JVM comes with a trustStore prepopulated with a fairly decent number of well known authorities (the Oracle JVM stores it in JAVA_HOME/jre/lib/security/cacerts).

This keyStore will be used as the default by the JSSE (Java Secure Socket Extension) by default to validate SSL handshakes.

The javax.net.ssl.trustStore environnement variable overrides this default location, meaning none of its content's are relevant any more.

Going forward, you have a few solutions:
One is : you build your own JKS containing everything you need.
Second is : you add certificates to your JVM's default file.
Third is : you code.

Getting your own SSL Context "by hand" ?

Sockets that underly HTTPURLConnection are made out of SocketFactory instances. When HTTPS is involved, what happens is that you need to initialize your own SSLSocketFactory with wathever certificate/private keys are needed for your call, and associate the SocketFactory with the HTTPURLConnection before connecting it : see http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/HttpsURLConnection.html#setSSLSocketFactory%28javax.net.ssl.SSLSocketFactory%29

This works like this. First, you need to load your KeyStore (JKS file containing your certificate, exception handling cut for shortening) :

InputStream keyStoreStream = ... // Wherever it is
KeyStore ks = KeyStore.getInstance("JKS"); // or "PKCS12" for pfx/p12
ks.load(is, password);

Once you have a KeyStore instance, you can build a "TrustManager" that will use any certificates declared as trusted in the Keystore as valid trust anchors.

TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); // PKIX
tmf.init(yourKeyStore); // if you pass null, you get the JVM defaults
                        // which is CACerts file or javax.net.ssl.trustStore

You can do the same for your SSL KeyManagerFactory (if you use 2 way SSL), the pattern is exactly the same. Once you have TrustManagerFactory and KeyManagerFactory instances, you are ready to build a SSLSocketFactory.

  SSLContext sslCtx = SSLContext.getInstance("TLS");
  sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
  SSLSocketFactory sslSF = sslCtx.getSocketFactory();

At this point, you can do

  URL url = new URL("https://test.com/test");
  URLConnection conn = url.openConnection();
  if(conn instanceof HttpsURLConnection) {
    ((HttpsURLConnection) conn).setSSLSocketFactory(sslSF);
  }
  conn.connect();
0
votes

The application doesn't find the right truststore. You can define it that way: System.setProperty("javax.net.ssl.trustStore", ".\\src\\truststore.jks"); System.setProperty("javax.net.ssl.trustStorePassword", "psw123");