Сбои рукопожатия SSL

Сбой SSL рукопожатия

1. обзор

Secured Socket Layer (SSL) - это криптографический протокол, который обеспечивает безопасность связи по сети. In this tutorial, we’ll discuss various scenarios that can result in an SSL handshake failure and how to it.с

Обратите внимание, что нашIntroduction to SSL using JSSE более подробно описывает основы SSL.

2. терминология

Важно отметить, что из-за уязвимостей системы безопасности SSL как стандарт заменяется безопасностью транспортного уровня (TLS). Большинство языков программирования, включая Java, имеют библиотеки для поддержки как SSL, так и TLS.

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

3. Настроить

Для целей этого руководства мы создадим простые серверные и клиентские приложения, используяthe Java Socket API для имитации сетевого подключения.

3.1. Создание клиента и сервера

В Java мы можем использоватьsockets to establish a communication channel between a server and client over the network. Сокеты являются частью Java Secure Socket Extension (JSSE) в Java.

Начнем с определения простого сервера:

int port = 8443;
ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
try (ServerSocket listener = factory.createServerSocket(port)) {
    SSLServerSocket sslListener = (SSLServerSocket) listener;
    sslListener.setNeedClientAuth(true);
    sslListener.setEnabledCipherSuites(
      new String[] { "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" });
    sslListener.setEnabledProtocols(
      new String[] { "TLSv1.2" });
    while (true) {
        try (Socket socket = sslListener.accept()) {
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            out.println("Hello World!");
        }
    }
}

Определенный выше сервер возвращает сообщение «Hello World!» Подключенному клиенту.

Затем давайте определим базового клиента, которого мы подключим к нашемуSimpleServer:

String host = "localhost";
int port = 8443;
SocketFactory factory = SSLSocketFactory.getDefault();
try (Socket connection = factory.createSocket(host, port)) {
    ((SSLSocket) connection).setEnabledCipherSuites(
      new String[] { "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" });
    ((SSLSocket) connection).setEnabledProtocols(
      new String[] { "TLSv1.2" });

    SSLParameters sslParams = new SSLParameters();
    sslParams.setEndpointIdentificationAlgorithm("HTTPS");
    ((SSLSocket) connection).setSSLParameters(sslParams);

    BufferedReader input = new BufferedReader(
      new InputStreamReader(connection.getInputStream()));
    return input.readLine();
}

Наш клиент печатает сообщение, возвращаемое сервером.

3.2. Создание сертификатов в Java

SSL обеспечивает секретность, целостность и аутентичность в сетевых коммуникациях. Сертификаты играют важную роль в установлении подлинности.

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

Для этого мы можем использоватьkeytool, w, который поставляется с JDK:

$ keytool -genkey -keypass password \
                  -storepass password \
                  -keystore serverkeystore.jks

Приведенная выше команда запускает интерактивную оболочку для сбора информации для сертификата, например Common Name (CN) и Distinguished Name (DN). Когда мы предоставляем всю необходимую информацию, он генерирует файлserverkeystore.jks, который содержит закрытый ключ сервера и его открытый сертификат.

Обратите внимание, чтоserverkeystore.jks  хранится в формате Java Key Store (JKS), который является проприетарным для Java. These days, keytool will remind us that we ought to consider using PKCS#12, which it also supports.

Далее мы можем использоватьkeytool to для извлечения публичного сертификата из сгенерированного файла хранилища ключей:

$ keytool -export -storepass password \
                  -file server.cer \
                  -keystore serverkeystore.jks

Приведенная выше команда экспортирует открытый сертификат из хранилища ключей как файлserver.cer. Давайте использовать экспортированный сертификат для клиента, добавив его в его хранилище доверенных сертификатов:

$ keytool -import -v -trustcacerts \
                     -file server.cer \
                     -keypass password \
                     -storepass password \
                     -keystore clienttruststore.jks

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

И более подробную информацию об использовании хранилища ключей Java можно найти в нашемprevious tutorial.

4. Подтверждение SSL

Подтверждения SSL составляютa mechanism by which a client and server establish the trust and logistics required to secure their connection over the network.

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

Типичные шаги в рукопожатии SSL:

  1. Клиент предоставляет список возможных версий SSL и наборов шифров для использования

  2. Сервер соглашается на конкретную версию SSL и набор шифров, отвечая своим сертификатом

  3. Клиент извлекает открытый ключ из сертификата и отвечает зашифрованным «предварительным главным ключом»

  4. Сервер расшифровывает «предварительный мастер-ключ», используя свой закрытый ключ

  5. Клиент и сервер вычисляют «общий секрет», используя обмененный «предварительный мастер-ключ»

  6. Клиент и сервер обмениваются сообщениями, подтверждающими успешное шифрование и дешифрование с использованием «общего секрета»

