В большинстве корпоративных сред в соединениях между приложениями используется некоторая форма защищенной связи (например, TLS или SSL). В некоторых средах взаимная (двусторонняя) аутентификация также является нефункциональным требованием. Это иногда называют двусторонней SSL или взаимной аутентификацией TLS. Так же, как и сервер, представляющий свой сертификат, он запрашивает у клиента отправку своего сертификата, чтобы затем его можно было использовать для аутентификации вызывающей стороны.
Партнер моего текущего клиента разрабатывал сервер, который получает данные через MQTT, и, поскольку данные достаточно чувствительны, клиент решил, что данные должны быть защищены с использованием взаимной аутентификации TLS. Кроме того, клиент требует, чтобы, когда агрегированные данные, которые собирает этот сервер, были размещены в последующих нисходящих сервисах, это также выполнялось с использованием взаимной аутентификации TLS. Этот сервер должен представить сертификат сервера своим абонентам, чтобы они могли проверить имя хоста и идентификационные данные, но дополнительно он должен представить сертификат клиента с действительным идентификатором пользователя на нижестоящий сервер при запросе сделать это во время рукопожатия SSL.
Первоначальная идея заключалась в том, чтобы реализовать это с использованием стандартных системных свойств JVM для настройки хранилища ключей: «-Djavax.net.ssl.keyStore =…», т.е. поместить сертификаты клиента и сервера в одно хранилище ключей. Однако вскоре мы поняли, что это не работает, и отслеживание журналов отладки SSL показало, что сервер представлял неверный сертификат, либо во время входящего SSL-рукопожатия, либо исходящего SSL-рукопожатия. Во время входящего рукопожатия он должен представить свой сертификат сервера. Во время исходящего рукопожатия он должен представить свой сертификат клиента.
Следующие выдержки из журналов были аннотированы и показывают проблемы:
После дальнейших исследований выяснилось, что проблема связана с реализацией менеджера ключей по умолчанию в JVM.
Класс SunX509KeyManagerImpl
используется для выбора сертификата, который JVM должна представить во время рукопожатия, а для выбора сертификата клиента и сертификата сервера код просто берет первый найденный сертификат:
1
2
3
4
|
String[] aliases = getXYZAliases(keyTypes[i], issuers); if ((aliases != null ) && (aliases.length > 0 )) { return aliases[ 0 ]; <========== NEEDS TO BE MORE SELECTIVE } |
Псевдонимы, возвращаемые методом в первой строке, просто соответствуют типам ключей (например, DSA) и необязательным источникам. Так что в случае, когда хранилище ключей содержит два или более сертификатов, это недостаточно избирательно. Кроме того, порядок списка основан на итерации по набору записей HashMap, поэтому порядок не является, скажем, алфавитным, но он является детерминированным и постоянным. Поэтому при поиске сертификата сервера алгоритм может вернуть сертификат клиента. Однако, если эта часть работает, алгоритм потерпит неудачу, когда сервер установит нисходящее соединение и должен будет представить свой сертификат клиента, так как снова будет представлен первый сертификат, а именно сертификат сервера. Таким образом, поскольку невозможно создать одновременные входящие и исходящие двусторонние SSL-соединения, я подал ошибку в Oracle (внутренний идентификатор ID 9052786 сообщается в Oracle на 20180225).
Одним из решений является использование двух хранилищ ключей, по одному для каждого сертификата, как показано здесь .
Возможным исправлением для JVM было бы сделать алгоритм более избирательным, используя
Расширение сертификата «расширенный ключ» . По сути, приведенный выше код может быть улучшен для дополнительной проверки использования расширенного ключа и принятия более обоснованного решения при выборе псевдонима, например:
1
2
3
4
5
6
7
8
|
String[] aliases = getXYZAliases(keyTypes[i], issuers); if ((aliases != null ) && (aliases.length > 0 )) { String alias = selectAliasBasedOnExtendedKeyUsage(aliases, "1.3.6.1.5.5.7.3.2" ); //TODO replace with constant if (alias != null ) return alias; //default as implemented in openjdk return aliases[ 0 ]; } |
Метод выбора псевдонима будет следующим:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
private String selectAliasBasedOnExtendedKeyUsage(String[] aliases, String targetExtendedKeyUsage) { for (String alias : aliases){ //assume cert in index 0 is the lowest one in the chain, and check its EKU X509Certificate certificate = this .credentialsMap.get(alias).certificates[ 0 ]; List ekus = certificate.getExtendedKeyUsage(); for (String eku : ekus) { if (eku.equals(targetExtendedKeyUsage)){ return alias; } } } return null ; } |
Более подробная информация, включая полностью запущенный пример и модульные тесты, доступна здесь .
См. Оригинальную статью здесь: проблема Java с взаимной аутентификацией TLS при одновременном использовании входящих и исходящих соединений
Мнения, высказанные участниками Java Code Geeks, являются их собственными. |