How do I initialize a TrustManagerFactory with multiple sources of trust?
Asked Answered
E

6

25

My application has a personal keystore containing trusted self-signed certificates for use in the local network - say mykeystore.jks. I wish to be able to connect to public sites(say google.com) as well as ones in my local network using self-signed certificates which have been provisioned locally.

The problem here is that, when I connect to https://google.com, path building fails, because setting my own keystore overrides the default keystore containing root CAs bundled with the JRE, reporting the exception

sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

However, if I import a CA certificate into my own keystore(mykeystore.jks) it works fine. Is there a way to support both?

I have my own TrustManger for this purpose,

public class CustomX509TrustManager implements X509TrustManager {

        X509TrustManager defaultTrustManager;

        public MyX509TrustManager(KeyStore keystore) {
                TrustManagerFactory trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                trustMgrFactory.init(keystore);
                TrustManager trustManagers[] = trustMgrFactory.getTrustManagers();
                for (int i = 0; i < trustManagers.length; i++) {
                    if (trustManagers[i] instanceof X509TrustManager) {
                        defaultTrustManager = (X509TrustManager) trustManagers[i];
                        return;
                    }
                }

        public void checkServerTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            try {
                defaultTrustManager.checkServerTrusted(chain, authType);
            } catch (CertificateException ce) {
            /* Handle untrusted certificates */
            }
        }
    }

I then initialize the SSLContext,

TrustManager[] trustManagers =
            new TrustManager[] { new CustomX509TrustManager(keystore) };
SSLContext customSSLContext =
        SSLContext.getInstance("TLS");
customSSLContext.init(null, trustManagers, null);

and set the socket factory,

HttpsURLConnection.setDefaultSSLSocketFactory(customSSLContext.getSocketFactory());

The main program,

URL targetServer = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection) targetServer.openConnection();

If I don't set my own trust managers, it connects to https://google.com just fine. How do I get a "default trust manager" which points to the default key store?

Exaggeration answered 17/4, 2014 at 22:13 Comment(1)
Possible duplicate of Registering multiple keystores in JVMHaya
G
19

In trustMgrFactory.init(keystore); you're configuring defaultTrustManager with your own personal keystore, not the system default keystore.

Based on reading the source code for sun.security.ssl.TrustManagerFactoryImpl, it looks like trustMgrFactory.init((KeyStore) null); would do exactly what you need (load the system default keystore), and based on quick testing, it seems to work for me.

Glum answered 23/4, 2014 at 20:53 Comment(3)
I've tried it with Java 1.8 (build 1.8.0_60-b27). But it didn't work for me. I've got the same error: PKIX path building failed.Glucose
But this doesn't let you use your certs in a custom keystore, only the CA certs installed on the system.Tycoon
Kudos for figuring this out by reading source! This simplifies a lot of cacerts path and name handling jugglery! Also amazes me that there is no password requirement for it too.Pizor
T
17

The answer here is how I came to understand how to do this. If you just want to accept the system CA certs plus a custom keystore of certs I simplified it into a single class with some convenience methods. Full code available here:

https://gist.github.com/HughJeffner/6eac419b18c6001aeadb

KeyStore keystore; // Get your own keystore here
SSLContext sslContext = SSLContext.getInstance("TLS");
TrustManager[] tm = CompositeX509TrustManager.getTrustManagers(keystore);
sslContext.init(null, tm, null);
Tycoon answered 2/3, 2016 at 22:3 Comment(0)
H
4

I've run into the same issue with Commons HttpClient. Working solution for my case was to create delegation chain for PKIX TrustManagers in following way:

public class TrustManagerDelegate implements X509TrustManager {
    private final X509TrustManager mainTrustManager;
    private final X509TrustManager trustManager;
    private final TrustStrategy trustStrategy;

    public TrustManagerDelegate(X509TrustManager mainTrustManager, X509TrustManager trustManager, TrustStrategy trustStrategy) {
        this.mainTrustManager = mainTrustManager;
        this.trustManager = trustManager;
        this.trustStrategy = trustStrategy;
    }

