На страницах этого блога уже неоднократно затрагивались вопросы, связанные с защитой передаваемых по сети данных с использованием технологии SSL / TLS. Вопрос надёжности защищённых соединений в последнее время стал особенно актуальным для России, где на государственном уровне предполагается внедрение механизма для тотальной дешифровки всего трафика в Интернет одним из наиболее вероятных способов реализации которого, как раз, и может стать внедрение промежуточного сертификата для защищённых соединений.
В развитие темы, предлагаю вашему вниманию статью о внедрении относительно и новой пока малораспространённой технологии HTTP Public Key Pining (HPKP) и её использованию совместно с удостоверяющим центром Let's Encrypt.
Стандарт HPKP или key pining изложен в документе RFC7469 и описывает механизм, который призван повысить уровень защиты в WWW через дополнительную идентификацию и проверку того факта, что используемый в ходе защищённого соединения сертификат действительно принадлежит посещаемому сайту и не был подменён тем или иным заинтересованным лицом или структурой для несанкционированного доступа к передаваемой информации, то есть нейтрализации атак Man in the Middle (MitM).
Не вдаваясь глубоко в основы функционирования технологии SSL / TLS которые, впрочем, без проблем можно изучить начав, к примеру, с вышеуказанных ссылок, остановимся лишь на базовых моментах, понимание которых важно для выбора способа реализации и поддержания функционирования key pining для конкретного сайта.
Итак, в процессе соединения и передачи данных участвует набор цифровых сертификатов, который, сильно упрощая, представляют собой взаимосвязанные цифровые документы на базе электронных подписей на основании которых осуществляется идентификация того или иного сайта и принимается решение, можно ли этому сайту доверять.
Вопрос доверия это главный вопрос во всей системе. Для этого все сертификаты, которые используются конечными пользователями, как правило, подписываются одним или несколькими связанными сертификатами, которые принадлежат организациям с заведомо высоким уровнем доверия. Например, в случае с сертификатом, который используется на этом сайте, в качестве удостоверяющих его используется набор из двух других — корневой, принадлежащий одному из наиболее уважаемых удостоверяющих центров IdenTrust (DST Root CA X3) и промежуточный, принадлежащий удостоверяющему центру Let's Encrypt (Let's Encrypt Authority X1) выдавшему сайту конечный сертификат.
Все эти сертификаты, которые называются публичными и являются доступными всем, подписаны, в свою очередь, закрытыми ключами, которые гарантируют защиту их от подделки.
Во все системы уже загружен набор корневых сертификатов, которым доверят данный поставщик программного обеспечения и, следовательно, доверие будет оказано и выданным таким удостоверяющим центром от своего имени сертификатам.
Однако, как уже было упомянуто, такого способа выстраивания доверительных отношений может быть недостаточно ввиду возможной (и реально случавшейся в истории — см., например, взлом промежуточного сертификата StartCom в 2011 или уязвимость механизма выпуска сертификатов WoSign в 2016) компрометации системы выдачи сертификатов или даже выпуска поддельных сертификатов конечных пользователей и путём их подмены дешифрации трафика. Именно возможную проблему с подменой и призван решить HPKP.
Для этого на базе одного из сертификатов в используемой в защищённом соединении цепочки генерируется специальный цифровой отпечаток, который однозначно идентифицирует связку сайт - сертификат. При первом соединении этот отпечаток предоставляется сайтом - владельцем сертификата в специальном поле заголовка HTTP, запоминается клиентом и в последующем каждый раз в течении определённого времени проверяется на возможную подмену злоумышленником. Key pining для протокола HTTP напоминает аналогичный давно используемый механизм начальной идентификации при работе по SSH.
При этом для обеспечения дополнительной надёжности доступа к сайту, защищаемому при помощи key pining, в передаваемом наборе данных должно фигурировать, как минимум, два отпечатка — один из них должен соответствовать одному из используемых в цепочке сертификтов, а второй являться резервным на случай проблем с текущим набором удостоверяющих данных.
Отпечаток сертификата может быть создан на базе любого используемого в применяемой цепочке сертификата — корневого, промежуточного и конечного публичных, или закрытых, включая запрос на получение сертификата (CSR), ключей. Каждый из этих способов имеет как свои плюсы, так и минусы.
Например, выбор в качестве источника цифрового отпечатка корневого сертификата решает проблему с обновлением низлежащих сертификатов — промежуточный и конечный сертификаты, которые им подписаны, могут заменяться без потери доступа к сайту, защищаемого при помощи HPKP. Однако, это предполагает полное и безоговорочное доверие к выдавшей такой сертификат организации. Кроме того, как и любые, корневые сертификаты время от времени заменяются по истечению срока действия или, что тоже изредка случается, их компрометации.
Примерно те же преимущества и недостатки имеет и выбор для формирования отпечатка промежуточного сертификата. Такие сертификаты ротируются чаще, чем корневые, что повышает риск потери доступа к сайту в случае наступления такого события. По этой причине Let's Encrypt, к примеру, специально предостерегает от практики использования своих промежуточных сертификатов для key pining.
Использование конечных сертификатов — публичного, закрытого или CSR, снимает вопрос доверия и внезапной ротации сертификатов более высокой иерархии. При этом возникает несколько сопутствующих вопросов.
Принимая во внимание вышеизложенное, я решил остановиться на варианте использования механизма HPKP на базе публичного сертификата от Let's Encrypt.
Главным вопросом, который следует решить, является обеспечение доступности сайта при обновлении сертификатов. Для этого как нельзя кстати оказывается заложенное в стандарте требование по указанию как минимум одного резервного отпечатка, который должен принадлежать сертификату не участвующему в текущей сессии.
Хорошей идеей представляется использование двух действующих сертификатов — более старого, который будет использоваться в текущих соединениях, и более нового, который будет прописан в качестве резервного и будет использован взамен основного при очередной ротации. При этом обновлении заголовок HPKP также будет пересоздан и место старого сертификата займёт бывший до этого резервный, а резервным станет самый последний выпущенный.
Для иллюстрации см. отчёт о текущих отпечатках для этого сайта на момент публикации статьи.
Здесь первый (Valid Pin) отпечаток, который в настоящее время используется, соответствует сертификату выпущенному 4 сентября 2016 года, а второй (Backup Pin), резервный, выпущенному 4 октября 2016 года и пока не используемому.
Обратите также внимание на параметр Max Age, который указывает срок в течение которого сайт должен использовать сертификаты, отпечатки которых были предоставлены в заголовке HPKP при первом контакте с клиентом. Здесь указан срок примерно в 2 месяца (если быть точнее 60 секунд x 60 минут x 24 часа x 60 дней), что обусловлено периодичностью обновления и сроками действия выдаваемых Let's Encrypt сертификатов.
Напомню, что сами сертификаты от Let's Encrypt имеют срок действия в 90 дней. В данной системе они обновляются не ранее чем за 60 дней до срока их окончания, то есть примерно, через месяц. Это действие производится shell-скриптом, который запускается один раз в неделю. Максимальный срок использования любого сертификата на сайте kostikov.co не может быть более 37 дней. Таким образом, срок действия записи с key pining заведомо перекрывает этот срок и гарантирует неразрывность цепочки безопасности при замене сертификатов на новые.
Рассмотрим сам процесс внедрения выбранного варианта с сохранением преимуществ автоматизации процесса обновления сертификатов, которые предоставляет Let's Encrypt (см. статью "Получаем и обновляем SSL сертификат Let's Encrypt" в качестве вводного курса) на примере этого сайта.
Итак, имеем следующий набор программного обеспечения.
root@beta:~ # uname -v
FreeBSD 11.0-RELEASE #0 r306211: Thu Sep 22 21:43:30 UTC 2016 root@releng2.nyi.freebsd.org:/usr/obj/usr/src/sys/GENERIC
root@beta:~ # lighttpd -v
lighttpd/1.4.41 (ssl) - a light and fast webserver
Build-Date: Aug 17 2016 23:13:40
root@beta:~ # certbot --version
certbot 0.8.1
Для начала, следует уведомить клиент Let's Encrypt certbot о том, что теперь мы намерены обновлять сертификаты датой выпуска не не менее 60, как это делается по умолчанию, а 30 дней, как это предложено в реализуемой методике. Для этого достаточно изменить лишь одну строку в файле конфигурации, описывающем параметры получения и обновления сертификатов для сайта kostikov.co.
root@beta:~ # cd /usr/local/etc/letsencrypt/
root@beta:/usr/local/etc/letsencrypt # cat renewal/kostikov.co.conf
renew_before_expiry = 60 days
cert = /usr/local/etc/letsencrypt/live/kostikov.co/cert.pem
privkey = /usr/local/etc/letsencrypt/live/kostikov.co/privkey.pem
chain = /usr/local/etc/letsencrypt/live/kostikov.co/chain.pem
fullchain = /usr/local/etc/letsencrypt/live/kostikov.co/fullchain.pem
version = 0.8.1
# Options used in the renewal process
[renewalparams]
authenticator = webroot
installer = None
account = ...
[[webroot_map]]
www.kostikov.co = /usr/local/www/kostikov.co
kostikov.co = /usr/local/www/kostikov.co
Изменённый параметр renew_before_expiry запустит механизм обновления не ранее чем за 60 дней до окончания срока действия сертификата, что принимая во внимание те стандартные 90 дней, на которые он выпускается, и даст нам искомые не менее 30 и не более 37 дней использования до момента обновления.
Теперь модифицируем конфигурацию web-сервера Lighttpd для подготоваливаемого внедрения HPKP.
root@beta:/usr/local/etc/letsencrypt # cat ../lighttpd/lighttpd.conf
# --- by Max Kostikov (c) 2010...2016 v.20160928
...
var.conf_dir = "/usr/local/etc/lighttpd"
...
# Load the modules
include "modules.conf"
...
# --- path to Let's Encrypt certs storage
lepath = "/usr/local/etc/letsencrypt/live"
$SERVER["socket"] == ":443" {
protocol = "https://"
ssl.engine = "enable"
ssl.disable-client-renegotiation = "enable"
ssl.dh-file = "/etc/ssl/dhparams.pem"
ssl.ec-curve = "secp384r1"
setenv.add-environment = (
"HTTPS" => "on"
)
ssl.use-sslv2 = "disable"
ssl.use-sslv3 = "disable"
ssl.honor-cipher-order = "enable"
ssl.cipher-list = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS"
# -- main cert
ssl.ca-file = lepath + "/my.server/chain.cur.pem"
ssl.pemfile = lepath + "/my.server/privandcert.cur.pem"
include "pins.d/my.server.conf"
...
$HTTP["host"] == "kostikov.co" {
ssl.ca-file = lepath + "/kostikov.co/chain.cur.pem"
ssl.pemfile = lepath + "/kostikov.co/privandcert.cur.pem"
include "pins.d/kostikov.co.conf"
}
...
}
...
Здесь указано, что в качестве сертификатов для доменов будут использоваться ссылки на предпоследний выданный Let's Encrypt набор сертификатов, симлинки на которые сформированы в каталоге /usr/local/etc/letsencrypt/live с добавлением постфикса cur к имени.
Добавление заголовков с key pining производится индивидуально для каждого домена через включение подконфигурационных файлов одноимённых домену и расположенных в специальном подкаталоге /usr/local/etc/lighttpd/pins.d. Не забудем создать его.
root@beta:/usr/local/etc/letsencrypt # mkdir ../lighttpd/pins.d
root@beta:/usr/local/etc/letsencrypt # ll ../lighttpd/
total 36
-rw-r--r-- 1 root wheel 17 28 сент. 11:55 .htpasswd
drwxr-xr-x 2 root wheel 1536 17 авг. 23:14 conf.d/
-r--r--r-- 1 root wheel 6979 4 окт. 18:14 lighttpd.conf
-rw-r--r-- 1 root wheel 13541 17 авг. 23:14 lighttpd.conf.sample
-r--r--r-- 1 root wheel 1135 27 сент. 23:23 modules.conf
-rw-r--r-- 1 root wheel 3235 17 авг. 23:14 modules.conf.sample
drwxr-xr-x 2 root wheel 512 4 окт. 18:41 pins.d/
drwxr-xr-x 2 root wheel 512 17 авг. 23:14 vhosts.d/
В pins.d будут сформированы директивы LIghttpd для формирования специального HTTP заголовка с именем Public-Key-Pins, который будет содержать отпечатки наших сертификатов (pin-sha256) и срок действия key pinig (max-age) для них. Кроме того, могут быть добавлены дополнительные флаги для распространения действия на поддомены (директива includeSubdomains), а также специальная ссылка на сервер, который может обрабатывать отчёты об ошибках (report-uri).
Для этого в Lighttpd используется оператор setenv.add-response-header для работоспособности которого должен быть включён модуль mod_setenv.
root@beta:/usr/local/etc/letsencrypt # cat ../lighttpd/modules.conf
server.modules = (
...
"mod_setenv",
...
)
И, наконец, напишем shell-скрипт, который будет обновлять набор сертификатов Let's Encrypt на данном сервере, создавать файл содержащий публичный и закрытый ключи, как это требуется для реализации SSL / TLS на Lighttpd, создавать дополнительные симлинки на предыдущую версию сертификатов, а также фомировать файл с заголовками HPKP для включения в конфигурацию веб-сервера.
root@beta:/usr/local/etc/letsencrypt # cat lerenew.sh
#!/bin/sh
# Update "Let's Encrypt" certificates,
# make cert+key joined files and key pining hashes for Lighttpd
# (c)2016 by Max Kostikov http://kostikov.co e-mail: max@kostikov.co
#
# cat /etc/crontab | grep lerenew
# 0 0 * * 1 root /usr/local/etc/letsencrypt/lerenew.sh >/dev/null 2>&1
lepath="/usr/local/etc/letsencrypt"
log="/var/log/letsencrypt/lerenew.log"
pins="/usr/local/etc/lighttpd/pins.d" # where pins stored
joincrt="privandcert" # joined file name
curpref="cur" # current set postfix
echo "`date '+%Y-%m-%d %H:%M:%S'` --- Starting SSL certs renew..." >>$log
certbot renew -nv $* >>$log 2>&1
certs=`find ${lepath}/archive/*/cert*.pem -mtime -1d`
if [ "${certs}" ];
then
for i in $certs;
do
dir=`dirname $i` # path to certs
dom=`basename $dir` # domain name
ind=`basename $i | tr -d '[:alpha:].'` # latest cert index
cur=`expr $ind - 1` # previous cert index
# create new joined cert and symlink
cat ${dir}/privkey${ind}.pem $i > ${dir}/${joincrt}${ind}.pem
cd ${lepath}/live/${dom}
ln -fs ../../archive/${dom}/${joincrt}${ind}.pem ${joincrt}.pem
# create symlinks for previous certs set
for j in `ls -d ../../archive/${dom}/*${cur}.*`;
do
ln -fs $j `basename -s .pem $j | tr -d '[:digit:]'`.${curpref}.pem
done
# create cert pins
conf="${pins}/${dom}.conf" # pin config name
echo ' setenv.add-response-header = (' >$conf
echo ' "Strict-Transport-Security" => "max-age=15768000",' >>$conf
echo -n ' "Public-Key-Pins" => ' >>$conf
echo -n '"pin-sha256=\"' >>$conf
echo -n "`openssl x509 -in cert.${curpref}.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64`" >>$conf
echo -n '\"; pin-sha256=\"' >>$conf
echo -n "`openssl x509 -in cert.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64`" >>$conf
echo '\"; max-age=5184000"' >>$conf
echo ' )' >>$conf
done
service lighttpd restart
fi
echo "`date '+%Y-%m-%d %H:%M:%S'` --- SSL certs renew done!" >>$log
Данный скрипт будет запускаться еженедельно, проверять сроки действия сертификатов и при необходимости обновлять их. В последнем случае будет сформирован дополнительный набор ссылок на набор сертификатов предыдущей версии, на базе них и только что полученных новых будут сформированы отпечатки, которые будут добавлены с созданный файл с заголовками. Кроме того, туда же добавляется и используемый на данной системе заголовок Strict-Transport-Security для HSTS, призванный повысить защищённость передаваемых данных путем принуждения к использованию HTTPS.
Его исполнение предполагает, что ранее вручную уже были получены и сформированы необходимые сертификаты для начала работы (см. статью). Также перед началом реального использования всей выстроенной конструкции необходимо один раз запустить данный скрипт вручную с указанием ключа --force-renewal для принудительного обновления всей базы сертификтов Let's Encrypt. Это необходимо для формирования полного набора всех необходимых файлов и ссылок для запуска механизма HPKP в работу.
root@beta:/usr/local/etc/letsencrypt # /bin/sh lerenew.sh --force-renewal
...
Performing sanity check on lighttpd configuration:
Syntax OK
Stopping lighttpd.
Waiting for PIDS: 34976, 34976.
Starting lighttpd.
Сервер перезапустился и начал свою работу с новыми параметрами, которые теперь включают и механизм key pining. Дополнительно проконтролировать результаты его работы можно в соответствующих подкаталогах.
root@beta:/usr/local/etc/letsencrypt # ll live/kostikov.co/
total 0
lrwxr-xr-x 1 root wheel 35 4 окт. 20:45 cert.cur.pem@ -> ../../archive/kostikov.co/cert3.pem
lrwxr-xr-x 1 root wheel 35 4 окт. 19:41 cert.pem@ -> ../../archive/kostikov.co/cert4.pem
lrwxr-xr-x 1 root wheel 36 4 окт. 20:45 chain.cur.pem@ -> ../../archive/kostikov.co/chain3.pem
lrwxr-xr-x 1 root wheel 36 4 окт. 19:41 chain.pem@ -> ../../archive/kostikov.co/chain4.pem
lrwxr-xr-x 1 root wheel 40 4 окт. 20:45 fullchain.cur.pem@ -> ../../archive/kostikov.co/fullchain3.pem
lrwxr-xr-x 1 root wheel 40 4 окт. 19:41 fullchain.pem@ -> ../../archive/kostikov.co/fullchain4.pem
lrwxr-xr-x 1 root wheel 42 4 окт. 20:45 privandcert.cur.pem@ -> ../../archive/kostikov.co/privandcert3.pem
lrwxr-xr-x 1 root wheel 42 4 окт. 20:45 privandcert.pem@ -> ../../archive/kostikov.co/privandcert4.pem
lrwxr-xr-x 1 root wheel 38 4 окт. 20:45 privkey.cur.pem@ -> ../../archive/kostikov.co/privkey3.pem
lrwxr-xr-x 1 root wheel 38 4 окт. 19:41 privkey.pem@ -> ../../archive/kostikov.co/privkey4.pem
root@beta:/usr/local/etc/letsencrypt # cat ../lighttpd/pins.d/kostikov.co.conf
setenv.add-response-header = (
"Strict-Transport-Security" => "max-age=15768000",
"Public-Key-Pins" => "pin-sha256=\"/Zzd6uB5QqnA/H7iu/X4cFjO5eY2zZ/4cJt32L4MQQQ=\"; pin-sha256=\"WE6nwK3QjTqBzMJibxXUD2MP0OtmQEU+EbyQDlU+7w8=\"; max-age=5184000"
)
Проверить корректность функционирования HPKP можно на сайте Report URI или через комплексный тест безопасности SSL / TLS на сайте Qualys SSL labs. Также, к примеру, в браузере Google Chrome можно проверить используется ли HPKP через запрос по имени домена на специальной сервисной странице.
Готово!
Несмотря на очевидные плюсы внедрения HTTP Public Key Pining, данная технология открывает и возможности "отстрелить себе ногу". Например, ошибка в конфигурировании заголовка HPKP или утрата сертификтов соответствующим указанных в нём цифровым отпечатками, может привести к тому, что ваш сайт может оказаться недоступным по защищённому каналу на длительное время.
Для отладки системы на начальном этапе внедрения хорошей практикой может быть указание малого значения параметра max-age пока не будет достаточной степени уверенности в том, что всё функционирует так, как это задумано. Также можно воспользоваться взамен основного заголовка специальный тестовый Public-Key-Pins-Report-Only, который при наличии внешнего обработчика отчётов из поля report-uri поможет отладить механизм работы HPKP.
Но самым большим кошмаром может оказаться взлом вашего сервера и указание в качестве резервных отпечатков заведомо несуществующих сертификатов при одновременном задании огромного значения max-age с последующим удалением ныне используемого сертификата. Это приведёт к длительной недоступности сайта для клиентов, которые его посещали между периодом замены резервных отпечатков на несуществующие и удаления действительного сертификата.
В этой связи я рекомендую дополнительно сгенерировать третий набор серификатов от другого поставщика с длительным сроком действия, прописать отпечаток в качестве резервного, а сами сертификаты перенести в защищённое хранилище, которое будет никак не связано с вашим сервером. Это позволит быстро и относительно безболезненно восстановить защищённый доступ к вашему сайту после взлома.
Статья была полезной? Тогда прошу не стесняться и поддерживать деньгами через PayPal или Яндекс.Деньги.