Хотя большинство шагов одинаковы для любого рукопожатия SSL, между односторонним и двусторонним SSL есть небольшая разница. Давайте быстро рассмотрим эти различия.

4.1. Рукопожатие в одностороннем SSL

Если мы ссылаемся на шаги, упомянутые выше, на шаге два упоминается обмен сертификатами. Односторонний SSL требует, чтобы клиент мог доверять серверу через свой открытый сертификат. Этоleaves the server to trust all clients, которые запрашивают соединение. Сервер не может запрашивать и проверять общедоступный сертификат у клиентов, что может представлять угрозу безопасности.

4.2. Рукопожатие в двустороннем SSL

При использовании одностороннего SSL сервер должен доверять всем клиентам. Но двусторонний SSL добавляет возможность для сервера также устанавливать доверенных клиентов. Во время двустороннего рукопожатияboth the client and server must present and accept each other’s public certificates перед установкой успешного соединения.

5. Сценарии отказа рукопожатия

Сделав этот быстрый обзор, мы можем более четко рассмотреть сценарии сбоев.

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

В каждом из этих сценариев мы будем использоватьSimpleClient иSimpleServer, которые мы создали ранее.

5.1. Отсутствует сертификат сервера

Давайте попробуем запуститьSimpleServer и подключить его черезSimpleClient. Хотя мы ожидаем увидеть сообщение «Hello World!», Мы представляем исключение:

Exception in thread "main" javax.net.ssl.SSLHandshakeException:
  Received fatal alert: handshake_failure

Теперь это означает, что что-то пошло не так. SSLHandshakeException выше, абстрактно,is stating that the client when connecting to the server did not receive any certificate.

Для решения этой проблемы мы будем использовать хранилище ключей, которое мы сгенерировали ранее, передавая их в качестве системных свойств серверу:

-Djavax.net.ssl.keyStore=clientkeystore.jks -Djavax.net.ssl.keyStorePassword=password

Важно отметить, что системное свойство для пути к файлу хранилища ключей должно быть либо абсолютным путем, либо файл хранилища ключей должен быть помещен в тот же каталог, из которого вызывается команда Java для запуска сервера. Java system property for keystore does not support relative paths.с

Помогает ли это нам получить ожидаемый результат? Давайте узнаем в следующем подразделе.

5.2. Сертификат ненадежного сервера

Когда мы снова запустимSimpleServer иSimpleClient с изменениями в предыдущем подразделе, что мы получим в качестве вывода:

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

Ну, это не сработало так, как мы ожидали, но похоже, что это не удалось по другой причине.

Эта конкретная ошибка вызвана тем, что наш сервер использует сертификатself-signed, который не подписан центром сертификации (CA).

Really, any time the certificate is signed by something other than what is in the default truststore, we’ll see this error. Склад доверенных сертификатов по умолчанию в JDK обычно поставляется с информацией об используемых общих центрах сертификации.

Чтобы решить эту проблему здесь, нам придется заставитьSimpleClient доверять сертификату, представленномуSimpleServer. Давайте воспользуемся хранилищем доверенных сертификатов, которое мы создали ранее, передав их клиенту как системные свойства:

-Djavax.net.ssl.trustStore=clienttruststore.jks -Djavax.net.ssl.trustStorePassword=password

Обратите внимание, что это не идеальное решение. In an ideal scenario, we should not use a self-signed certificate but a certificate which has been certified by a Certificate Authority (CA) which clients can trust by default.с

Давайте перейдем к следующему подразделу, чтобы узнать, получим ли мы ожидаемый результат сейчас.

5.3. Отсутствует сертификат клиента

Давайте попробуем еще раз запустить SimpleServer и SimpleClient, применив изменения из предыдущих подразделов:

Exception in thread "main" java.net.SocketException:
  Software caused connection abort: recv failed

Опять же, не то, что мы ожидали. SocketException здесь говорит нам, что сервер не может доверять клиенту. Это потому, что мы настроили двусторонний SSL. В нашемSimpleServer we:

((SSLServerSocket) listener).setNeedClientAuth(true);

Приведенный выше код указывает, чтоSSLServerSocket требуется для аутентификации клиента через его открытый сертификат.

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