    @Override
    public void checkClientTrusted(
            final X509Certificate[] chain, final String authType) throws CertificateException {
        this.trustManager.checkClientTrusted(chain, authType);
    }

    @Override
    public void checkServerTrusted(
            final X509Certificate[] chain, final String authType) throws CertificateException {
        if (!this.trustStrategy.isTrusted(chain, authType)) {
            try {
                mainTrustManager.checkServerTrusted(chain, authType);
            } catch (CertificateException ex) {
                this.trustManager.checkServerTrusted(chain, authType);
            }
        }
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return this.trustManager.getAcceptedIssuers();
    }

}

And initialize HttpClient in following way (yes it's ugly):

final SSLContext sslContext;
try {
    sslContext = SSLContext.getInstance("TLS");
    final TrustManagerFactory javaDefaultTrustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    javaDefaultTrustManager.init((KeyStore)null);
    final TrustManagerFactory customCaTrustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    customCaTrustManager.init(getKeyStore());

    sslContext.init(
        null,
        new TrustManager[]{
            new TrustManagerDelegate(
                    (X509TrustManager)customCaTrustManager.getTrustManagers()[0],
                    (X509TrustManager)javaDefaultTrustManager.getTrustManagers()[0],
                    new TrustSelfSignedStrategy()
            )
        },
        secureRandom
    );

} catch (final NoSuchAlgorithmException ex) {
    throw new SSLInitializationException(ex.getMessage(), ex);
} catch (final KeyManagementException ex) {
    throw new SSLInitializationException(ex.getMessage(), ex);
}

SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
        RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", sslSocketFactory)
                .build()
);
//maximum parallel requests is 500
cm.setMaxTotal(500);
cm.setDefaultMaxPerRoute(500);

CredentialsProvider cp = new BasicCredentialsProvider();
cp.setCredentials(
        new AuthScope(apiSettings.getIdcApiUrl(), 443),
        new UsernamePasswordCredentials(apiSettings.getAgencyId(), apiSettings.getAgencyPassword())
);

client = HttpClients.custom()
                    .setConnectionManager(cm)
                    .build();

In your case with simple HttpsURLConnection you may get by with simplified version of delegating class:

public class TrustManagerDelegate implements X509TrustManager {
    private final X509TrustManager mainTrustManager;
    private final X509TrustManager trustManager;

    public TrustManagerDelegate(X509TrustManager mainTrustManager, X509TrustManager trustManager) {
        this.mainTrustManager = mainTrustManager;
        this.trustManager = trustManager;
    }

    @Override
    public void checkClientTrusted(
            final X509Certificate[] chain, final String authType) throws CertificateException {
        this.trustManager.checkClientTrusted(chain, authType);
    }

    @Override
    public void checkServerTrusted(
            final X509Certificate[] chain, final String authType) throws CertificateException {
        try {
            mainTrustManager.checkServerTrusted(chain, authType);
        } catch (CertificateException ex) {
            this.trustManager.checkServerTrusted(chain, authType);
        }
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return this.trustManager.getAcceptedIssuers();
    }

}

A detailed description of the solution is here: https://blog.novoj.net/posts/2016-02-29-how-to-make-apache-httpclient-trust-lets-encrypt-certificate-authority/

Hett answered 17/2, 2016 at 16:12 Comment(1)
Solution is described in more details here: blog.novoj.net/2016/02/29/…Hett
A
1

For Android developers, this can be much easier. In summary, you can add a xml res file to config your custom certs.

Step 1: open your manifest xml add an attribute.

<manifest ... >
    <application android:networkSecurityConfig="@xml/network_security_config"
                    ... >
        ...
    </application>
</manifest>

Step 2: Add network_security_config.xml to res/xml, config certs as you want.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config>
        <trust-anchors>
            <certificates src="@raw/extracas"/>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
</network-security-config>

Note: this xml can support many other usage, and this solution only works on api24+.

Official reference: here

Allege answered 26/2, 2019 at 2:53 Comment(0)
S
1
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

