Monday, January 24, 2011

Avoiding the "javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed" error

Please note: this post focuses on the standard Java https implementation. If you are using the Apache HttpClient library, please see this post.

The SSLHandshakeException is thrown by java when the host you are trying to contact doesn't have a valid SSL certificate for that hostname. Most of the time this is very useful, since it means something on that host is wrong (the certificate has expired, the machine you're contacting is not who it is pretending to be etc...). However, in development mode you often don't want to pay for a "real" certificate, signed by a CA (certificate authority) like Verisign. You will then use a self-signed certificate, which gets rejected by java. It's for these cases that we're going to build a workaround. Please note that you should probably not use this code in a production environment. If you do, there's no reason to use https, since you're bypassing its functionality and you might just as well stick to http.

The first thing we need to do is create a custom TrustManager for SSL. SSL uses a protocol called X.509 (see http://en.wikipedia.org/wiki/X.509 for more info on this). We will build a TrustManager that trusts all servers:
X509TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}

@Override
public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException {

}

@Override
public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException {
}
};
As you can see, the checkXXXTrusted() methods throw Exceptions when something is wrong. We never throw an exception, effectively trusting all hosts.

The next thing we'll need to do is use this TrustManager on an SSLContext. An SSLContext (http://download.oracle.com/javase/1.5.0/docs/api/javax/net/ssl/SSLContext.html) is a factory class that is used to create socket factories, which in their turn create the actual ssl sockets used to communicate with the server. Here's how we do this:
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[] { tm }, null);
SSLContext.setDefault(ctx);

There now remains one more thing to be done: set a custom HostnameVerifier. A HostnameVerifier (http://download.oracle.com/javase/1.5.0/docs/api/javax/net/ssl/HostnameVerifier.html) is a class that makes sure the host you are contacting doesn't use a spoofed URL. We will again build a HostnameVerifier that trusts all hosts:

HttpsURLConnection conn = (HttpsURLConnection) new URL("https://serverAddress").openConnection();
conn.setHostnameVerifier(new HostnameVerifier() {

@Override
public boolean verify(String paramString, SSLSession paramSSLSession) {
return true;
}
});


Again, this HostnameVerifier will trust all hosts.

Putting all our code together, the final class will look like this:
public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException {
X509TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}

@Override
public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException {

}

@Override
public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException {
}
};
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[] { tm }, null);
SSLContext.setDefault(ctx);  
HttpsURLConnection conn = (HttpsURLConnection) new URL("https://serverAddress").openConnection();
conn.setHostnameVerifier(new HostnameVerifier() {

@Override
public boolean verify(String paramString, SSLSession paramSSLSession) {
return true;
}
});
conn.connect();

}

One final note: I prefer the way the Apache HttpClient library handles this. In the HttpClient library you can make a clean separation between the ssl verification logic and the code that does the actual work. This allows you to easily remove the code in the production environment or to use a switch between the development and production environment. This is much harder in the plain java version, since the code is more entangled. See this post for how to do this with the Apache HttpClient.

13 comments:

  1. [...] Avoiding the “javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: ...ICEFaces 2.0 charts migrationEmbedding fonts into PDF generated by JasperReportsPayPal Mass Payments using HttpClient 4Slow ICEFacesAvoiding the “javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated” with HttpClientSetting the notifyURL for the PayPal IPNPayPal Instant Payment Notification (IPN) Servlet with HttpClient 4 [...]

    ReplyDelete
  2. Thanks for sharing this!
    One question:
    Will this trust all servers ONLY in this class? Or does it affect everything running in the same JVM?

    ReplyDelete
  3. Hi Arne,

    This will affect all newly created SSL Sockets in the JVM. If you only want to apply it to one connection, you can use the setSSLSocketFactory method on the HttpsURLConnection class instead. I don't think there's a way to just tie it to one class.

    ReplyDelete
  4. Thanks for your answer. Thats exactly what i'm looking for. I have one connection i'd want to bypass the certificate validation.
    I took a look at the setSSLSocketFactory and the SSLSocketFactory, but with my limited skills im unable to pinpoint exactly what needs to be set for it to do what i want.
    If you'd like to point me even further in the right direction, that would be great!

    Thanks

    ReplyDelete
  5. Sure, it's actually just one line that needs to be changed. The line

    SSLContext.setDefault(ctx);

    needs to be removed, since this sets the default for ALL new sockets.
    You will need to add the following line instead:

    conn.setSSLSocketFactory(ctx.getSocketFactory());

    this will get a socket factory from the context you initialized and apply it only to that one connection. Here's the final code:

    public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException {
    X509TrustManager tm = new X509TrustManager() {
    @Override
    public X509Certificate[] getAcceptedIssuers() {
    return null;
    }

    @Override
    public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException {

    }

    @Override
    public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException {
    }
    };
    SSLContext ctx = SSLContext.getInstance("TLS");
    ctx.init(null, new TrustManager[] { tm }, null);
    HttpsURLConnection conn = (HttpsURLConnection) new URL("serverAddress:port").openConnection();
    conn.setSSLSocketFactory(ctx.getSocketFactory());
    conn.setHostnameVerifier(new HostnameVerifier() {

    @Override
    public boolean verify(String paramString, SSLSession paramSSLSession) {
    return true;
    }
    });
    conn.connect();

    }

    ReplyDelete
  6. Thanks Mathias!
    This works perfectly.

    ReplyDelete
  7. Wonderful! Works perfect! I can't thank you enough for this!

    ReplyDelete
  8. Hi,

    I am facing big problem with this HTTPS. I am very new to the field i am not aware of HTTPs more.
    Basically my requirement is i need to send a request server which is running on HTTPs port with some parameters the server will give response as part of response headers. i need to read the headers and do the further process.

    Can you help me to fix the issue. Iam always getting
    javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: error.

    Thanks & Regards,
    kishore

    ReplyDelete
  9. Hi,
    Can a server be used to act as both client and server ie I want the server to send the request from a client to another server with different certificate.Is this possible?

    Regards,
    B.Prakash

    ReplyDelete
  10. Thanks for sharing these helpful points and great knowledge with us. I'm glad you provided helpful illustrations of most points.Thanks for the detailed guidance.
    website design

    ReplyDelete
  11. this can be resolved in two ways: the client trust all certificates or server-side add a certificate, the specific cause analysis and solutions see: http://www.trinea.cn/android/android-java-https-ssl-exception-2/

    ReplyDelete
  12. Your blog has given me that thing which I never expect to get from all over the websites. Nice post guys!

    Web Design

    ReplyDelete