Статьи

Взаимные проблемы

Протокол HTTPS является общепризнанным стандартом защиты наших соединений. Понимание того, как работает этот протокол, не является проблемой, и соответствующий документ RFC доступен с 2000 года.

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

Как работает HTTPS?

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

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

Вместе с моим товарищем по команде мы попытались внедрить клиент HTTPS в Java . Объединяя наши знания о рукопожатии TLS / SSL и опыт ручной проверки с использованием curl мы предположили, что для реализации клиентской стороны потребовалось всего три файла: сертификат клиента, личный ключ клиента и доверенный сертификат для проверки сертификата сервера .

О, как неправильно мы так думали.

Java — проблема, решение и почему это так сложно

Поскольку использование взаимной аутентификации каждый день довольно необычно, мы попросили лучшего источника в мире за небольшую помощь. Первый взгляд на результаты, предоставленные дядей Google , не выявил сложности реализации, но каждый щелчок по результатам приводил нас к все более и более запутанным решениям (некоторые из которых были сделаны в 90-х годах). Что еще хуже, мы должны были использовать Apache HttpComponents для реализации нашего соединения, но большинство предлагаемых решений были основаны на чистых библиотеках Java .

Знания из Интернета позволяют нам установить, что:

  • Java не может напрямую использовать какие-либо сертификаты или закрытые ключи (например, curl )
  • Java требует отдельных файлов ( Java Keystores ), которые могут содержать оригинальные сертификаты и ключи.
  • Нам требовалось надежное хранилище ключей с сертификатом, необходимым для проверки сертификата сервера для каждого соединения HTTPS.
  • Нам требовалось хранилище ключей с сертификатом клиента и личным ключом клиента для взаимной аутентификации.

Сначала мы должны были создать доверенное хранилище ключей. Мы создали хранилище ключей с сертификатом с помощью команды keytool :

1
$ keytool -import -alias trusted_certificate -keystore trusted.jks -file trusted.crt

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

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

К сожалению, Java поддерживает только формат PKCS12 . Поэтому нам пришлось перевести наш сертификат и закрытый ключ в формат PKCS12 . Мы можем сделать это используя OpenSSL.

1
2
3
4
5
$ openssl pkcs12 -export \
    -in client.crt \
    -inkey client.key \
    -out key.p12 \
    -name client

Мы сгенерировали файл key.p12 из файлов client.crt и client.key . Еще раз требуется ввод пароля. Этот пароль используется для защиты закрытого ключа.

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

1
2
3
4
5
6
7
8
$ keytool -importkeystore \
    -destkeystore key.jks \
    -deststorepass <<keystore_password>> \
    -destkeypass <<key_password_in_keystore>> \
    -alias client \
    -srckeystore key.p12 \
    -srcstoretype PKCS12 \
    -srcstorepass <<original_password_of_PKCS12_file>>

Эта команда выглядит немного более сложной, но ее довольно легко расшифровать. В начале команды мы объявляем параметры нового хранилища ключей с именем key.jks . Мы определяем пароль для хранилища ключей и пароль для закрытого ключа, который будет использоваться этим хранилищем ключей. Мы также назначаем закрытый ключ для некоторого псевдонима в хранилище ключей (в данном случае это client ). Далее мы указываем исходный файл ( key.p12 ), формат этого файла и оригинальный пароль.

С key.jks и key.jks мы были готовы к коду. На первом этапе мы должны были описать, как мы хотели использовать наши хранилища ключей.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
File trustedKeystoreFile = new File("trusted.jks");
File keystoreFile = new File("key.jks");
 
SSLContext sslcontext = SSLContexts.custom()
    .loadTrustMaterial(trustedKeystoreFile,
                    "<<trusted_keystore_password>>".toCharArray())
    .loadKeyMaterial(keystoreFile,
                    "<<keystore_password>>".toCharArray(),
                    "<<original_password_of_PKCS12_file>>".toCharArray())
    .build();
 
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
                sslcontext,
                new String[]{"TLSv1.2"},
                null,
                SSLConnectionSocketFactory.getDefaultHostnameVerifier());

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

И, наконец, мы смогли вызвать нашу конечную точку из Java :

01
02
03
04
05
06
07
08
09
10
11
12
try (CloseableHttpClient httpclient = HttpClients.custom()
        .setSSLSocketFactory(sslsf)
        .build()) {
 
    HttpGet httpGet = new HttpGet("https://ourserver.com/our/endpoint");
 
    try (CloseableHttpResponse response = httpclient.execute(httGet)) {
        HttpEntity entity = response.getEntity();
        System.out.println(response.getStatusLine());
        EntityUtils.consume(entity);
    }
}

Готово. После создания двух дополнительных файлов (хранилищ ключей), которые были эквивалентны нашему оригинальному сертификату и закрытому ключу, мы реализовали взаимную аутентификацию с Java . Возможно, реализация соединений HTTPS в Java имеет какое-то оправдание, но сейчас это просто головная боль.

Ссылка: Взаимные проблемы от нашего партнера JCG Роберта Фирека в блоге Crafted Software .