Как я могу использовать разные сертификаты для определенных подключений?

StackOverflow https://stackoverflow.com/questions/859111

Вопрос

Модуль, который я добавляю в наше большое Java-приложение, должен взаимодействовать с веб-сайтом другой компании, защищенным SSL.Проблема в том, что сайт использует самозаверяющий сертификат.У меня есть копия сертификата, чтобы убедиться, что я не сталкиваюсь с атакой типа "человек посередине", и мне нужно включить этот сертификат в наш код таким образом, чтобы соединение с сервером было успешным.

Вот базовый код:

void sendRequest(String dataPacket) {
  String urlStr = "https://host.example.com/";
  URL url = new URL(urlStr);
  HttpURLConnection conn = (HttpURLConnection)url.openConnection();
  conn.setMethod("POST");
  conn.setRequestProperty("Content-Length", data.length());
  conn.setDoOutput(true);
  OutputStreamWriter o = new OutputStreamWriter(conn.getOutputStream());
  o.write(data);
  o.flush();
}

Без какой-либо дополнительной обработки для самозаверяющего сертификата это заканчивается в conn.getOutputStream() со следующим исключением:

Exception in thread "main" 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
....
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
....
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

В идеале мой код должен научить Java принимать этот единственный самозаверяющий сертификат для этого одного места в приложении и нигде больше.

Я знаю, что могу импортировать сертификат в хранилище центра сертификации JRE, и это позволит Java принять его.Это не тот подход, который я хотел бы использовать, если я могу помочь;это кажется очень инвазивным делать на всех машинах наших клиентов для одного модуля, который они, возможно, не используют;это повлияло бы на все другие Java-приложения, использующие тот же JRE, и мне это не нравится, даже несмотря на то, что шансы любого другого Java-приложения когда-либо получить доступ к этому сайту равны нулю.Это также не тривиальная операция:в UNIX я должен получить права доступа для изменения JRE таким образом.

Я также видел, что я могу создать экземпляр TrustManager, который выполняет некоторую пользовательскую проверку.Похоже, я мог бы даже создать TrustManager, который делегирует реальному TrustManager во всех экземплярах, кроме этого одного сертификата.Но похоже, что TrustManager устанавливается глобально, и я предполагаю, что это повлияет на все другие подключения из нашего приложения, и мне это тоже кажется не совсем правильным.

Какой предпочтительный, стандартный или наилучший способ настроить Java-приложение на принятие самозаверяющего сертификата?Могу ли я достичь всех целей, которые я имею в виду выше, или мне придется пойти на компромисс?Есть ли какой-нибудь вариант, включающий файлы, каталоги и параметры конфигурации, а также практически никакой код?

Это было полезно?

Решение

Создать SSLSocket изготовьте сами и установите его на HttpsURLConnection перед подключением.

...
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslFactory);
conn.setMethod("POST");
...

Вы захотите создать его SSLSocketFactory и держи это при себе.Вот набросок того, как его инициализировать:

/* Load the keyStore that includes self-signed cert as a "trusted" entry. */
KeyStore keyStore = ... 
TrustManagerFactory tmf = 
  TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
sslFactory = ctx.getSocketFactory();

Если вам нужна помощь в создании хранилища ключей, пожалуйста, прокомментируйте.


Вот пример загрузки хранилища ключей:

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(trustStore, trustStorePassword);
trustStore.close();

Чтобы создать хранилище ключей с сертификатом формата PEM, вы можете написать свой собственный код, используя CertificateFactory, или просто импортируйте его с keytool из JDK (keytool не будет работает для "ключевой записи", но вполне подходит для "доверенной записи").

keytool -import -file selfsigned.pem -alias server -keystore server.jks

Другие советы

Я прочитал множество мест в Интернете, чтобы решить эту проблему.Это код, который я написал, чтобы заставить его работать:

                ByteArrayInputStream derInputStream = new ByteArrayInputStream(app.certificateString.getBytes());
                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
                X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream);
                String alias = "alias";//cert.getSubjectX500Principal().getName();

                KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
                trustStore.load(null);
                trustStore.setCertificateEntry(alias, cert);
                KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
                kmf.init(trustStore, null);
                KeyManager[] keyManagers = kmf.getKeyManagers();

                TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
                tmf.init(trustStore);
                TrustManager[] trustManagers = tmf.getTrustManagers();

                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(keyManagers, trustManagers, null);
                URL url = new URL(someURL);
                conn = (HttpsURLConnection) url.openConnection();
                conn.setSSLSocketFactory(sslContext.getSocketFactory());

app.certificateString - это строка, содержащая сертификат, например:

            static public String certificateString=
            "-----BEGIN CERTIFICATE-----\n" +
            "MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" +
            "BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" +
            ... a bunch of characters...
            "5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" +
            "-----END CERTIFICATE-----";

Я проверил, что вы можете поместить любые символы в строку сертификата, если она самоподписана, при условии сохранения точной структуры, приведенной выше.Я получил строку сертификата с помощью командной строки терминала моего ноутбука.

Если создание SSLSocketFactory это не вариант, просто импортируйте ключ в JVM

  1. Извлеките открытый ключ:$openssl s_client -connect dev-server:443, затем создайте файл dev-сервер.pem это выглядит как

    -----BEGIN CERTIFICATE----- 
    lklkkkllklklklklllkllklkl
    lklkkkllklklklklllkllklkl
    lklkkkllklk....
    -----END CERTIFICATE-----
    
  2. Импортируйте ключ: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem.Пароль: изменить это

  3. Перезапустить JVM

Источник: Как решить javax.net.ssl.SSLHandshakeException?

Мы копируем truststore JRE и добавляем наши пользовательские сертификаты в это truststore, затем указываем приложению использовать пользовательский truststore с системным свойством.Таким образом, мы оставляем JRE truststore по умолчанию в покое.

Недостатком является то, что при обновлении JRE вы не получаете автоматического объединения его нового хранилища доверия с вашим пользовательским.

Возможно, вы могли бы справиться с этим сценарием, установив программу установки или запуска, которая проверяет truststore / jdk и проверяет на несоответствие или автоматически обновляет truststore.Я не знаю, что произойдет, если вы обновите truststore во время работы приложения.

Это решение не является на 100% элегантным или надежным, но оно простое, работает и не требует кода.

Мне приходилось делать что-то подобное при использовании commons-httpclient для доступа к внутреннему https-серверу с самозаверяющим сертификатом.Да, наше решение состояло в том, чтобы создать пользовательский TrustManager, который просто передавал все (регистрируя сообщение отладки).

Это сводится к наличию нашего собственного SSLSocketFactory, который создает SSL-сокеты из нашего локального SSLContext, который настроен так, чтобы с ним был связан только наш локальный TrustManager.Вам вообще не нужно приближаться к хранилищу ключей / certstore.

Итак, это находится в нашей LocalSSLSocketFactory:

static {
    try {
        SSL_CONTEXT = SSLContext.getInstance("SSL");
        SSL_CONTEXT.init(null, new TrustManager[] { new LocalSSLTrustManager() }, null);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException("Unable to initialise SSL context", e);
    } catch (KeyManagementException e) {
        throw new RuntimeException("Unable to initialise SSL context", e);
    }
}

public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
    LOG.trace("createSocket(host => {}, port => {})", new Object[] { host, new Integer(port) });

    return SSL_CONTEXT.getSocketFactory().createSocket(host, port);
}

Наряду с другими методами, реализующими SecureProtocolSocketFactory.LocalSSLTrustManager - это вышеупомянутая фиктивная реализация trust manager.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top