Рубрики
Без рубрики

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

Специализированный учебник по сбоям SSL-квитирования и тому, как их исправить.

Автор оригинала: Kumar Chandrakant.

1. Обзор

Защищенный уровень сокета (SSL) – это криптографический протокол, который обеспечивает безопасность при передаче данных по сети. В этом уроке мы обсудим различные сценарии, которые могут привести к сбою SSL-квитирования, и как это сделать.

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

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

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

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

3. Настройка

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

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

В Java мы можем использовать s pockets для установления канала связи между сервером и клиентом по сети . Сокеты являются частью расширения 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!” подключенному клиенту.

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

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 обеспечивает секретность, целостность и подлинность сетевых коммуникаций. Сертификаты играют важную роль в установлении подлинности.

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

Для достижения этой цели мы можем использовать инструмент для ключей, который поставляется с JDK:

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

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

Обратите внимание, что serverkeystore.jks хранится в формате хранилища ключей Java (JKS), который является собственностью Java. В эти дни keytool напомнит нам, что мы должны рассмотреть возможность использования PKCS#12, который он также поддерживает.

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

$ 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 можно найти в нашем предыдущем руководстве .

4. SSL-рукопожатие

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

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

Типичными шагами в SSL-рукопожатии являются:

  1. Клиент предоставляет список возможных версий SSL и наборов шифров для использования
  2. Сервер соглашается с конкретной версией SSL и набором шифров, отвечая своим сертификатом
  3. Клиент извлекает открытый ключ из сертификата и отвечает зашифрованным “предварительным главным ключом”
  4. Сервер расшифровывает “предварительный главный ключ”, используя свой закрытый ключ
  5. Клиент и сервер вычисляют “общий секрет”, используя обмененный “предварительный главный ключ”
  6. Клиент и сервер обмениваются сообщениями, подтверждающими успешное шифрование и дешифрование с использованием “общего секрета”

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

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

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

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

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

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

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

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

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

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

Давайте попробуем запустить SimpleServer и подключить его через Simple Client . В то время как мы ожидаем увидеть сообщение “Привет, мир!”, мы представляем исключение:

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

Теперь это указывает на то, что что-то пошло не так. Исключение SSLHandshakeException выше, абстрактно, указывает, что клиент при подключении к серверу не получил никакого сертификата.

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

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

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

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

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

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

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

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

Этот конкретный сбой вызван тем, что наш сервер использует самозаверяющий сертификат, который не подписан Центром сертификации (ЦС).

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

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

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

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

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

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

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

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

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

((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=baeldung, 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 по умолчанию не требует проверки имени хоста. Мы включили проверку имени хоста в Простом клиенте с помощью явного использования HTTPS:

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

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

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

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

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

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

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

((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)

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

5.6. Несовместимый набор шифров

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

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

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

((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

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

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

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

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

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