Руководство разработчика Java по сертификатам SSL

Обзор

При разработке веб-приложений нам часто требуется интегрироваться с другими приложениями с использованием SSL. Это может быть по различным протоколам, таким как HTTPS, IMAPS или LDAPS.

В этой статье мы расскажем, что Java-разработчикам нужно знать о сертификатах SSL.

Цепочка сертификатов

SSL-соединение устанавливается успешно только в том случае, если клиент может доверять серверу. Давайте посмотрим, как работает эта модель доверия.

В Chrome перейдите на google.com и откройте Инструменты разработчика (F12 в Windows, Cmd + Option + i в Mac).

На вкладке «Безопасность» нажмите кнопку «Просмотр сертификата», чтобы отобразить сведения о сертификате.

Мы видим, что сертификат сайта является частью цепочки. Эта конкретная цепочка состоит из 3 сертификатов.

Сертификат сайта был выдан сертификатом с именем Google Internet Authority G2. Это промежуточный сертификат. В свою очередь, промежуточный сертификат выдается корневым сертификатом GeoTrust Global CA.

Когда мы устанавливаем соединение через HTTPS, веб-сервер отвечает, предоставляя свой сайт и промежуточные сертификаты. Затем клиент должен завершить цепочку, имея корневой сертификат. Эта проверка цепочки необходима для того, чтобы клиент доверял сайту.

Поскольку Chrome имеет корневой сертификат GeoTrust Global CA в своем хранилище сертификатов, наше соединение успешно, и мы не получаем никаких ошибок или предупреждений.

Примечание. В зависимости от сайта в цепочке может быть несколько промежуточных сертификатов.

Самоподписанные сертификаты

Сертификаты, выпущенные не известным ЦС, а скорее сервером, на котором размещен сертификат, называются самозаверяющими.

Они часто используются во внутренних средах разработки, не предназначенных для клиентов.

Корневые сертификаты для них будут отсутствовать в хранилище сертификатов браузера.

Пример самоподписанного сертификата находится на https://self-signed.badssl.com. Мы видим, что это было выдано ненадежным центром сертификации Avast, который браузер не распознает, поэтому отображает предупреждение.

Java Truststore и KeyStore

В этом разделе мы обсудим, где находятся сертификаты в системе, где установлен JDK / JRE.

Truststore

хранилище доверенных сертификатов - это файл, содержащий корневые сертификаты для центров сертификации (CA), которые выдают сертификаты, такие как GoDaddy, Verisign, Network Solutions и другие.

Склад доверенных сертификатов поставляется в комплекте с JDK / JRE и находится в $JAVA_HOME/lib/security/cacerts.

Склад доверенных сертификатов используется всякий раз, когда наш код Java устанавливает соединение через SSL.

Хранилище ключей

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

Итак, если бы мы запускали веб-приложение через SSL на tomcat.codebyamir.com, файл хранилища ключей с именем keystore.jks содержал бы две записи - одну для закрытого ключа и одну для сертификата.

Хранилище ключей используется серверами приложений Java, такими как Tomcat, для обслуживания сертификатов.

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

Keytool

Keytool - это утилита в комплекте с JRE для управления парами ключей и сертификатами. Это позволяет нам просматривать / изменять / создавать хранилища сертификатов в мире Java.

Список сертификатов в хранилище доверенных сертификатов

keytool -list -keystore $JAVA_HOME/lib/security/cacerts

Нам будет предложено ввести пароль для доверенного магазина. Пароль по умолчанию - «changeit».

Это хранилище доверенных сертификатов содержит 104 записи, и каждая запись имеет уникальный псевдоним и отпечаток. Мы для краткости усекли вывод ниже.

Keystore type: JKS
Keystore provider: SUN
Your keystore contains 104 entries
verisignclass2g2ca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): B3:EA:C4:47:76:C9:C8:1C:EA:F2:9D:95:B6:CC:A0:08:1B:67:EC:9D
digicertassuredidg3 [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): F5:17:A2:4F:9A:48:C6:C9:F8:A2:00:26:9F:DC:0F:48:2C:AB:30:89
verisignuniversalrootca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): 36:79:CA:35:66:87:72:30:4D:30:A5:FB:87:3B:0F:A7:7B:B7:0D:54
digicerttrustedrootg4 [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): DD:FB:16:CD:49:31:C9:73:A2:03:7D:3F:C8:3A:4D:7D:77:5D:05:E4
verisignclass1g3ca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): 20:42:85:DC:F7:EB:76:41:95:57:8E:13:6B:D4:B7:D1:E9:8E:46:A5
identrustpublicca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): BA:29:41:60:77:98:3F:F4:F3:EF:F2:31:05:3B:2E:EA:6D:4D:45:FD
utnuserfirstobjectca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): E1:2D:FB:4B:41:D7:D9:C3:2B:30:51:4B:AC:1D:81:D8:38:5E:2D:46
geotrustuniversalca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): E6:21:F3:35:43:79:05:9A:4B:68:30:9D:8A:2F:74:22:15:87:EC:79
digicertglobalrootg3 [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): 7E:04:DE:89:6A:3E:66:6D:00:E6:87:D3:3F:FA:D9:3B:E8:3D:34:9E
deutschetelekomrootca2 [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): 85:A4:08:C0:9C:19:3E:5D:51:58:7D:CD:D6:13:30:FD:8C:DE:37:BF
...

