Java and TrustStores

This post is going to go over how to use custom x509 certificates with Java clients.

Normally Java applications will use CA certificates provided by the operating system. In the case of CentOS 7 certificates can be found in a TrustStore called /etc/pki/ca-trust/extracted/java/cacerts.

However if you are doing anything like using a self signed certificate connections will fail. This happens because the Java client cannot verify the identity of the server it's connecting to, without a corresponding certificate.

Example code

Below is a short Java class which attempts to connect to a URL and print the HTTP return code:

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

class Example {

    public static void main(String[] args) {
        try {
            URL url = new URL(args[0]);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("HEAD");
            System.out.printf("Connecting to : %s\n", url);
            connection.connect();
            System.out.printf("Return code   : %d\n", connection.getResponseCode());
        } catch (ArrayIndexOutOfBoundsException error) {
            System.err.println("Missing URL argument.");
        } catch (MalformedURLException error) {
            System.err.println(error.getMessage());
        } catch (IOException error) {
            System.err.println(error.getMessage());
        }
    }
}

The certificate used by example.com is signed by DigiCert Inc. Therefore it can be verified using the certificates found in /etc/pki/ca-trust/extracted/java/cacerts:

$ java Example https://example.com
Connecting to : https://example.com
Return code   : 200

However trying to connect to a web server using a self signed certificate will fail:

$ java Example https://foobar.localdomain
Connecting to : https://foobar.localdomain
sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Downloading the certificate

The first thing to do is get a copy of the CA certificate used by the server you're trying to connect to. If you have access to the server you can just copy the file. Alternatively you can use the s_client command in OpenSSL:

$ echo | openssl s_client -connect foobar.localdomain:443
CONNECTED(00000003)
...
-----BEGIN CERTIFICATE-----
MIIEBTCCAu2gAwIBAgICTMwwDQYJKoZIhvcNAQELBQAwgbUxCzAJBgNVBAYTAi0t
MRIwEAYDVQQIDAlTb21lU3RhdGUxETAPBgNVBAcMCFNvbWVDaXR5MRkwFwYDVQQK
DBBTb21lT3JnYW5pemF0aW9uMR8wHQYDVQQLDBZTb21lT3JnYW5pemF0aW9uYWxV
bml0MRswGQYDVQQDDBJmb29iYXIubG9jYWxkb21haW4xJjAkBgkqhkiG9w0BCQEW
F3Jvb3RAZm9vYmFyLmxvY2FsZG9tYWluMB4XDTE2MDcxNjE3MjY0N1oXDTE3MDcx
NjE3MjY0N1owgbUxCzAJBgNVBAYTAi0tMRIwEAYDVQQIDAlTb21lU3RhdGUxETAP
BgNVBAcMCFNvbWVDaXR5MRkwFwYDVQQKDBBTb21lT3JnYW5pemF0aW9uMR8wHQYD
VQQLDBZTb21lT3JnYW5pemF0aW9uYWxVbml0MRswGQYDVQQDDBJmb29iYXIubG9j
YWxkb21haW4xJjAkBgkqhkiG9w0BCQEWF3Jvb3RAZm9vYmFyLmxvY2FsZG9tYWlu
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt8edqst2h5pjo/tYe8z5
WYfgC+iOOL/WmcrLJgYZDwlCBHZN/7kMCvzD4oJZVB8bxC0sYLCwwfIsB+9Im+PN
aYL324g4+Z7B7VyCLcjP8npf2YHQeTXdYJzaJeqgadjvJ22AqOlR7y7lAJzl3jZQ
gg7PNLuODecEolUgPNmR7J1rlaFuUDSK02VTM7bqAnjtOdkyKDJI9C3I9GHsePQa
WH0lL9rpKQpdbrS6i/9g3w8etIhxadW4HFKZd2uOm01ggdbBNC1UYszPb6wgff4m
ZXen3A+ldj0BDjbBDeR+bELxd4m2v/AuBeW717lf5nV7V80Wm+64rvs3EMBFPIZU
bwIDAQABox0wGzAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIF4DANBgkqhkiG9w0B
AQsFAAOCAQEAAVKyrm3AueU9udhm5E4EehVGQ/JdMmCQEP18ct6KIU8v7PkYApaS
tbBN27jBSrEuGeP3Dt6Wy8xw2Qt5oSRh2EN4S6qx9oAUiA0rJz6c+RjdGVTjB8H7
bwAQ6vOphqxwjsnnswv60cd6Bex2ymox11+TlAl1jVUM/QC7nC8HIffk2oQKjN1E
VFG+kgh18Q7DllYSaycjtFmDKR4KvzcIu0EvENgAMi+6DnFtlm6picdn2iRlHwCc
GM2KSefgCgIxnfVUHuuSvj70PuJuuGsupRWu2zE2FdOC2gJpzQtrXWnNgnGb4gPm
P3CPTHVCKbt3BbuCv3AIOiSou6hFuSj9RQ==
-----END CERTIFICATE-----
...

