2009-05-13 11 views
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 – 2011-08-16 14:06:49

+16

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

ответ

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 – skiphoppy

+1

Я рад, что все получилось. Мне жаль, что ты боролся с хранилищем ключей; Я должен был просто включить его в свой ответ. С CertFactory это не сложно. На самом деле, я думаю, что сделаю обновление для всех, кто придет позже. – erickson

+0

Ничего себе. Это означает, что преобразование PEM в JKS значительно проще, чем то, что я сделал! Благодаря! :) – skiphoppy

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. Это нормально для тестирования локально, но нет, если вы хотите подключиться снаружи. – Bruno

+0

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

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

Я не думаю, что это решает исходный вопрос, но он решил * мою * проблему, так что спасибо! –

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

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