Как получить всплывающее меню для выбора сертификата пользователя на сервере Tomcat 10?

Я работаю над веб-приложением Java, которое я разверну на сервере Tomcat 10, который запрашивает у пользователя сертификат (смарт-карты) для его аутентификации (после ввода своих обычных учетных данных пользователя). (См. также этот вопрос, в котором описано больше об этом.) Настроив правильную конфигурацию сервера Tomcat, я подключил свое веб-приложение через https (через порт 8443). Затем, при необходимости, он отправит https-запрос на другой порт (8444) для запроса сертификата пользователя.

Однако проблема, с которой я столкнулся, заключается в том, что браузер никогда не предлагает мне выбрать один из доступных сертификатов, как я видел, как это работает у других, которые делают то же самое (см. этот вопрос и этот сайт). Я протестировал свой браузер на таких сайтах, как Login.gov и других правительственных сайтах: у меня появляется всплывающее меню, в котором я могу выбрать сертификат, поэтому я знаю, что проблема не в настройках моего браузера. Я даже могу открыть настройки браузера и просмотреть сертификаты на подключенной смарт-карте, чтобы знать, что она их видит.

В настоящее время мой server.xml имеет такие https-коннекторы:

<Connector
    port = "8444" secure = "true" scheme = "https" maxThreads = "150" SSLEnabled = "true"
    maxParameterCount = "1000" protocol = "org.apache.coyote.http11.Http11NioProtocol">
    
    <SSLHostConfig
        sslProtocol = "TLSv1.2" certificateVerification = "true"
        truststoreFile = "C:/Program Files/Java/jdk-17/lib/security/cacerts"
        truststorePassword = "changeit">
        
        <Certificate
            certificateKeystoreFile = "path/to/keystore.jks"
            certificateKeystorePassword = "password" type = "RSA"/>
    </SSLHostConfig>
</Connector>

Разъем порта 8443 практически идентичен, но с certificateVerification = "false". (Обновление от 17.06.24: я пробовал certificateVerification = "required" вместо "true" и "optional" или "none" вместо "false", но безрезультатно.)

В моем Java-сервлете у меня есть:

// there are various types and providers, but these were the ones that worked
KeyStore keyStore = KeyStore.getInstance("Windows-MY", "SunMSCAPI");
// load local certificates (the certificates visible in your browser)
keyStore.load(null);
// get all the names of the available certificates
Enumeration<String> aliases = keyStore.aliases();

что позволяет мне видеть те же сертификаты, которые я вижу в настройках браузера. (Обновление от 17.06.24: после прошлых выходных я больше не вижу список сертификатов... и понятия не имею, почему.) Затем я делаю запрос на соответствующий URL-адрес через порт 8444:

URL url = new URL(certificateRequestUrl);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setRequestMethod("GET");
Integer responseCode = connection.getResponseCode(); // error thrown here

Я получаю ошибку javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate. Согласно этому обсуждению, я думаю, что ошибка означает, что сервер не доверяет сертификату, представленному во время установления связи SSL, - но он даже не спросил меня, какой сертификат использовать! Я знаю, что URL-адрес структурирован правильно, потому что я получаю ожидаемый ответ, когда для certificateVerification установлено значение "false" на порту, на который отправляется запрос.

Обновление от 18.06.2024: я добавил Java Option (в Tomcat) -Djavax.net.debug=ssl и увидел некоторые подробности:

javax.net.ssl|ERROR|F3|https-jsse-nio-8444-exec-6|2024-06-18 13:29:13.491 MDT|TransportContext.java:363|Fatal (BAD_CERTIFICATE): Empty client certificate chain (
"throwable" : { javax.net.ssl.SSLHandshakeException: Empty client certificate chain at ... }
)
javax.net.ssl|WARNING|F3|https-jsse-nio-8444-exec-6|2024-06-18 13:29:13.494 MDT|SSLEngineOutputRecord.java:173|outbound has closed, ignore outbound application data
javax.net.ssl|DEBUG|E2|https-jsse-nio-8443-exec-1|2024-06-18 13:29:13.497 MDT|Alert.java:238|Received alert message (
"Alert": {
  "level"      : "fatal",
  "description": "bad_certificate"
}
)
javax.net.ssl|ERROR|E2|https-jsse-nio-8443-exec-1|2024-06-18 13:29:13.500 MDT|TransportContext.java:363|Fatal (BAD_CERTIFICATE): Received fatal alert: bad_certificate (
"throwable" : {
  javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate at ... }
)

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

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

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

Я знаю, что и Java, и мой браузер могут видеть мои сертификаты, так почему же мой браузер не предоставляет всплывающее меню для выбора сертификата и ввода PIN-кода (при необходимости)? Как я могу заставить браузер отображать эти всплывающие окна?

Почему для сертификатаVerification установлено значение true вместо required?

aled 14.06.2024 20:26

@aled, это именно то, что я видел. Я тоже видела required, но еще не пробовала. Знаете, в чем разница?

Izek H 14.06.2024 23:12

Где вы видели в документации Tomcat, что true или false являются для него допустимыми значениями?

aled 15.06.2024 01:34

@aled Я видел это где-то еще... не помню где. И спасибо за предложение! Я проверил документацию и попробовал значения там, но не увидел никакой разницы. Я попробую еще кое-что... Но дайте мне знать, если подумаете о чем-нибудь еще, что я мог бы попробовать!

Izek H 17.06.2024 19:54
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
4
83
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Наконец-то мы нашли решение! По сути, проблема заключалась в том, что используемые нами смарт-карты не содержат цепочки сертификатов. Чтобы аутентифицировать пользователей с помощью сертификатов, вам просто нужно добавить сертификат эмитента в truststore, а не сам сертификат карты (как мы пытались). Другими словами, вы не можете сказать: «Я доверяю себе, поэтому все остальные должны доверять мне» — кто-то другой должен сказать, что вам доверяют. Итак, мы получили сертификат эмитента карты и добавили его на сервер truststore.

Еще мы изменили server.xml вот так:

<Connector
    port = "8444" secure = "true" scheme = "https" maxThreads = "150" SSLEnabled = "true"
    maxParameterCount = "1000" protocol = "org.apache.coyote.http11.Http11NioProtocol">
    
    <SSLHostConfig
        sslProtocol = "TLS" certificateVerification = "required"
        truststoreFile = "path/to/truststore"
        truststorePassword = "password">
        
        <Certificate
            certificateKeystoreFile = "path/to/keystore.jks"
            certificateKeystorePassword = "password" type = "RSA"/>
    </SSLHostConfig>
</Connector>

Обратите внимание, что certificateVerification = "required" — это критическое изменение.

Ошибка javax.net.ssl.SSLHandshakeException: Empty client certificate chain просто означает, что сертификат на смарт-карту получен, но цепочки сертификатов не было, или следующий сертификат в цепочке не найден (на сервере truststore и т.п.).

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

Другие вопросы по теме