The contents of a x509 certificate can easily be checked using the following OpenSSL command:

openssl x509 -in foobar.localdomain.pem -noout -text

You can also use the following curl command to verify you have the correct certificate:

curl -I --cacert foobar.localdomain.pem https://foobar.localdomain

X509v3 basic constraints

Before continuing it's worth pointing out a gotcha. For this post I set up Apache on CentOS 7. When you install the mod_ssl package the following files are created by the package scriptlet using OpenSSL:

  • /etc/pki/tls/private/localhost.key
  • /etc/pki/tls/certs/localhost.crt

The default OpenSSL configuration (/etc/pki/tls/openssl.cnf) has the following lines:

[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE

As a result the self-signed certificate will have the CA boolean set to false:

  X509v3 extensions:
        X509v3 Basic Constraints:
            CA:FALSE

As per the x509 rfc, if the CA boolean is false, it should not be used to verify certificate signatures. To get around this the certificate needs to be regenerated with the CA boolean set to true.

Adding to the system TrustStore

Under CentOS 7 adding the new certificate is just a case of running the following:

cp foobar.localdomain.pem /etc/pki/ca-trust/source/anchors/
update-ca-trust

This will update the entries in /etc/pki/ca-trust/extracted/java/cacerts. You can verify the new certificate is present using the keytool command:

$ keytool -list \
  -keystore /etc/pki/ca-trust/extracted/java/cacerts \
  -storepass changeit
...
foobar.localdomain, 16-Jul-2016, trustedCertEntry,
Certificate fingerprint (SHA1): 75:EE:B3:4E:68:17:43:57:D9:A6:B1:6B:19:B2:7C:69:ED:0B:39:6F
...

Note: Adding files to /etc/pki/ca-trust/source/anchors/ will normally require root access.

Using a custom TrustStore

Alternatively a custom TrustStore file can be created. This is helpful if you are not able to change the system TrustStore. Note that Java will not look at the system TrustStore if a custom TrustStore is used.

To create a new TrustStore and import the certificate the following keytool command can be run:

keytool -import \
  -alias foobar.localdomain \
  -keystore cacerts.ts \
  -file foobar.localdomain.pem

Note: It's not possible to set an empty password. Instead you need to use the Java default password changeit. Alternatively you can set a TrustStore password and then provide this to Java at runtime.

Once the TrustStore has been created you can set the javax.net.ssl.trustStore option to get Java to use it:

$ java -Djavax.net.ssl.trustStore=cacerts.ts \
  Example https://foobar.localdomain
Connecting to : https://foobar.localdomain
Return code   : 200

If you used a non-standard password you will also need to use the javax.net.ssl.trustStorePassowrd option:

$ java -Djavax.net.ssl.trustStore=cacerts.ts  \
       -Djavax.net.ssl.trustStorePassword=password \
       Example https://foobar.localdomain
Connecting to : https://foobar.localdomain
Return code   : 200