В прошлый роз я писал про замечательный пакет opensc-pkcs11, в котором есть программа pkcs11-tool, позволяющая работать с USB-токенами, и в частности, с Rutoken (если скачать и установить нужную библиотеку-модуль librtpkcs11ecp.so).
Можно использовать USB-токен как аппаратное хранилище для паролей шифрования, защищенное пинкодом.
А сейчас - о том, как можно использовать USB-токен в качестве средства авторизации пользователей при входе на сайты.
Но начнем с конца:
Вместе с pkcs11-tool есть в пакете и программа pkcs11-register.
Что она делает - она "учит" другие программы работать с PKCS#11, например, браузеры.
Запускается она примерно вот так, однократно:
pkcs11-register --module librtpkcs11ecp.so
при этом она создает ряд записей в настроечных файлах.
Например, для Firefox будет создан файл ~/.mozilla/firefox/-current-profile-/pkcs11.txt, в котором будут указаны настройки для работы с PKCS#11, с правильным модулем и т.д.
Если после этого запустить Firefox, зайти в настройки, в раздел Privacy & Security, найти кнопку Security Devices - там должен появиться подключенный к компьютеру Rutoken.
Но есть нюанс: Rutoken Lite, о котором шла речь в прошлой статье, тут будет совершенно бесполезен: он не понимает «сертификаты», а браузер, в свою очередь, не понимает «просто записи».
Зато с сертификатами умеет работать полноценный Rutoken ЭЦП, со встроенным криптомодулем. Тот, который в описании у продавцов «подходит для ЕГАИС» (ну и разумеется, его аналоги, но их у меня нет, остановлюсь на Рутокене ЭЦП).
Отличие в том, что ЭЦП-версия не только умеет записывать данные типа data, но понимает разницу между публичными, приватными ключами и сертификатами, умеет создавать ключевые пары, верифицировать сертификаты и т.д. - и всё это через ту же самую программу pkcs11-tool, либо через соотвествующие библиотеки от нее (с которыми теперь умеет работать и Firefox).
То есть, заметно функциональнее.
А теперь — как можно это использовать к своей выгоде:
Можно настроить «клиентский сертификат» — сертификат будет записан на токене и отправляться при соединении с сервером, сервер сможет проверить правильность сертификата, и тем самым определить, кто к нему подключился.
То есть, вместо логина с паролем, двухфакторной авторизации и прочего — вставить в компьютер ключ, зайти на сайт, ввести свой пинкод, выбрать свой сертификат — и залогиниться в систему под нужными правами.
Разумеется, это такая больше корпоративная тема, «для своих» (выдача сертификатов, ключей, контроль доступа — вот это всё).
Допустим, нам надо такое - попробуем это настроить:
Прежде всего, понадобится CA, он же - Удостоверяющий Центр, УЦ.
Задача УЦ - удостоверить, что сертификат Васи Пупкина выдан действительно Васе Пупкину, а не сгенерирован каким-то посторонним челом.
Строго говоря, Вася Пупкин может и сам сгенерировать себе сертификат, главное - подписать его в УЦ, но сейчас не будем лишний раз усложнять.
Для организации УЦ существует специальный софт, также существуют специальные наборы скриптов типа EasyRSA, но традиционно напишу свой велосипед, чисто для понимания как оно устроено.
По сути всё просто:
в УЦ есть свой приватный и открытый ключи
сгенерированные сертификаты (неважно кем) подписываются на приватном ключе УЦ, что может сделать только держатель УЦ
если кто-то предьявляет сертификат - его можно проверить на открытом ключе УЦ, и понять, настоящий он или поддельный
также проверяется владение приватным ключом хозяина сертификата - то есть нельзя просто так взять и отправить чужой сертификат вместо своего
поскольку приватные ключи на токене неизвлекаемые - только тот, кто держит токен и знает пинкод, может отправить правильный сертификат, а значит скорее всего это хозяин и есть (исключая ректальный криптоанализ или иные силовые методы).
Для всего этого понадобятся несколько скриптов и системные утилиты, часть из которых уже установлена
apt install opensc-pkcs11 openssl libengine-pkcs11-openssl
Первый скрипт будет собственно для создания CA/УЦ:
нужно подготовить рабочие каталоги, списки для выданных и отозванных сертификатов, и пару ключей.
#!/bin/bash
# Create CA
DIR='./ca'
mkdir -p ${DIR}/{certs,crl,newcerts,private}
chmod 700 ${DIR}/private
touch ${DIR}/index.txt
echo 1000 > ${DIR}/serial
echo 1000 > ${DIR}/crlnumber
cat <<EOF > ${DIR}/openssl.cnf
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = './ca'
database = \$dir/index.txt
serial = \$dir/serial
private_key = \$dir/private/ca.key.pem
certificate = \$dir/certs/ca.cert.pem
crl = \$dir/crl/ca.crl.pem
new_certs_dir = \$dir/newcerts
crl_dir = \$dir/crl
certs = \$dir/certs
crlnumber = \$dir/crlnumber
default_md = sha256
default_crl_days = 40
policy = policy_any
[ policy_any ]
commonName = supplied
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
emailAddress = optional
EOF
# private key CA
openssl genrsa -out ${DIR}/private/ca.key.pem 4096
chmod 400 ${DIR}/private/ca.key.pem
# public key CA
openssl req -x509 \
-days 3650 \
-key ${DIR}/private/ca.key.pem \
-out ${DIR}/certs/ca.cert.pem
# empty revoke list
openssl ca -gencrl -config ${DIR}/openssl.cnf -out ${DIR}/crl/ca.crl.pem
# check
openssl x509 -in ${DIR}/certs/ca.cert.pem -noout -text |more
Скрипт создает каталоги, инициирует файлы, создает ключи, а также подготавливает пустой список отозванных сертификатов - он нужен для того, чтобы сразу подключить его к сайту, хотя ни одного сертификата еще нет.
Теперь у нас есть публичный ключ CA, список отозванных сертификатов CRL, и всё это нужно подключить к сайту.
На примере Nginx это будет выглядеть так:
server { listen 443 ssl; server_name example.com; # это СЕРВЕРНЫЕ ключи, для HTTPS, это никак не связано! ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; # а вот это - контроль клиентов ssl_client_certificate /etc/ssl/ca/ca.cert.pem; ssl_crl /etc/ssl/ca/ca.crl.pem; ssl_verify_client optional; # пробросим сертификат в backend proxy_set_header X-SSL-CERT $ssl_client_escaped_cert; proxy_set_header X-SSL-CLIENT-SUBJECT $ssl_client_s_dn; proxy_set_header X-SSL-CLIENT-VERIFY $ssl_client_verify; ... ... }
Что это дает? Теперь при входе на сайт браузер предложит выбрать клиентский сертификат, он будет проверен на публичном ключе ca.cert.pem, по списку отозванных ca.crl.pem, и если пройдет валидацию — в бекенд будут переданы заголовки HTTP, содержащие весь сертификат целиком, его Subject и флаг успешной валидации.
Останется только разобрать эти заголовки, и принять решение, например, какие права выдавать юзеру с такими параметрами.
Теперь нужно создать сам клиентский сертификат. Для начала — простой, файлами, для понимания принципа:
#!/bin/bash
# Create user certificate
DIR='./ca'
read -e -p "Enter user nickname: " user_nick
if [ "x$user_nick" = "x" ]; then
exit 0
fi
if [ ! -f "${DIR}/newcerts/$user_nick.key.pem" ]; then
openssl genrsa -out "${DIR}/newcerts/$user_nick.key.pem" 2048
fi
if [ ! -f "${DIR}/newcerts/$user_nick.key.pem" ]; then
echo "ERROR: private key is not created!"
exit 1
fi
rm -f "${DIR}/newcerts/$user_nick.csr.pem"
openssl req -new \
-key "${DIR}/newcerts/$user_nick.key.pem" \
-out "${DIR}/newcerts/$user_nick.csr.pem" \
-subj "/CN=$user_nick"
if [ ! -f "${DIR}/newcerts/$user_nick.csr.pem" ]; then
echo "ERROR: certificate request is not created!"
exit 1
fi
rm -f "${DIR}/newcerts/$user_nick.cert.pem"
openssl ca -config ${DIR}/openssl.cnf \
-days 365 -notext -md sha256 \
-in "${DIR}/newcerts/$user_nick.csr.pem" \
-out "${DIR}/newcerts/$user_nick.cert.pem"
if [ ! -f "${DIR}/newcerts/$user_nick.cert.pem" ]; then
echo "ERROR: certificate is not signed!"
exit 1
fi
ls ca/*/${user_nick}.*
echo "Signed certificate: ${DIR}/newcerts/$user_nick.cert.pem"
openssl verify \
-CAfile ${DIR}/certs/ca.cert.pem \
"${DIR}/newcerts/$user_nick.cert.pem"
Создается закрытый ключ, затем запрос на сертификацию, затем запрос подписывается ключом CA, затем просто проверка, что получилось.
А получились два файла, приватный ключ и публичный сертификат (запрос не считаем).
Теперь то же самое — для USB‑токена:
#!/bin/bash
# Create user certificate
export PKCS11_MODULE_PATH=/usr/lib/librtpkcs11ecp.so
DIR='./ca'
read -e -p "Enter user nickname: " user_nick
if [ "x$user_nick" = "x" ]; then
exit 0
fi
read -s -p "Enter token PIN: " user_pin
if [ "x$user_pin" = "x" ]; then
exit 0
fi
UPIN="$user_pin" pkcs11-tool --module $PKCS11_MODULE_PATH --pin env:UPIN \
--keypairgen --key-type rsa:2048 \
--label "$user_nick"
openssl req -new -engine pkcs11 \
-keyform engine \
-key "pkcs11:object=$user_nick;type=private" \
-subj "/CN=$user_nick" \
-out "${DIR}/newcerts/$user_nick.csr.pem"
openssl ca -config ${DIR}/openssl.cnf \
-days 365 -notext -md sha256 \
-in "${DIR}/newcerts/$user_nick.csr.pem" \
-out "${DIR}/newcerts/$user_nick.cert.pem"
UPIN="$user_pin" pkcs11-tool --module $PKCS11_MODULE_PATH --pin env:UPIN \
--write-object "${DIR}/newcerts/$user_nick.cert.pem" --type cert \
--label "$user_nick"
echo "Signed certificate: ${DIR}/newcerts/$user_nick.cert.pem"
openssl verify \
-CAfile ${DIR}/certs/ca.cert.pem \
"${DIR}/newcerts/$user_nick.cert.pem"
UPIN="$user_pin" pkcs11-tool --module $PKCS11_MODULE_PATH --pin env:UPIN -O --type cert
Создается пара ключей на токене, затем на их основании создается запрос на сертификацию, подписывается, сертификат помещается обратно на токен.
И наконец, еще одна нужная процедура — отзыв сертификата. Если по какой‑то причине сертификат необходимо отменить до истечения срока действия — он отзывается принудительно:
#!/bin/sh
DIR='./ca'
if [ "x$1" = "x" ]; then
exit 0
fi
if [ ! -f "$1" ]; then
exit 0
fi
openssl ca -config ${DIR}/openssl.cnf \
-revoke "$1" \
-keyfile ${DIR}/private/ca.key.pem \
-cert ${DIR}/certs/ca.cert.pem
openssl ca -gencrl -config ${DIR}/openssl.cnf -out ${DIR}/crl/ca.crl.pem
openssl crl -in ${DIR}/crl/ca.crl.pem -noout -text
openssl verify -crl_check \
-CRLfile ${DIR}/crl/ca.crl.pem \
-CAfile ${DIR}/certs/ca.cert.pem \
"$1"
Останется скопировать полученный ca.crl.pem на сервер, что можно вообще делать по крону автоматически. Включенные в него сертификаты не пройдут проверку.
Теперь подключаем USB‑токен к компьютеру, заходим на сайт с настроенной проверкой сертификатов — всё должно работать.
Если проверка прошла успешно — в http‑запросе на сервере появляются заданные заголовки, как их будет обрабатывать бекенд — это уже выходит за рамки данной статьи.
Скрипты собрал в один и закинул на Гитхаб.