Using a custom truststore in java as well as the default one
Asked Answered
C

5

97

I'm writing an application in Java which connects to two web servers via HTTPS. One got a certificate trusted via the default chain of trust, the other uses a self signed certificate. Of course, connecting to the first server worked out of the box, whereas connecting to the server with the self signed certificate did not work until I created a trustStore with the certificate from that server. However, the connection to the by default trusted server does not work any more, because apparently the default trustStore gets to be ignored once I created my own.

One solution I found was to add the certificates from the default trustStore to my own. However, I don't like this solution, because it requires me to keep managing that trustStore. (I cannot assume these certificates remain static in the foreseeable future, right?)

Apart from that I found two 5 year old threads with a similar problem:

Registering multiple keystores in JVM

How can I have multiple SSL certificates for a Java server

They both go deep into the Java SSL infrastructure. I was hoping that by now there is a more convenient solution which I can explain easily in a security review of my code.

Cryolite answered 3/7, 2014 at 14:5 Comment(0)
D
114

You could use a similar pattern to what I've mentioned in a previous answer (for a different problem).

Essentially, get hold of the default trust manager, create a second trust manager that uses your own trust store. Wrap them both in a custom trust manager implementation that delegates call to both (falling back on the other when one fails).

TrustManagerFactory tmf = TrustManagerFactory
    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
// Using null here initialises the TMF with the default trust store.
tmf.init((KeyStore) null);

// Get hold of the default trust manager
X509TrustManager defaultTm = null;
for (TrustManager tm : tmf.getTrustManagers()) {
    if (tm instanceof X509TrustManager) {
        defaultTm = (X509TrustManager) tm;
        break;
    }
}

FileInputStream myKeys = new FileInputStream("truststore.jks");

// Do the same with your trust store this time
// Adapt how you load the keystore to your needs
KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
myTrustStore.load(myKeys, "password".toCharArray());

myKeys.close();

tmf = TrustManagerFactory
    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(myTrustStore);

// Get hold of the default trust manager
X509TrustManager myTm = null;
for (TrustManager tm : tmf.getTrustManagers()) {
    if (tm instanceof X509TrustManager) {
        myTm = (X509TrustManager) tm;
        break;
    }
}

// Wrap it in your own class.
final X509TrustManager finalDefaultTm = defaultTm;
final X509TrustManager finalMyTm = myTm;
X509TrustManager customTm = new X509TrustManager() {
    @Override
    public X509Certificate[] getAcceptedIssuers() {
        // If you're planning to use client-cert auth,
        // merge results from "defaultTm" and "myTm".
        return finalDefaultTm.getAcceptedIssuers();
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain,
            String authType) throws CertificateException {
        try {
            finalMyTm.checkServerTrusted(chain, authType);
        } catch (CertificateException e) {
            // This will throw another CertificateException if this fails too.
            finalDefaultTm.checkServerTrusted(chain, authType);
        }
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain,
            String authType) throws CertificateException {
        // If you're planning to use client-cert auth,
        // do the same as checking the server.
        finalDefaultTm.checkClientTrusted(chain, authType);
    }
};


SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { customTm }, null);

// You don't have to set this as the default context,
// it depends on the library you're using.
SSLContext.setDefault(sslContext);

You don't have to set that context as the default context. How you use it depends on the client library you're using (and where it gets its socket factories from).


This being said, in principle, you'd always have to update the truststore as required anyway. The Java 7 JSSE Reference Guide had an "important note" about this, now downgraded to just a "note" in version 8 of the same guide:

The JDK ships with a limited number of trusted root certificates in the java-home/lib/security/cacerts file. As documented in keytool reference pages, it is your responsibility to maintain (that is, add and remove) the certificates contained in this file if you use this file as a truststore.

Depending on the certificate configuration of the servers that you contact, you may need to add additional root certificates. Obtain the needed specific root certificates from the appropriate vendor.

