Я работаю над веб-приложением 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-кода (при необходимости)? Как я могу заставить браузер отображать эти всплывающие окна?
@aled, это именно то, что я видел. Я тоже видела required
, но еще не пробовала. Знаете, в чем разница?
Где вы видели в документации Tomcat, что true или false являются для него допустимыми значениями?
@aled Я видел это где-то еще... не помню где. И спасибо за предложение! Я проверил документацию и попробовал значения там, но не увидел никакой разницы. Я попробую еще кое-что... Но дайте мне знать, если подумаете о чем-нибудь еще, что я мог бы попробовать!
Наконец-то мы нашли решение! По сути, проблема заключалась в том, что используемые нами смарт-карты не содержат цепочки сертификатов. Чтобы аутентифицировать пользователей с помощью сертификатов, вам просто нужно добавить сертификат эмитента в 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
для дополнительного контроля над процессом аутентификации).
Почему для сертификатаVerification установлено значение
true
вместоrequired
?