/**
 * Represents an ordered list of {@link X509TrustManager}s with additive trust. If any one of the composed managers
 * trusts a certificate chain, then it is trusted by the composite manager.
 *
 * This is necessary because of the fine-print on {@link SSLContext#init}: Only the first instance of a particular key
 * and/or trust manager implementation type in the array is used. (For example, only the first
 * javax.net.ssl.X509KeyManager in the array will be used.)
 *
 * @author codyaray
 * @since 4/22/2013
 * @see <a href="https://mcmap.net/q/18229/-registering-multiple-keystores-in-jvm-duplicate">
 *     https://mcmap.net/q/18229/-registering-multiple-keystores-in-jvm-duplicate
 *     </a>
 */
@SuppressWarnings("unused")
public class CompositeX509TrustManager implements X509TrustManager {

    private final List<X509TrustManager> trustManagers;

    public CompositeX509TrustManager(List<X509TrustManager> trustManagers) {
        this.trustManagers = ImmutableList.copyOf(trustManagers);
    }

    public CompositeX509TrustManager(KeyStore keystore) {

        this.trustManagers = ImmutableList.of(getDefaultTrustManager(), getTrustManager(keystore));

    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        for (X509TrustManager trustManager : trustManagers) {
            try {
                trustManager.checkClientTrusted(chain, authType);
                return; // someone trusts them. success!
            } catch (CertificateException e) {
                // maybe someone else will trust them
            }
        }
        throw new CertificateException("None of the TrustManagers trust this certificate chain");
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        for (X509TrustManager trustManager : trustManagers) {
            try {
                trustManager.checkServerTrusted(chain, authType);
                return; // someone trusts them. success!
            } catch (CertificateException e) {
                // maybe someone else will trust them
            }
        }
        throw new CertificateException("None of the TrustManagers trust this certificate chain");
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        ImmutableList.Builder<X509Certificate> certificates = ImmutableList.builder();
        for (X509TrustManager trustManager : trustManagers) {
            for (X509Certificate cert : trustManager.getAcceptedIssuers()) {
                certificates.add(cert);
            }
        }
        return Iterables.toArray(certificates.build(), X509Certificate.class);
    }

    public static TrustManager[] getTrustManagers(KeyStore keyStore) {

        return new TrustManager[] { new CompositeX509TrustManager(keyStore) };

    }

    public static X509TrustManager getDefaultTrustManager() {

        return getTrustManager(null);

    }

    public static X509TrustManager getTrustManager(KeyStore keystore) {

        return getTrustManager(TrustManagerFactory.getDefaultAlgorithm(), keystore);

    }

    public static X509TrustManager getTrustManager(String algorithm, KeyStore keystore) {

        TrustManagerFactory factory;

        try {
            factory = TrustManagerFactory.getInstance(algorithm);
            factory.init(keystore);
            return Iterables.getFirst(Iterables.filter(
                    Arrays.asList(factory.getTrustManagers()), X509TrustManager.class), null);
        } catch (NoSuchAlgorithmException | KeyStoreException e) {
            e.printStackTrace();
        }

        return null;

    }

}
Senghor answered 16/12, 2020 at 12:19 Comment(1)
Please don't post only code as answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes.Tightlipped
L
0

Although this question is 6 years old, I want to share my solution for this challenge. It uses the same code snippet under the covers from Cody A. Ray which Hugh Jeffner also shared.

SSLFactory sslFactory = SSLFactory.builder()
    .withDefaultTrustMaterial() // --> uses the JDK trusted certificates
    .withTrustMaterial("/path/to/mykeystore.jks", "password".toCharArray())
    .build();

HttpsURLConnection.setDefaultSSLSocketFactory(sslFactory.getSslSocketFactory());

During the ssl handshake process it will first check if the server certificate is present in the jdk trusted certificates, if not it will continue by also checking your custom keystore and if it doesn't find a match it will fail. You can even further chain it with more custom keystores, or pem files, or list of certificates etc. See here for other configurations: other possible configurations

This library is maintained by me and you can find it here: https://github.com/Hakky54/sslcontext-kickstart

Luminosity answered 20/12, 2020 at 14:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.