Протокол 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()) { try (CloseableHttpResponse response = httpclient.execute(httGet)) { HttpEntity entity = response.getEntity(); System.out.println(response.getStatusLine()); EntityUtils.consume(entity); } } |
Готово. После создания двух дополнительных файлов (хранилищ ключей), которые были эквивалентны нашему оригинальному сертификату и закрытому ключу, мы реализовали взаимную аутентификацию с Java . Возможно, реализация соединений HTTPS в Java имеет какое-то оправдание, но сейчас это просто головная боль.
Ссылка: | Взаимные проблемы от нашего партнера JCG Роберта Фирека в блоге Crafted Software . |