Defect answered 3/7, 2014 at 19:2 Comment(3)
this one seems to not work in a Java spring container env.Anomaly
@DenisWang It depends on what is used in that container (not sure if it's Spring or Spring Boot), there is a chance that it doesn't use the default SSLContext but has its own. You'd need to look at what's used by the part of Spring you're using.Defect
Just to mention this, sslcontext-kickstart is a handy library, which provides wrappers, which allow to do this with a simple builder which can have multiple truststores, including the system one. github.com/Hakky54/sslcontext-kickstartBrawny
H
24

Maybe I am 6 years too late to answer this question, but it could be maybe helpful for other developers too. I also ran into the same challenge of loading the default truststore and my own custom truststore. After using the same custom solution for multiple projects, I thought it would be handy to create a library and also make it publicly available to contribute back to the community. Please have a look here: Github - SSLContext-Kickstart

Usage:

import nl.altindag.ssl.SSLFactory;

import javax.net.ssl.SSLContext;
import java.security.cert.X509Certificate;
import java.nio.file.Path;
import java.util.List;

public class App {

    public static void main(String[] args) {
        Path trustStorePath = ...;
        char[] password = "password".toCharArray();

        SSLFactory sslFactory = SSLFactory.builder()
                .withDefaultTrustMaterial() // JDK trusted CA's
                .withSystemTrustMaterial()  // OS trusted CA's
                .withTrustMaterial(trustStorePath, password)
                .build();

        SSLContext sslContext = sslFactory.getSslContext();
        List<X509Certificate> trustedCertificates = sslFactory.getTrustedCertificates();
    }

}

I wasn't quite sure if I should post this here, because it could also be seen as a way to promote "my library" but I thought it could be helpful for developers who have the same challenges.

You can add the dependency with the following snippet:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>sslcontext-kickstart</artifactId>
    <version>8.1.5</version>
</dependency>
Hammad answered 3/7, 2020 at 22:0 Comment(0)
C
11

You can retrieve the default trust store by calling TrustManagerFactory.init((KeyStore)null) and get its X509Certificates. Combine this with your own certificate. You can either load the self-signed certificate from a .jks or .p12 file with KeyStore.load or you can load a .crt (or .cer) file via CertificateFactory.

Here is a some demo code that illustrates the point. You can run the code if you download the certificate from stackoverflow.com with your browser. If you comment out both adding the loaded certificate and the default, the code will get a SSLHandshakeException, but if you keep either, it will return status code 200.

import javax.net.ssl.*;
import java.io.*;
import java.net.URL;
import java.security.*;
import java.security.cert.*;

public class HttpsWithCustomCertificateDemo {
    public static void main(String[] args) throws Exception {
        // Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        // Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files
        trustStore.load(null, null);

        // If you comment out the following, the request will fail
        trustStore.setCertificateEntry(
                "stackoverflow",
                // To test, download the certificate from stackoverflow.com with your browser
                loadCertificate(new File("stackoverflow.crt"))
        );
        // Uncomment to following to add the installed certificates to the keystore as well
        //addDefaultRootCaCertificates(trustStore);

        SSLSocketFactory sslSocketFactory = createSslSocketFactory(trustStore);

        URL url = new URL("https://stackoverflow.com/");
        HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
        // Alternatively, to use the sslSocketFactory for all Http requests, uncomment
        //HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory);
        conn.setSSLSocketFactory(sslSocketFactory);
        System.out.println(conn.getResponseCode());
    }


    private static SSLSocketFactory createSslSocketFactory(KeyStore trustStore) throws GeneralSecurityException {
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);
        TrustManager[] trustManagers = tmf.getTrustManagers();

        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, trustManagers, null);
        return sslContext.getSocketFactory();
    }

    private static X509Certificate loadCertificate(File certificateFile) throws IOException, CertificateException {
        try (FileInputStream inputStream = new FileInputStream(certificateFile)) {
            return (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(inputStream);
        }
    }

    private static void addDefaultRootCaCertificates(KeyStore trustStore) throws GeneralSecurityException {
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        // Loads default Root CA certificates (generally, from JAVA_HOME/lib/cacerts)
        trustManagerFactory.init((KeyStore)null);
        for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
            if (trustManager instanceof X509TrustManager) {
                for (X509Certificate acceptedIssuer : ((X509TrustManager) trustManager).getAcceptedIssuers()) {
                    trustStore.setCertificateEntry(acceptedIssuer.getSubjectDN().getName(), acceptedIssuer);
                }
            }
        }
    }
}
Chamade answered 25/10, 2019 at 10:38 Comment(8)
It's not working on this line. trustStore.setCertificateEntry(acceptedIssuer.getSubjectDN().getName(), acceptedIssuer); It crashes with KeyStoreException: Couldn't insert certificate; is KeyStore initialized?Spin
Did you forget to call load(null, null). Even though it looks strange, it's essential as it, well, it initializes the keystore 🤓Chamade
I have the same though. It's strange because the keystore object is already loaded.Spin
@PedroRomão Instead of trying to understand the problem, I replaced the code example with one you can test out easily. Let me know if it works for you.Chamade
In the loop to add all the certificates, there's one that crashes. I found is crashing on setCertificateEntry adding one of my own with the error -- is Keystore initialized?Spin
@PedroRomão Does it work if you keep the line "addDefaultRootCaCertificates" commented out?Chamade
Yes, it works in that case. But I really need that part of code. I don't understand the problem. I had to surround that line of code with a try-catch. It throws an error for 3 or 4 certificates (not only mine as I said before).Spin
@PedroRomão Probably there's something fishy with this one certificate. Perhaps try the code in the debugger and check all the values of acceptedIssuer. I'd also be interested in seeing your stack trace.Chamade
R
6

