HTTPS request with trust store for server certificates

The Java trust store

HTTPS requests in the JVM must use a trust store to validate the origin of the presented server certificate when the TLS connection is made. The JVM ships with a default trust store containing a set of root certificates, potentially with intermediates, of popular approved certificate authorities (CA), such as Let's Encrypt.

On a Linux distribution the default trust store file may be found in the following location and can be inspected with the keytool utility or programmatically via the KeyStore class:

/etc/ssl/certs/java/cacerts

If the web server presents a certificate issued and signed by a CA which root certificate is found in the trust store the HTTPS connection will succeed.

If not the request from your Java client will fail with an exception similar to this one:

javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Supporting other CAs

If the web server has a certificate issued by a CA which root certificate isn't found in the default Java trust store, or the certificate is self-issued, you have three options:

  1. Update the default trust store

    Add the CA root certificate or the server certificate (if self-issued) to the default trust store. This may require administrator privileges. Check out our article which explains the steps.

  2. Switch to a new default trust store

    Create a new trust store with keytool and import the CA root certificate into it. Then instruct your Java application to use the new trust store as the default one with the following system property:

    javax.net.ssl.trustStore=/path/to/my/truststore.jks
    

    Beware that if your Java application tries to make TLS connections to servers with certificates issued by the CAs found in the original default trust store those will fail with the mentioned SSL handshake / PKIX exception. If your application needs to make such connections copy the original default trust store file to a new location and add the required new CA root certificate(s) to it.

  3. Update your application code

    Update your Java code to use your custom trust store only for the individual HTTPS connections which need it. The default trust store remains unaffected and other HTTPS connections from your Java application will likewise not be affected

    This OAuth / OpenID Connect SDK provides you with an utility to do this quickly and efficiently. Read on to find out how.

Configure a custom trust store for each HTTPS request

In order to create a TLS connection with a custom trust store Java needs to be provided a specially configured SSLSocketFactory.

The TLSUtility which is part of this SDK since v7.1 simplifies the task.

Suppose your application needs to make token requests and the OAuth 2.0 server is issued with a certificate from a CA which is not present the default trust store.

CA certificate(s) in trust store file

Import the CA certificate into a new trust store file and then use it with each HTTPS connection like this:

import java.io.*;
import java.security.KeyStore;
import javax.net.ssl.SSLSocketFactory;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.http.*;
import com.nimbusds.oauth2.sdk.util.tls.*;

// The trust store file and optional password to unlock it
File trustStoreFile = new File("/path/to/my/truststore.jks");
char[] trustStorePassword = null; // assuming no trust store password

// Load the trust store, the default type is "pkcs12", the alternative is "jks"
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(new FileInputStream(trustStoreFile), trustStorePassword);

// Create a new SSLSocketFactory, you can keep it around for each HTTPS
// request as it's thread-safe. Use the most recent TLS version supported by
// the web server. TLS 1.3 has been standard since 2018 and is recommended.
SSLSocketFactory sslSocketFactory = TLSUtils.createSSLSocketFactory(
    trustStore,
    TLSVersion.TLS_1_3);

// Create your token request the usual way
TokenRequest tokenRequest = new TokenRequest(...);
HTTPRequest httpRequest = tokenRequest.toHTTPRequest();

// Set the SSLSocketFactory with the configured trust store
httpRequest.setSSLSocketFactory(sslSocketFactory);

// Proceed with the request as usual
HTTPResponse httpResponse = httpRequest.send();
System.out.println(httpResponse.getStatusCode());

CA certificate(s) in PEM file

If the CA certificate(s) (or the self-issued server certificate) was supplied in the ubiquitous ASCII-encoded PEM format and you don't want to deal with setting up a trust store:

import java.io.*;
import java.security.KeyStore;
import javax.net.ssl.SSLSocketFactory;
import com.nimbusds.jose.util.*;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.http.*;
import com.nimbusds.oauth2.sdk.util.tls.*;

// The PEM file with one or more CA root and potentially
// intermediate certificates, can also contain just the
// server's certificate (self-issued or not)
File pemFile = new File("/path/to/my/caCerts.pem");

// Parse the X.509 certificates from the PEM file
List<X509Certificate> caCerts = X509CertChainUtils.parse(pemFile);

// Create an in-memory trust store and put the
// certificates in it
KeyStore trustStore = KeyStore.getInstance("jks");
trustStore.load(null);
X509CertChainUtils.store(trustStore, caCerts);

// Create a new SSLSocketFactory, you can keep it around for each HTTPS
// request as it's thread-safe. Use the most recent TLS version supported by
// the web server. TLS 1.3 has been standard since 2018 and is recommended.
SSLSocketFactory sslSocketFactory = TLSUtils.createSSLSocketFactory(
    trustStore,
    TLSVersion.TLS_1_3);

// Create your token request the usual way
TokenRequest tokenRequest = new TokenRequest(...);
HTTPRequest httpRequest = tokenRequest.toHTTPRequest();

// Set the SSLSocketFactory with the configured trust store
httpRequest.setSSLSocketFactory(sslSocketFactory);

// Proceed with the request as usual
HTTPResponse httpResponse = httpRequest.send();
System.out.println(httpResponse.getStatusCode());

How to debug TLS connections

Simply set the following Java system property and this will print the TLS handshake for each connection to the console:

javax.net.debug=ssl,handshake

For example, this will print the certificate chain presented by the web server:

"Certificates": [
  "certificate" : {
    "version"            : "v3",
    "serial number"      : "03 6E AC 75 C6 00 99 6A D3 4A 69 F6 93 F3 FD 34 F2 75",
    "signature algorithm": "SHA256withRSA",
    "issuer"             : "CN=Let's Encrypt Authority X3, O=Let's Encrypt, C=US",
    "not before"         : "2020-02-13 19:24:37.000 EET",
    "not  after"         : "2020-05-13 20:24:37.000 EEST",
    "subject"            : "CN=demo.c2id.com",
    "subject public key" : "RSA",
    "extensions"         : [ "more data ..." ]
  },
  "certificate" : {
    "version"            : "v3",
    "serial number"      : "0A 01 41 42 00 00 01 53 85 73 6A 0B 85 EC A7 08",
    "signature algorithm": "SHA256withRSA",
    "issuer"             : "CN=DST Root CA X3, O=Digital Signature Trust Co.",
    "not before"         : "2016-03-17 18:40:46.000 EET",
    "not  after"         : "2021-03-17 18:40:46.000 EET",
    "subject"            : "CN=Let's Encrypt Authority X3, O=Let's Encrypt, C=US",
    "subject public key" : "RSA",
    "extensions"         : [ "more data ..." ]
  }
]