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


147

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

  0

маленький, рабочий исправление: http://www.rgagnon.com/javadetails/java-fix-certificate-problem-in-HTTPS.html 16 авг. 112011-08-16 14:06:49

+16

@Hasenpriester: просьба не предлагать эту страницу. Он отключает проверку доверия. Вы не только согласитесь, что хотите получить самозаверяющий сертификат, но и принять любой сертификат, который вам представит злоумышленник MITM. 23 дек. 112011-12-23 13:23:14

150

Создайте завод 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, и я нашел помощь для этого здесь: http://stackoverflow.com/questions/722931/ssl-socket-php-code-needs-to- be-convert-to-java 13 май. 092009-05-13 21:37:13

+1

Я рад, что все получилось. Мне жаль, что ты боролся с хранилищем ключей; Я должен был просто включить его в свой ответ. С CertFactory это не сложно. На самом деле, я думаю, что сделаю обновление для всех, кто придет позже. 13 май. 092009-05-13 21:40:51

  0

Ничего себе. Это означает, что преобразование PEM в JKS значительно проще, чем то, что я сделал! Благодаря! :) 14 май. 092009-05-14 19:19:00

+1

Весь этот код выполняет репликацию того, что вы можете выполнить, установив три системных свойства, описанных в Руководстве по проверке JSSE. 31 май. 122012-05-31 19:36:53

+1

@EJP: Это было давно, поэтому я не помню точно, но я предполагаю, что было обосновано, что в «большом приложении Java», вероятно, будут установлены другие HTTP-соединения. Настройка глобальных свойств может помешать рабочим соединениям или разрешить этой стороне обманывать серверы. Это пример использования встроенного менеджера доверия в каждом конкретном случае. 31 май. 122012-05-31 22:26:58

+2

Как сказал OP, «мой код должен научить Java принимать этот один самозаверяющий сертификат, для этого одного места в приложении, и нигде больше». 31 май. 122012-05-31 22:29:09

  0

Я попытался реализовать ваше решение безрезультатно, пожалуйста, помогите мне http://stackoverflow.com/questions/20190364/how-to-bypass-certificateexception-by-java 25 ноя. 132013-11-25 10:59:37

  0

Если вы хотите, чтобы его принимали «глобально», используйте keytool для установки сертификата в качестве доверенного сертификата в файле cacerts. Тогда ему будет доверять любая виртуальная машина, которая использует эту JRE. 06 дек. 132013-12-06 19:59:08

  0

@KyleM Это не очень хороший подход к глобальному признанию. 06 дек. 132013-12-06 20:03:33

  0

Вот почему я поставил «глобальный» в кавычки и объяснил, как это работает. Никто не упомянул об этом так, хотя это очень распространенный и общепринятый метод. 06 дек. 132013-12-06 20:07:48

  0

Многие крупные организации не принимают этот метод. Это было бы нарушение безопасности с серьезными последствиями. 06 дек. 132013-12-06 21:09:37

  0

@erickson Привет - это четыре года спустя, и подход, который вы нам научили, хорошо напевал и использовался для нескольких разных серверов, с которыми мы общаемся. Теперь у меня есть новый трюк, который мне нужно выяснить (может сделать для него новый вопрос StackOverflow): я бы хотел доверять как хранилищу ключей по умолчанию, так и настраиваемому хранилищу ключей с самозаверяющим сертификатом сервера. У нас был случай, когда сервер обновился до реального сертификата и соединение сломалось, но было бы хорошо, если бы мы доверяли хранилищу ключей по умолчанию. 13 фев. 142014-02-13 19:25:02

  0

@erickson API немного вводит в заблуждение: это означает, что вы можете передать массив TrustManagers в SSLContext.init(), но javadoc этого метода указывает, что используется только первый TrustManager в массиве. Мне бы хотелось услышать любые идеи, которые вы можете поделиться. 13 фев. 142014-02-13 19:26:18

  0

@skiphoppy Мне нужно немного поэкспериментировать с этим и вернуться к вам. Раньше я этого не делал, но у меня есть идея, которая может работать. 13 фев. 142014-02-13 20:03:55

+1

@skiphoppy Хмм, единственный способ, которым я нашел, - создать временный «KeyStore» (в памяти) и добавить корневые сертификаты из всех ваших «реальных» хранилищ ключей к этому или (почти эквивалентно) создать 'Set <TrustAnchor>' из всех записей в ваших надежных хранилищах ключей и использовать их для инициализации 'PKIXParameters', а с этим, в свою очередь,« TrustManagerFactory ». 13 фев. 142014-02-13 21:48:48

  0

Спасибо @erickson Я все еще играю с этим, и я блуждаю по длинному лабиринту документации. Я сделаю это. 13 фев. 142014-02-13 22:03:09


12

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

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

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

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


12

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

+7

Если вы отключите все проверки доверия, в первую очередь следует использовать SSL/TLS. Это нормально для тестирования локально, но нет, если вы хотите подключиться снаружи. 23 дек. 112011-12-23 13:26:31

  0

Я получаю это исключение при запуске на Java 7. javax.net.ssl.SSLHandshakeException: нет общих комплектов шифров Можете ли вы помочь? 19 дек. 132013-12-19 12:55:10


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. Пароль: changeit

  3. Restart JVM

Источник: How to solve javax.net.ssl.SSLHandshakeException?

+1

Я не думаю, что это решает исходный вопрос, но он решил * мою * проблему, так что спасибо! 15 май. 132013-05-15 15:10:40


11

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

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

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