Как мне инициализировать TrustManagerFactory с несколькими источниками доверия?

В моем приложении есть личное хранилище ключей, содержащее доверенные самозаверяющие сертификаты для использования в локальной сети, например mykeystore.jks. Я хочу иметь возможность подключаться к общедоступным сайтам (например, google.com), а также к сайтам в моей локальной сети, используя самозаверяющие сертификаты, которые были предоставлены локально.

Проблема здесь в том, что, когда я подключаюсь к https://google.com, создание пути не удается, потому что установка моего собственного хранилища ключей переопределяет хранилище ключей по умолчанию, содержащее корневые центры сертификации, связанные с JRE, сообщая об исключении

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

Однако, если я импортирую сертификат CA в свое собственное хранилище ключей (mykeystore.jks), он работает нормально. Есть ли способ поддержать и то, и другое?

Для этого у меня есть собственный TrustManger,

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 */
            }
        }
    }

Затем я инициализирую SSLContext,

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

и установите фабрику сокетов,

HttpsURLConnection.setDefaultSSLSocketFactory(customSSLContext.getSocketFactory());

Основная программа,

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

Если я не установлю собственные доверительные менеджеры, он будет нормально подключаться к https://google.com. Как мне получить «диспетчера доверия по умолчанию», который указывает на хранилище ключей по умолчанию?


person varrunr    schedule 17.04.2014    source источник
comment
Возможный дубликат регистрации нескольких хранилищ ключей в JVM   -  person OrangeDog    schedule 01.09.2016


Ответы (6)


В trustMgrFactory.init(keystore); вы настраиваете defaultTrustManager с вашим личным хранилищем ключей, а не с системным хранилищем ключей по умолчанию.

На основании чтения исходного кода sun.security.ssl.TrustManagerFactoryImpl, похоже, trustMgrFactory.init((KeyStore) null); сделает именно то, что вам нужно (загрузит системное хранилище ключей по умолчанию), и, судя по быстрому тестированию, мне кажется, что это сработало.

person Pasi    schedule 23.04.2014
comment
Я пробовал это с Java 1.8 (сборка 1.8.0_60-b27). Но у меня это не сработало. У меня такая же ошибка: не удалось построить путь PKIX. - person Sergey V.; 26.11.2015
comment
Но это не позволяет вам использовать свои сертификаты в настраиваемом хранилище ключей, только сертификаты CA, установленные в системе. - person Hugh Jeffner; 03.03.2016
comment
Престижность за выяснение этого, прочитав исходник! Это упрощает многие манипуляции с путями и именами! Также меня удивляет, что для него тоже не требуется пароль. - person FUD; 03.03.2017

Ответ здесь заключается в том, как я понял, как это сделать. Если вы просто хотите принять системные сертификаты CA плюс настраиваемое хранилище сертификатов, я упростил его до одного класса с некоторыми удобными методами. Полный код доступен здесь:

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);
person Hugh Jeffner    schedule 02.03.2016
comment
Очень полезное решение - person Junaed; 04.11.2019

Я столкнулся с той же проблемой с Commons HttpClient. Рабочим решением для моего случая было создание цепочки делегирования для PKIX TrustManager следующим образом:

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();
    }

}

И инициализируйте HttpClient следующим образом (да, это некрасиво):

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();

В вашем случае с простым HttpsURLConnection вы можете обойтись упрощенной версией делегирования класса:

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();
    }

}
person Novoj    schedule 17.02.2016
comment
Более подробно решение описано здесь: blog.novoj.net/2016/02/29/ - person Novoj; 01.03.2016

Для разработчиков Android это может быть намного проще. Таким образом, вы можете добавить XML-файл res для настройки своих пользовательских сертификатов.

Шаг 1: откройте свой XML-манифест и добавьте атрибут.

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

Шаг 2: Добавьте network_security_config.xml в res / xml, настройте сертификаты по своему усмотрению.

<?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>

Примечание: этот xml может поддерживать многие другие варианты использования, и это решение работает только на api24 +.

Официальная ссылка: здесь

person KFJK    schedule 26.02.2019

Хотя этому вопросу 6 лет, я хочу поделиться своим решением этой проблемы. Он использует тот же фрагмент кода под обложками от Коди А. Рэя которым также поделился Хью Джеффнер.

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

HttpsURLConnection.setDefaultSSLSocketFactory(sslFactory.getSslSocketFactory());

Во время процесса подтверждения ssl он сначала проверит, присутствует ли сертификат сервера в доверенных сертификатах jdk, если нет, он продолжит, также проверив ваше собственное хранилище ключей, и если он не найдет совпадения, он потерпит неудачу. Вы даже можете связать его с дополнительными пользовательскими хранилищами ключей, или файлами pem, или списком сертификатов и т. Д. Другие конфигурации см. Здесь: другие возможные конфигурации

Эта библиотека поддерживается мной, и вы можете найти ее здесь: https://github.com/Hakky54/sslcontext-kickstart

person Hakan54    schedule 20.12.2020

person    schedule
comment
Пожалуйста, не публикуйте только код в качестве ответа, но также объясните, что делает ваш код и как он решает проблему вопроса. Ответы с объяснением обычно более полезны и качественнее, и с большей вероятностью получат положительные отзывы. - person Doj; 16.12.2020