Используя предыдущий пример google.com, давайте посмотрим на отпечаток GeoTrust Global CA в нашем браузере:

Отпечаток SHA-1 - DE:28:F4:A4:FF:E5:B9:2F:A3:C5:03:D1:A3:49:A7:F9:96:2A:82:12

Давайте поищем это в нашем доверенном центре:

keytool -list -keystore $JAVA_HOME/lib/security/cacerts | grep -B1 -i DE:28
Enter keystore password:  changeit
geotrustglobalca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): DE:28:F4:A4:FF:E5:B9:2F:A3:C5:03:D1:A3:49:A7:F9:96:2A:82:12

Выходные данные говорят нам, что сертификат находится в хранилище доверенных сертификатов:

Что это значит для Java-разработчика?

Это означает, что код, подключающийся к https://www.google.com, не вызовет исключения из-за ошибки подтверждения SSL.

Добавить сертификат в хранилище доверенных сертификатов

Добавление сертификата в хранилище доверенных сертификатов необходимо, если мы хотим доверять сертификату, выпущенному из центра сертификации, отсутствующего в объединенном хранилище доверенных сертификатов.

keytool -import -trustcacerts -file [certificate] -alias [alias] -keystore $JAVA_HOME/lib/security/cacerts

Пример кода

Ниже приведен код Java, который будет подключаться к URL-адресу и печатать содержимое страницы на экране.

Подключение к сайту с помощью доверенного сертификата

Давайте попробуем наш код на другом сайте с действующим сертификатом SSL.

Код

Замените строку 12 кода этой строкой:

private static final String URL = "https://httpbin.org/user-agent";

Выход

Мы видим, что вывод кода успешно показывает, что наша строка пользовательского агента является нашей версией Java.

{
  "user-agent": "Java/1.8.0_131"
}

Подключение к сайту с ненадежным сертификатом

Давайте попробуем наш код на сайте с самозаверяющим сертификатом.

Код

Замените строку 12 кода этой строкой:

private static final String URL = "https://self-signed.badssl.com";

Выход

Код выдает SSLHandshakeException, потому что Java ему не доверяет.

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
	at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
	at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949)
	at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302)
	at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296)
	at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1514)
	at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216)
	at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1026)
	at sun.security.ssl.Handshaker.process_record(Handshaker.java:961)
	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062)
	at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
	at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
	at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1546)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474)
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
	at com.codebyamir.ssl.App.main(App.java:18)

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

После добавления сертификата повторный запуск кода успешно отображает содержимое страницы:

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="shortcut icon" href="/icons/favicon-red.ico"/>
  <link rel="apple-touch-icon" href="/icons/icon-red.png"/>
  <title>self-signed.badssl.com</title>
  <link rel="stylesheet" href="/style.css">
  <style>body { background: red; }</style>
</head>
<body>
<div id="content">
  <h1 style="font-size: 12vw;">
    self-signed.<br>badssl.com
  </h1>
</div>
</body>
</html>

Общие исключения проверки SSL

Сертификат с истекшим сроком действия

При подключении к сайту с истекшим SSL-сертификатом мы увидим следующее исключение:

java.security.cert.CertPathValidatorException: timestamp check failed

Неверное общее имя (CN)

При подключении к сайту с именем сертификата, отличным от имени хоста, мы увидим следующее исключение:

java.security.cert.CertificateException: No subject alternative DNS name matching wrong.host.badssl.com found.

часто задаваемые вопросы

Обновляется ли база доверенных сертификатов JRE cacerts?

Да, новые выпуски Oracle JDK / JRE будут добавлять новые сертификаты в хранилище доверенных сертификатов по мере необходимости.

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

При запуске приложения добавьте следующее свойство JVM:

-Djavax.net.ssl.trustStore=/app/security/truststore.jks

Если пароль хранилища доверенных сертификатов отличается от «changeit», также укажите пароль:

-Djavax.net.ssl.trustStorePassword=myTrustStorePassword

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

Мы можем использовать утилиту openssl в Linux, чтобы проверить это:

openssl s_client -showcerts -connect google.com:443