Мы перезапустим сервер и передадим ему следующие системные свойства:

-Djavax.net.ssl.keyStore=serverkeystore.jks \
    -Djavax.net.ssl.keyStorePassword=password \
    -Djavax.net.ssl.trustStore=servertruststore.jks \
    -Djavax.net.ssl.trustStorePassword=password

Затем мы перезапустим клиент, передав следующие системные свойства:

-Djavax.net.ssl.keyStore=clientkeystore.jks \
    -Djavax.net.ssl.keyStorePassword=password \
    -Djavax.net.ssl.trustStore=clienttruststore.jks \
    -Djavax.net.ssl.trustStorePassword=password

Наконец, у нас есть желаемый результат:

Hello World!

5.4. Неверные сертификаты

Помимо вышеперечисленных ошибок, рукопожатие может потерпеть неудачу из-за множества причин, связанных с тем, как мы создали сертификаты. Одна распространенная ошибка связана с неправильным CN. Давайте подробно рассмотрим созданное нами ранее хранилище ключей сервера:

keytool -v -list -keystore serverkeystore.jks

Когда мы запускаем указанную выше команду, мы можем видеть детали хранилища ключей, в частности, владельца:

...
Owner: CN=localhost, OU=technology, O=example, L=city, ST=state, C=xx
...

CN владельца этого сертификата установлен на localhost. CN владельца должен точно соответствовать хосту сервера. Если есть какое-либо несоответствие, это приведет кSSLHandshakeException.

Давайте попробуем повторно сгенерировать сертификат сервера с CN как что-нибудь кроме localhost. Когда мы используем регенерированный сертификат сейчас для запускаSimpleServer иSimpleClient, он сразу же терпит неудачу:

Exception in thread "main" javax.net.ssl.SSLHandshakeException:
    java.security.cert.CertificateException:
    No name matching localhost found

Приведенная выше трассировка исключений ясно указывает на то, что клиент ожидал сертификат с именем localhost, который он не нашел.

Обратите внимание, чтоJSSE does not mandate hostname verification by default. Мы включили проверку имени хоста вSimpleClient посредством явного использования HTTPS:

SSLParameters sslParams = new SSLParameters();
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
((SSLSocket) connection).setSSLParameters(sslParams);

Проверка имени хоста является распространенной причиной сбоев и, как правило, всегда должна применяться для обеспечения большей безопасности. Дополнительные сведения о проверке имени хоста и ее важности для безопасности с помощью TLS см. Вthis article.

5.5. Несовместимая версия SSL

В настоящее время используются различные криптографические протоколы, в том числе разные версии SSL и TLS.

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

Например, если сервер использует криптографический протокол SSL3, а клиент использует TLS1.3, они не могут договориться о криптографическом протоколе, и будет сгенерированSSLHandshakeException.

В нашемSimpleClient давайте изменим протокол на то, что несовместимо с протоколом, установленным для сервера:

((SSLSocket) connection).setEnabledProtocols(new String[] { "TLSv1.1" });

Когда мы снова запустим наш клиент, мы получимSSLHandshakeException:

Exception in thread "main" javax.net.ssl.SSLHandshakeException:
  No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

Трассировка исключений в таких случаях является абстрактной и не говорит нам точную проблему. To resolve these types of problems it is necessary to verify that both the client and server are using either the same or compatible cryptographic protocols.с

5.6. Несовместимый Cipher Suite

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

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

В нашемSimpleClient давайте изменим набор шифров на что-то, несовместимое с набором шифров, используемым нашим сервером:

((SSLSocket) connection).setEnabledCipherSuites(
  new String[] { "TLS_RSA_WITH_AES_128_GCM_SHA256" });

Когда мы перезапустим наш клиент, мы получимSSLHandshakeException:

Exception in thread "main" javax.net.ssl.SSLHandshakeException:
  Received fatal alert: handshake_failure

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

Как правило, клиенты и серверы настроены на использование широкого набора наборов шифров, поэтому вероятность возникновения этой ошибки меньше. If we encounter this error it is typically because the server has been configured to use a very selective cipher. Сервер может выбрать принудительное применение выборочного набора шифров по соображениям безопасности.

6. Заключение

В этом уроке мы узнали о настройке SSL с использованием сокетов Java. Затем мы обсудили рукопожатия SSL с односторонним и двусторонним SSL. Наконец, мы рассмотрели список возможных причин, по которым рукопожатия SSL могут потерпеть неудачу, и обсудили решения.

Как всегда, доступен код для примеровover on GitHub.