Here is a cleaner version of Bruno's answer

public void configureTrustStore() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException,
        CertificateException, IOException {
    X509TrustManager jreTrustManager = getJreTrustManager();
    X509TrustManager myTrustManager = getMyTrustManager();

    X509TrustManager mergedTrustManager = createMergedTrustManager(jreTrustManager, myTrustManager);
    setSystemTrustManager(mergedTrustManager);
}

private X509TrustManager getJreTrustManager() throws NoSuchAlgorithmException, KeyStoreException {
    return findDefaultTrustManager(null);
}

private X509TrustManager getMyTrustManager() throws FileNotFoundException, KeyStoreException, IOException,
        NoSuchAlgorithmException, CertificateException {
    // Adapt to load your keystore
    try (FileInputStream myKeys = new FileInputStream("truststore.jks")) {
        KeyStore myTrustStore = KeyStore.getInstance("jks");
        myTrustStore.load(myKeys, "password".toCharArray());

        return findDefaultTrustManager(myTrustStore);
    }
}

private X509TrustManager findDefaultTrustManager(KeyStore keyStore)
        throws NoSuchAlgorithmException, KeyStoreException {
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(keyStore); // If keyStore is null, tmf will be initialized with the default trust store

    for (TrustManager tm : tmf.getTrustManagers()) {
        if (tm instanceof X509TrustManager) {
            return (X509TrustManager) tm;
        }
    }
    return null;
}

private X509TrustManager createMergedTrustManager(X509TrustManager jreTrustManager,
        X509TrustManager customTrustManager) {
    return new X509TrustManager() {
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            // If you're planning to use client-cert auth,
            // merge results from "defaultTm" and "myTm".
            return jreTrustManager.getAcceptedIssuers();
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            try {
                customTrustManager.checkServerTrusted(chain, authType);
            } catch (CertificateException e) {
                // This will throw another CertificateException if this fails too.
                jreTrustManager.checkServerTrusted(chain, authType);
            }
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            // If you're planning to use client-cert auth,
            // do the same as checking the server.
            jreTrustManager.checkClientTrusted(chain, authType);
        }

    };
}

private void setSystemTrustManager(X509TrustManager mergedTrustManager)
        throws NoSuchAlgorithmException, KeyManagementException {
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, new TrustManager[] { mergedTrustManager }, null);

    // You don't have to set this as the default context,
    // it depends on the library you're using.
    SSLContext.setDefault(sslContext);
}
Rudy answered 26/6, 2020 at 1:17 Comment(1)
This helped me to fix javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target. Thank you!Chelsae
H
-2

As I figured out, you can also use SSLContextBuilder class from the Apache HttpComponents library to add your custom keystore to a SSLContext:

SSLContextBuilder builder = new SSLContextBuilder();
try {
     keyStore.load(null, null);
     builder.loadTrustMaterial(keyStore, null);
     builder.loadKeyMaterial(keyStore, null);
} catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException
          | UnrecoverableKeyException e) {
     log.error("Can not load keys from keystore '{}'", keyStore.getProvider(), e);
}
return builder.build();
Haustorium answered 23/11, 2018 at 15:24 Comment(3)
Isn't the question about having adding all the trust material into a single trust store? SSLContextBuilder#loadTrustMaterial can be used to load only one truststore. The other method SSLContextBuilder#loadKeyMaterial not for truststore, but keystore which serves different purpose. Isn't it?Maggiore
No, I think loadTrustMaterial() loads the TrustManagers from the given parameters and adds them internally to a list. So you can call it multiple times and the TrustManagers get aggregated.Haustorium
I take a look into what loadTrustMaterial will do. At least in Java11 implementation the list is going to be replace with given list. if you decompile sun.security.provider.JavaKeyStore#engineLoad you will see this.entries.clear(); there.Alphosis

© 2022 - 2024 — McMap. All rights reserved.