Статьи

Аутентификация клиента TLS

Я решил создать прототип для схемы электронной идентификации , поэтому я исследовал, как выполнить аутентификацию клиента TLS на стороне сервера Java / Spring (вы можете читать дальше, даже если вы не являетесь Java-разработчиком – большая часть поста – java -agnostic).

Почему аутентификация клиента TLS? Потому что это самый стандартный способ аутентификации пользователя, которому принадлежит сертификат (например, на смарт-карте). Конечно, сертификаты смарт-карт – не единственное приложение – организации могут выдавать внутренние сертификаты пользователям, которые они хранят на своих компьютерах. Дело в том, чтобы иметь механизм аутентификации, который был бы более безопасным, чем простая пара имя пользователя / пароль. Это проблема юзабилити, особенно со смарт-картами, но она выходит за рамки этого поста.

Таким образом, с TLS clientAuth, в дополнение к тому, что идентификатор сервера проверяется клиентом (через сертификат сервера), идентификатор клиента также проверяется сервером. Это означает, что у клиента есть сертификат, выданный органом, которому сервер явно доверяет. Грубо говоря, клиент должен подписать запрос в цифровой форме, чтобы доказать, что ему принадлежит закрытый ключ, соответствующий сертификату, который он предоставляет. (Этот процесс также можно найти в разделе «взаимная аутентификация» )

Есть два способа приблизиться к этому. Первый и самый интуитивно понятный – проверить, как настроить Tomcat (или ваш контейнер сервлетов). Страница аутентификации Spring Security x509 показывает конфигурацию Tomcat внизу. «Хранилище ключей» – это хранилище, в котором хранится сертификат сервера (+ закрытый ключ), а «trustStore» – это хранилище, в котором хранится корневой сертификат органа, который используется для подписи клиентских сертификатов.

Однако эта конфигурация применима только в том случае, если у вас есть один экземпляр контейнера сервлета, предоставленный вашим пользователям. Скорее всего, в производственной среде у вас будет несколько экземпляров / узлов, выполняющих ваше приложение за балансировщиком нагрузки, и TLS обычно прерывается на балансировщике нагрузки, который затем перенаправляет расшифрованные запросы в контейнер сервлета через простое соединение HTTP. В этом случае вы можете либо не прерывать TLS на балансировщике нагрузки, что, скорее всего, не очень хорошая идея, либо вам нужно каким-то образом перенаправить сертификат клиента с балансировщика нагрузки на ваш узел.

Я буду использовать nginx в качестве примера. Генерация пар ключей, сертификатов, запросов на подпись сертификатов, подписанных сертификатов и хранилищ ключей стоит отдельной публикации. Я обрисовал в общих чертах, что здесь необходимо . Вам нужны openssl и keytool / Portecle и куча команд. Для производства, конечно, это еще сложнее, потому что для сертификата сервера вам нужно отправить CSR в CA. Сделав это, в вашей конфигурации nginx у вас должно получиться что-то вроде:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
server {
   listen 443 ssl;
   server_name yourdomain.com;
 
   ssl_certificate server.cer;
   # that's the private key
   ssl_certificate_key server.key;
   # that holds the certificate of the CA that signed the client certificates that you trust. =trustStore in tomcat
   ssl_client_certificate ca.pem;
   # this indicates whether client authentication is required, or optional (clientAuth="true" vs "want" in tomcat)
   ssl_verify_client on;
 
   location / {
      #proxy_pass configuration here, inclding X-Forwarded-For headers
      proxy_set_header X-Client-Certificate $ssl_client_cert;
   }
}

Таким образом, сертификат клиента будет перенаправлен в качестве заголовка ( как указано здесь ). Это похоже на взлом, и, вероятно, так оно и есть, потому что сертификат клиента не совсем маленькая строка. Но это единственный способ, которым я могу думать.

