Complete guide on how to resolve "PKIX path building failed" issue in your Java API tests

When people say "API testing" they usually mean the API that exposes either HTTP or HTTPs interfaces to its clients. This is how (normally) REST architecture is implemented and how SOAP services work. While the first protocol does not usually cause problems, the latter one involves much more complicated procedures. This doesn’t let one execute the code that is intended to work with HTTPs straight away. In this post we’re going to see the example of HTTPs API call that encounters certificate validation issue.

You might have seen the error message like "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target". If yes, then this is the proper article for you.

Example description: Java HTTPs API test that fails to validate certificate path

In our example we’re going to make an "API call" to the endpoint "https://webelement.click" using the tool-set Java provides in java.net.* packages. This call will likely fail to validate certificate that the server sends to prove its identity. After that we’ll configure our custom trust store (I’ll explain what it is a bit later) and tell your code to use that one. Also we’re going to look at the approach how you could distribute your trust store with your tests so that make your tests less dependent on the environment with only few lines of code.

So here is the model of our API test that is trying to call API endpoint that is on top of Secure Socket Layer (now also known as Transport Layer Security) protocol.

package click.webelement.https;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

public class ApiWithSSL {

    public static void main(String[] args) {
        try {
            HttpURLConnection connection = (HttpURLConnection) new URL("https://webelement.click").openConnection();
            int responseCode = connection.getResponseCode();
            if(responseCode != 200){
                System.out.println("Test failed with actual status code: " + responseCode);
            }else{
                System.out.println("Test passed");
            }
        } catch (IOException e) {
            System.out.println("Test failed with exception: " + e.getMessage());
        }
    }

}

The test above is assumed to fail. Lets now look into the issue a bit deeper.

However it might happen that the test above will pass successfully at your machine. That might be in the case you are trying to execute it within the environment that has all the SSL harness configured to trust the certificates issues by well known certificate authorities. However if you would not have the issues with certificate validation you wouldn’t stop at this post eventually.

More often SSL things are come in place when you deal with the certificates issued by local corporate authorities.

Anyway I recommend you to keep reading the post to get better understanding on why you might go into issues and why you might not. I also recommend to amend the url of the example to the one you have the problems with so that it is more representative.

What does "PKIX path building failed" mean?

When your code attempts to call https endpoint, the remote server sends the certificate to your client that proves the server’s identity. Client needs to know if it can trust the certificate. Each certificate is digitally signed by an issuer who issued the certificate. To validate the sign client has to apply issuer’s public key to that original certificate. But how can we trust the issuer. Issuer usually has its own certificate which is to be verified with the same approach (that is called a certificate chain).

What is the general idea under https? It uses public key cryptography (also know as asymmetric cryptography) to negotiate a symmetric key that would be used to encrypt the data transferred between parties. Thus on the first stage of interaction, client needs the server’s public key to send encrypted negotiation messages to it. So in order to be sure the client is talking to the server it thinks it is, public keys are usually wrapped with the certificates. Certificate is a document that has the identity name (like webelement.click) and the public key that is certified to be associated with the name. All certificate data is computed to the hash string and digitally signed with the private key of certification authority (CA) that issued the certificate. The only way to validate the certificate is to use CA’s public key (that is also wrapped with a certificate which in turn might be signed with another authority).

Any implementation of HTTPs/SSL things implies that there is a storage of certificates somewhere that you consider trusted (such storage is called a trust store). Actually there could be several storage which you can use in different cases and for different purposes. The general rule that has to be met in order to have your code running perfectly is that at least one certificate of certificate chain has to be saved to your trust store. In Java (if you didn’t change default state) the trust store is placed in your %JRE_HOME%/lib/security/cacerts. For the majority of Java distributions, that trust store already includes the most of well known CAs' certificates so that there should no be issues with communication to most of public services over the Internet. However for intranet serivce not everything is so shiny.

The message "PKIX path building failed" means that Java ssl harness while going through (and validating) certificate chain returned from remote server, failed to reach the certificate that would exist in trust store that is currently in use.

How do I prepare a trust store so that my code trusts certificate from the service under test

We’re going to apply complex fix to our problem. First we need to prepare a trust store that would contain the certificate that our remote service provides. Follow the steps below in order to do that:

  1. Obtain a certificate file that contains certificate of your service. That can be done using one of two ways. 1) ask the devs or devops who are responsible for your server implementation and deployment. 2) Open the https endpoint in your web browser, click "lock" icon next to the address bar, follow the certificate examination dialog that would eventually bring you to the place where you could "export" or "download" the certificate provided by a server You need to eventually get either .pem or .cer file.

  2. Create some folder, get to that folder using command line. Make sure you have your %JRE_HOME%/bin folder in your PATH environment variable (the latter one is required in order to easily call keytool command from any place of your file system). Copy your certificate file to that folder.

  3. Being in that folder import your certificate to the storage (that does not yet exist) using the following command.

keytool -importcert -file your-cert-file.pem -keystore mytruststore -storetype PKCS12

Set up some password that you will be using for accessing the store from your code. Also answer "yes" when the tool ask you if you do really want to trust this certificate.

After the import will have been finished you will see that new file mytruststore appeared in the folder. That file is your new trust store that contains the certificate of your service under test.

How do I use my new trust store in my Java test

Once you have configured your new trust store you now need to tell your code somehow that it now has to use that new one. This can be done by either setting up corresponding system properties in wither a command line when you start your code from console (this is also useful when you start your code from some CI tool) or set them straight int he code. We’re going to use the latter way. The properties would be javax.net.ssl.trustStore and javax.net.ssl.trustStorePassword.

Assume that we created a trust store that is in the file mytruststore which is in the folder /home/me/ssl and the password for that store is mystorepass. Then the faulty example shown at the beginning could look like:

package click.webelement.https;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

public class ApiWithSSL {

    static {
        System.setProperty("javax.net.ssl.trustStore", "/home/me/ssl/mytruststore");
        System.setProperty("javax.net.ssl.trustStorePassword", "mystorepass");
    }

    public static void main(String[] args) {
        try {
            HttpURLConnection connection = (HttpURLConnection) new URL("https://webelement.click").openConnection();
            int responseCode = connection.getResponseCode();
            if(responseCode != 200){
                System.out.println("Test failed with actual status code: " + responseCode);
            }else{
                System.out.println("Test passed");
            }
        } catch (IOException e) {
            System.out.println("Test failed with exception: " + e.getMessage());
        }
    }

}

Where we just add static initialization that sets up the proper values for the properties.

This set up does not necessarily have to be put in static block. It can be added to any place the actual https call is executed.

In my next post you can learn how to make your trust store a part of your test project so that you would use the same store wherever your tests are executed. That will involve some sort of standard Java test project composition including Maven and JUnit tools.

If you still have the questions please send them to me using this form. I will amend the article according to your feedback.