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