Однако есть одна небольшая проблема (и то же самое для решения Tomcat) – если вы включите аутентификацию клиента для всего своего домена, у вас не будет полностью незащищенных страниц. Даже если аутентификация является необязательной («хочу»), диалоговое окно браузера (из которого пользователь выбирает сертификат) все равно будет запущено независимо от того, какие страницы пользователь открывает в первую очередь. Хорошо, что пользователь без сертификата все равно сможет просматривать страницы, которые явно не защищены кодом. Но для человека, у которого есть сертификат, открытие домашней страницы откроет диалоговое окно, даже если он может не захотеть пройти аутентификацию. Есть что-то, что можно сделать, чтобы справиться с этим.

Я на самом деле видел, как это делается с Perl «на страницу», но я не уверен, что это можно сделать с помощью установки Java. Что ж, это возможно, если вы не используете контейнер сервлетов, а обрабатываете свои рукопожатия TLS самостоятельно. Но это не желательно.

Обычно диалог аутентификации браузера необходим только для одного URL. «/ Login», или, как в моем случае с моим форком MitreID реализации OpenID Connect , конечная точка «/ authenticate» (пользователь перенаправляется на URL-адрес провайдера идентификации / аутентификации, где обычно ему приходится вводить имя пользователя / пароль , но в этом случае ему придется просто выбрать соответствующий сертификат). Что можно сделать, это получить доступ к этой конкретной конечной точке из субдомена. Это означало бы наличие еще одного раздела «сервер» в конфигурации nginx с включенным поддоменом и ssl_verify_client on , в то время как обычный домен остается без какой-либо проверки сертификата клиента. Таким образом, только запросы к поддомену будут аутентифицированы.

Теперь, как сделать настоящую аутентификацию. Упомянутая выше реализация OpenID Connect использует пружинную защиту, но это может быть что угодно. Моя реализация поддерживает оба упомянутых выше случая (tomcat и nginx + tomcat). Это делает приложение осведомленным о балансировщике нагрузки, но вы можете смело выбирать тот или иной подход и избавляться от другой части кода.

Для подхода с X509Certificate получается просто следующими строками:

1
2
3
X509Certificate certs[] = (X509Certificate[]) request
       .getAttribute("javax.servlet.request.X509Certificate");
    // check if not empty and get the first one

Для подхода nginx-in-front это немного сложнее. Мы должны получить заголовок, преобразовать его в правильное состояние и затем проанализировать. (Обратите внимание, что я не использую фильтр X509 с пружинной защитой, потому что он поддерживает только подход с одним котом.)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
String certificateHeader =
    request.getHeader("X-Client-Certificate");
if (certificateHeader == null) {
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    return;
}
// the load balancer (e.g. nginx) forwards the certificate
// into a header by replacing new lines with whitespaces
// (2 or more). Also replace tabs, which sometimes nginx
// may send instead of whitespaces
String certificateContent = certificateHeader
     .replaceAll("\\s{2,}", System.lineSeparator())
     .replaceAll("\\t+", System.lineSeparator());
userCertificate = (X509Certificate) certificateFactory
    .generateCertificate(new ByteArrayInputStream(
        certificateContent.getBytes("ISO-8859-11")));

«Хакерство» теперь очевидно, потому что способ, которым nginx отправляет сертификат, закодированный PEM, но в одну строку. К счастью, строки разделяются каким-то пробелом (один раз это были пробелы, другой раз это были табуляции (на компьютере с Windows)), поэтому мы можем вернуть их к их исходному формату PEM (даже не обязательно зная, что строка PEM 64 символа). Может случиться так, что другие версии nginx или других серверов не ставят пробелы, поэтому может потребоваться разбиение на 64-символьные строки. Затем мы используем фабрику сертификатов X.509 для создания объекта сертификата.

Это в основном это. Затем мы можем использовать этот хитрый «трюк» для извлечения CN (общего имени) или любого другого уникально идентифицирующего поля из сертификата и использовать его для загрузки соответствующей пользовательской записи из нашей базы данных.

Вот так, или, по крайней мере, то, что я получил от моего доказательства концепции. Это нишевый вариант использования, и коммуникация между смарт-картами и компьютерами является большой проблемой для удобства использования , но для национальных безопасных схем e-id, для электронного банкинга и для внутренних приложений это, вероятно, неплохая идея.