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

164

Модуль, который я добавляю в наше большое 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 для принятия самозаверяющего сертификата? Могу ли я достичь всех целей, которые я имею в виду выше, или мне придется идти на компромисс? Есть ли опция, включающая файлы и каталоги, а также параметры конфигурации и практически неиспользуемый код?

skiphoppy
источник
маленький, работая фикс: rgagnon.com/javadetails/...
20
@Hasenpriester: пожалуйста, не предлагайте эту страницу. Это отключает все проверки доверия. Вы не только примете самоподписанный сертификат, который вы хотите, вы также примете любой сертификат, который вам предоставит злоумышленник MITM.
Бруно

Ответы:

168

Создайте 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
Эриксон
источник
3
Большое спасибо! Другие ребята помогли мне направить меня в правильном направлении, но в конце концов я выбрал именно ваш. У меня была изнурительная задача по преобразованию файла сертификата PEM в файл хранилища ключей JKS Java, и я нашел помощь для этого здесь: stackoverflow.com/questions/722931/…
skiphoppy
1
Я рад, что это сработало. Мне жаль, что вы боролись с хранилищем ключей; Я должен был просто включить это в свой ответ. Это не так уж сложно с CertificateFactory. На самом деле, я думаю, что сделаю обновление для всех, кто придет позже.
Эриксон
1
Все, что делает этот код, это копирует то, что вы можете сделать, установив три системных свойства, описанных в Справочном руководстве JSSE.
маркиз Лорн
2
@EJP: Это было некоторое время назад, поэтому я точно не помню, но думаю, что обоснование заключалось в том, что в «большом приложении Java», вероятно, будут установлены другие HTTP-соединения. Установка глобальных свойств может помешать рабочим соединениям или позволить этой стороне подделать серверы. Это пример использования встроенного диспетчера доверия в каждом конкретном случае.
Эриксон
2
Как сказал ОП, «мой код должен научить Java принимать этот один самозаверяющий сертификат, для этого единственного места в приложении и нигде больше».
Эриксон
18

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

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-----";

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

мистифицировать
источник
1
Спасибо, что поделились @Josh. Я создал небольшой проект Github, который демонстрирует ваш используемый код: github.com/aasaru/ConnectToTrustedServerExample
Master Drools
14

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

  1. Получите открытый ключ:, $openssl s_client -connect dev-server:443затем создайте файл dev-server.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?

user454322
источник
1
Я не думаю, что это решает первоначальный вопрос, но это решило мою проблему, так что спасибо!
Эд Норрис
Это также не очень хорошая идея: теперь у вас есть этот случайный дополнительный общесистемный самоподписанный сертификат, которому все процессы Java будут доверять по умолчанию.
user268396
12

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

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

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

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

Мистер Блестящий и Новый 安 宇
источник
12

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

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

Так что это в нашей 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 является вышеупомянутой фиктивной реализацией диспетчера доверия.

araqnid
источник
8
Если вы отключите все проверки доверия, то, в первую очередь, нет смысла использовать SSL / TLS. Это нормально для локального тестирования, но не для подключения снаружи.
Бруно
Я получаю это исключение при запуске его на Java 7. javax.net.ssl.SSLHandshakeException: нет общих наборов шифров. Вы можете помочь?
Ури Лукач