Як я можу використовувати різні сертифікати на конкретних з'єднаннях?


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


невеликий, робочий виправлення: 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або просто імпортувати його з keytoolJDK (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-----";

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


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?


1
Я не думаю, що це вирішує оригінальне питання, але це вирішило мою проблему, тож дякую!
Ед Норріс

Це також не чудова ідея: тепер у вас є цей випадковий додатковий загальносистемний самопідписаний сертифікат, якому всі процеси Java будуть довіряти за замовчуванням.
user268396

12

Ми копіюємо довірену сховище JRE і додаємо наші власні сертифікати до цього довіреного сховища, а потім повідомляємо програмі використовувати користувацькі довірені сховища із властивістю системи. Таким чином ми залишаємо довірені магазини JRE за замовчуванням.

Мінус полягає в тому, що при оновленні JRE ви не отримаєте його нове довірене сховище автоматично об'єднане з вашим користувацьким.

Ви можете, можливо, впоратися з цим сценарієм, встановивши програму встановлення або запуск, що перевіряє truststore / 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 - це згадана вище реалізація фіктивного менеджера.


8
Якщо ви відключите всю перевірку довіри, використання SSL / TLS в першу чергу мало. Це нормально для локального тестування, але не, якщо ви хочете підключитися зовні.
Бруно

Я отримую це виняток, коли запускаю його на Java 7. javax.net.ssl.SSLHandshakeException: немає загальних наборів шифрів. Чи можете ви допомогти?
Урі Лукач
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.