Анонс
В предыдущей статье "Сам себе PKI: Теория на примере Let’s Encrypt" мы рассмотрели теорию и разобрали пример, как происходит установка HTTPS соединения между браузером и веб-сайтом при использовании сертификатов Let's Encrypt. В этой статье перейдем к практике и самостоятельно реализуем ту же самую схему:
Сначала вручную (с помощью OpenSSL) пройдем цепочку генерации и использования самоподписанных сертификатов в HTTPS сервере.
Далее развернем собственный удостоверяющий центр Smallstep и будем получать сертификаты, подписанные им.
Схема PKI остаётся той же, только изменяются компоненты:
В качестве CA вместо Let's Encrypt, будет использоваться OpenSSL и CA Smallstep.
В роли Web-сервера вместо Интернет веб-сайта, будет выступать простой HTTPS сервер, запущенный на Node.js.
Практика Self-signed certificate
OpenSSL
Начнем с классики криптографии OpenSSL. Сейчас широко используется версия 1.1.1, но ее поддержка заканчивается в 2023. Следующая версия 3.0 будет поддерживаться до 2026 года. Проверка установленной версии:
openssl version
OpenSSL 1.1.1k FIPS 25 Mar 2021
Если OpenSSL не установлен, то его можно установить на всех востребованных платформах.
Реализация TLS
Для нашего CA генерируем приватный ключ 2048-бит RSA. Администратор CA должен хранить его в секрете от всех:
openssl genrsa -out root_ca.key 2048
Далее для нашего CA генерируем X.509 сертификат на 365 дней (root_ca.crt) и подписываем его приватным ключом (root_ca.key). Получается самоподписанный сертификат, его можно и нужно будет скопировать на все компьютеры, между которыми необходимо организовать TLS соединение. При ответах на вопросы, которые будет задавать OpenSSL, можно указывать произвольные данные, CA я назвал "My CN":
openssl req -x509 -new -key root_ca.key -days 365 -out root_ca.crt
Смотрим получившийся сертификат, убеждаемся, что данные в нем корректные:
openssl x509 -text -in root_ca.crt
Генерируем приватный ключ для HTTPS сервера, его нужно будет скопировать только на компьютер, на котором он будет работать и никуда больше:
openssl genrsa -out server.key 2048
Генерируем запрос на сертификат CSR. В этом запросе нужно передать конкретные значения в параметре "subj". Это должны быть один или несколько параметров CN (Common Name), которые будут прописаны в атрибутах сертификата "Subject" и "Subject Alternative Name". Фактически CN-ы - это IP адреса и имена хостов, по которым будет осуществляться доступ к серверу. Имена хостов могут быть как в формате доменного имени, так и FQDN:
openssl req -new -key server.key -subj "/CN=xx.xx.xx.xx/CN=server/CN=server.example.com" -out server.csr
Можно посмотреть получившийся запрос на сертификат и убедиться в его корректности:
openssl req -text -in server.csr
Теперь генерируем сертификат X.509 (server.crt) на 365 дней для HTTPS сервера и подписываем его приватным ключом CA (root_ca.key):
openssl x509 -req -in server.csr -CA root_ca.crt -CAkey root_ca.key -CAcreateserial -out server.crt -days 365 -extensions SAN -extfile openssl.cnf
Файл openssl.cnf должен содержать список SAN (Subject Alternative Name) идентичный тому, что был в CSR запросе на сертификат:
[SAN]
subjectAltName = @alt_names
[alt_names]
IP.1 = xx.xx.xx.xx
DNS.1 = server
DNS.2 = server.example.com
Можно посмотреть на получившийся сертификат и убедиться в его корректности:
openssl x509 -text -in root_ca.crt
-
С ключами и сертификатами закончили, переходим к запуску HTTPS сервера. Копируем следующие файлы на компьютер, на котором будет работать HTTPS сервер:
root_ca.crt
server.key
server.crt
Пишем простейший HTTPS сервер, на JS:
const fs = require('fs');
const https = require('https');
https
.createServer(
{
requestCert: false,
rejectUnauthorized: false,
ca: fs.readFileSync('root_ca.crt'),
cert: fs.readFileSync('server.crt'),
key: fs.readFileSync('server.key')
},
(req, res) => {
console.log("req.client.authorized: ", req.client.authorized)
if (req.client.authorized) {
const cert = req.socket.getPeerCertificate(false) // true - получить все сертификаты, false - последний
console.log("cert: ", cert)
}
res.end('Hello, world!');
}
)
.listen(9443);
Запускать данный скрипт будем с помощью Node.js. Если Node.js не установлен, то устанавливаем:
sudo yum groupinstall 'Development Tools'
sudo yum install nodejs
Проверяем, что Node и Npm установились успешно:
npm -v
node -v
Запускаем сервер:
node server.js
Проверяем сервер curl-ом:
curl https://server.example.com:9443
При первом запросе возникнет ошибка:
curl: (60) SSL certificate problem: self signed certificate in certificate chain
Это значит, что самоподписанный корневой сертификат нашего CA (root_ca.crt) не находится в хранилище доверительных сертификатов. Процесс загрузки сертификатов в это хранилище специфичен для операционной системы. Приведу алгоритм для RH Linux:
Если сертификат не в PEM (текстовом) формате, то его надо сконвертировать в текстовой формат:
openssl x509 -in root_ca.crt -out root_ca.pem -outform PEM
Копируем сертификат в определенный каталог на сервере. ДляRH 7 и выше это каталог:
/etc/pki/ca-trust/source/anchors
Выполняем команду обновления хранилища сертификатов:
update-ca-trust
Теперь curl проходит
curl https://server.example.com:9443
Hello, world!
на Windows есть специфика - в curl необходимо добавлять параметр "ssl-no-revoke", чтобы не проверялся список отозванных сертификатов:
curl --ssl-no-revoke https://server.example.com:9443
При этом сервер выведет в консоль строку:
req.client.authorized: false
Это значит, что мы установили TLS соединение только с проверкой сертификата сервера, а не клиента. Переходим к mTLS.
Реализация mTLS
В коде сервера включаем mTLS, а именно выставляем в true параметр requestCert, чтобы HTTPS сервер запрашивал клиентский сертификат и параметр rejectUnauthorized, чтобы сервер отклонял запросы клиентов, которые не прошли авторизацию:
{
requestCert: true,
rejectUnauthorized: true,
}
Алгоритм генерации клиентских сертификатов аналогичен серверным.
Генерируем приватный ключ для клиента:
openssl genrsa -out client.key 2048
Генерируем запрос на сертификат CSR со списком CN:
openssl req -new -key client.key -subj "/CN=xx.xx.xx.xx/CN=client/CN=client.example.com" -out client.csr
Генерируем сертификат для клиента и подписываем его приватным ключом CA:
openssl x509 -req -in client.csr -CA root_ca.crt -CAkey root_ca.key -CAcreateserial -out client.crt -days 365 -extensions SAN -extfile openssl.cnf
Файл openssl.cnf - такой же как и для сервера, только вместо "server" пишем "client".
-
Копируем следующие файлы на компьютер, на котором будет работать клиент:
root_ca.crt
client.key
client.crt
Теперь если просто вызвать curl:
curl https://server.example.com:9443
То произойдет ошибка авторизации
curl: (35) error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure
Поэтому вызываем curl с дополнительными параметрами, в которых указываем ключ клиента и сертификаты клиента и СА:
curl --cert ./client.crt --key client.key --cacert root_ca.crt https://server.example.com:9443
Теперь запрос проходит, при этом сервер выведет в консоль следующую строку и далее содержимое полученного от клиента сертификата:
req.client.authorized: true
Это значит, что мы установили mTLS соединение с проверкой сертификата как сервера, так и клиента. Теперь установим mTLS из браузера.
Реализация mTLS в браузере
Если просто запустить наш запрос к серверу (https://server.example.com:9443) из браузера, то выдается ошибка клиентского сертификата ERR_BAD_SSL_CLIENT_AUTH_CERT. Рабочие адреса закрасил желтым:
Для загрузки в браузер клиентский сертификат и приватный ключ надо сконвертировать в формат p12. Этот формат определяется стандартом PKCS #12 и является архивом для хранения нескольких объектов. В нашем случае файл будет хранить клиентский сертификат и его приватный ключ. Понятно, что приватный ключ - это чувствительная к разглашению информация и ее владелец не должен выкладывать p12 сертификат в открытый доступ. Для конвертации тоже используем OpenSSL:
openssl pkcs12 -inkey client.key -in client.crt -export -out client.p12
Импортируем client.p12 в браузер. Для этого, например в Chrome, идем в меню: Settings / Privacy and security / Manage certificates, в открывшемся окне нажимаем Import, выбираем наш файл client.p12 и место хранения "Personal". Видим, что клиентский сертификат, выданный нашим удостоверяющим центром "My CN", появился в хранилище персональных сертификатов:
Вызываем запрос заново, уже лучше, но браузер все равно пишет, что соединение не безопасно. Это из-за того, что клиентский сертификат выдан удостоверяющим центром "My CN", которого нет в списке доверительных CA (Trusted Root Certification Authorities).
Добавляем корневой сертификат нашего CA (root_ca.crt) в Trusted Root Certification Authorities. Делается с помощью того же вызова Import, что и для клиентского сертификата, но хранилище указываем Trusted Root Certification Authorities. Проверяем, что наш CA "My CN" появился в хранилище:
Повторяем наш запрос (https://server.example.com:9443). При первом запросе браузер находит подходящий клиентский сертификат и просит явно подтвердить его использование:
Дальнейшие вызовы проходят без запросов на подтверждение клиентского сертификата:
Если щелкнуть на замочке а адресной строке, то можно посмотреть сертификат сервера и убедиться, что мы взаимодействуем с правильным сервером (Issued to: server.example.com), сертификат которому выдан нашим удостоверяющим центром (Issued by: My CN) и время действия сертификата еще не прошло (Valid to: 31.05.2023):
Таким образом мы организовали mTLS соединение из браузера. Далее приведу для справки несколько дополнительных возможностей OpenSSL, которые могут пригодиться в процессе работы.
Дополнительные возможности OpenSSL
Конвертация форматов сертификатов
Конвертируем сертификат в der (бинарный формат):
openssl x509 -outform der -in client.crt -out client.der
Конвертируем сертификат в PEM (текстовой формат)
openssl x509 -inform der -in client.cer -out client.pem
Отладка с помощью OpenSSL
Проверка, что сертификат подписан определенным (root_ca.pem) удостоверяющим центром:
openssl verify -verbose -x509_strict -CAfile root_ca.pem server.crt
Запускаем OpenSSL server
openssl s_server -accept 9443 -cert server.crt -key server.key
Запускаем OpenSSL client
openssl s_client -connect xx.xx.xx.xx:9443 -prexit -showcerts -state -status -tlsextdebug -verify 10
-
При вызове curl параметр:
-k - позволяет подсоединяться к серверу без проверки цепочки сертификатов (certificate path validation).
--ssl-no-revoke - предотвращает проверку отозванных сертификатов
Практика CA Smallstep
Установка Smallstep и CLI step
CA Smallstep - это open source CA, который можно развернуть локально для автоматического получения X.509 сертификатов. На официальном сайте есть ряд инструкций по его установке и настройке. Для экспериментов я использовал вариант с docker:
Краткая выжимка из инструкции по установке на RH Linux. Сначала устанавливаем docker:
sudo yum -y update
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum -y install docker-ce
sudo systemctl start docker
Проверяем корректность установки docker:
sudo docker run hello-world
Запускаем Smallstep:
sudo docker pull smallstep/step-ca
Инициализируем наш CA:
docker run -it -v step:/home/step smallstep/step-ca step ca init
При инициализации по умолчанию будет ряд ограничений, например, будут одинаковые пароли для корневого и промежуточного приватного ключа. Для разворота в промышленную среду необходимо внимательно изучить рекомендации Production considerations when running a certificate authority server.
При создании CA в консоль будет выведен fingerprint корневого сертификата в виде шестнадцатеричной строки (например: 55de8dc05cd...8a70f725a56). Ее надо сохранить для последующей инициализации утилиты CLI step, которая используется для взаимодействия с CA Smallstep. Проверяем, что CA Smallstep запущен корректно. xx.xx.xx.xx:9000 - это IP и порт, на котором запущен Smallstep CA:
curl https://xx.xx.xx.xx:9000/health
должна вернуться строка:
"status": "ok"
На этом же или другом компьютере по инструкции устанавливаем утилиту CLI step. Краткая выжимка:
Загружаем бинарник step: https://github.com/smallstep/cli/releases/tag/v0.19.0
Распаковываем:
tar -xf step_linux_0.19.0_386.tar.gz
Копируем утилиту step в /usr/bin
Регистрируем CLI step для использования с нашим CA. Этот процесс называется "bootstrap". Значение параметра "ca-url" - это IP и порт, на котором запущен Smallstep CA, "fingerprint " - это сохраненное значение при инициализации CA:
step ca bootstrap --ca-url https://xx.xx.xx.xx:9000 --fingerprint 55de8dc05cd...8a70f725a56 --install
Проверка связи с CA:
step ca health
должна вернуться строка:
"ok"
Все готово для генерации ключей и сертификатов.
Генерация ключей и сертификатов
Также как и в случае с OpenSSL, сначала генерируем ключ и сертификат для сервера и установки TLS соединения, потом для клиента и установки mTLS.
В отличии от OpenSSL, CA Smallstep может одновременно сформировать приватный ключ сервера 2048-бит RSA (server.key) и запрос на сертификат (server.csr). В запросе явно указываем, что пароль должен быть пустой (no-password), xx.xx.xx.xx - это IP адрес сервера, для которого генерируется запрос:
step certificate create --csr --no-password --insecure --kty=RSA --size=2048 "xx.xx.xx.xx" server.csr server.key
Подписываем сертификат на нашем CA Smallstep:
step ca sign server.csr server.crt
Смотрим полученный сертификат:
step certificate inspect server.crt
Получаем с CA Smallstep его корневой сертификат:
step ca root root_ca.crt
Смотрим корневой сертификат:
step certificate inspect root_ca.crt
-
Аналогично OpenSSL копируем ключи и сертификаты на наш HTTPS сервер:
root_ca.crt
server.key
server.crt
Запускаем сервер:
node server.js
Проверяем сервер curl-ом:
curl https://xx.xx.xx.xx:9443
Hello, world!
Все нормально, теперь генерируем ключ и сертификат для клиента.
Аналогично серверу одновременно формируем приватный ключ клиента 2048-бит RSA (client.key) и запрос на сертификат (client.csr). Явно указываем, что пароль должен быть пустой (no-password), xx.xx.xx.xx - это IP адрес клиента, для которого генерируется запрос:
step certificate create --csr --no-password --insecure --kty=RSA --size=2048 "xx.xx.xx.xx" client.csr client.key
Подписываем сертификат на CA:
step ca sign client.csr client.crt
Смотрим сертификат:
step certificate inspect client.crt
Получаем корневой сертификат CA:
step ca root root_ca.crt
Смотрим корневой сертификат:
step certificate inspect root_ca.crt
-
Копируем на клиента:
root_ca.crt
client.key
client.crt
Для проверки вызываем curl с параметрами:
curl --cert ./client.crt --key client.key --cacert root_ca.crt https://xx.xx.xx.xx:9443
Hello, world!
Отлично - mTLS тоже работает с выданными CA Smallstep сертификатами!
Дополнительные возможности CA Smallstep
С помощью CLI step можно проверить необходимость обновления сертификата:
step certificate needs-renewal server.crt
Если обновление не требуется возвращается строка:
certificate does not need renewal
Если требуется, то строка пустая и тогда необходимо обновить сертификат:
step ca renew server.crt server.key
При необходимости сертификат можно отозвать, в этом случае его невозможно будет продлить, только получить заново:
step ca revoke --cert server.crt --key server.key
Посмотреть все сертификаты в цепочке:
step certificate inspect server.crt --bundle
Запуск CLI step в режиме отладки:
STEPDEBUG=1 step /команда/
Заключение
В данной статье мы рассмотрели две возможности генерации ключей и сертификатов для клиента и сервера:
С помощью OpenSSL.
Используя CA Smallstep.
И двумя способами установили TLS и mTLS соединения:
С помощью curl.
Из браузера, загрузив в него клиентский и корневой сертификаты.
Теперь у нас есть понимание, как организовать свою систему PKI с помощью CA любого производителя, т.к. логика взаимодействия у всех аналогичная. Более того, большинство CA используют под капотом OpenSSL, оборачивая его в свой API.
saipr
А можно еще использовать CAFL63 с графическим интерфейсом: