4 Октябрь 2016

Технология HTTP Public Key Pining: внедрение и автоматизация совместно с Let's Encrypt

HTTP Public Key Pining: implementation and automation with Let's Encrypt

На страницах этого блога уже неоднократно затрагивались вопросы, связанные с защитой передаваемых по сети данных с использованием технологии SSL / TLS. Вопрос надёжности защищённых соединений в последнее время стал особенно актуальным для России, где на государственном уровне предполагается внедрение механизма для тотальной дешифровки всего трафика в Интернет одним из наиболее вероятных способов реализации которого, как раз, и может стать внедрение промежуточного сертификата для защищённых соединений.

В развитие темы, предлагаю вашему вниманию статью о внедрении относительно и новой пока малораспространённой технологии HTTP Public Key Pining (HPKP) и её использованию совместно с удостоверяющим центром Let's Encrypt.

Стандарт HPKP или key pining изложен в документе RFC7469 и описывает механизм, который призван повысить уровень защиты в WWW через дополнительную идентификацию и проверку того факта, что используемый в ходе защищённого соединения сертификат действительно принадлежит посещаемому сайту и не был подменён тем или иным заинтересованным лицом или структурой для несанкционированного доступа к передаваемой информации, то есть нейтрализации атак Man in the Middle (MitM).

1. Теория

Не вдаваясь глубоко в основы функционирования технологии SSL / TLS которые, впрочем, без проблем можно изучить начав, к примеру, с вышеуказанных ссылок, остановимся лишь на базовых моментах, понимание которых важно для выбора способа реализации и поддержания функционирования key pining для конкретного сайта.

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

Вопрос доверия это главный вопрос во всей системе. Для этого все сертификаты, которые используются конечными пользователями, как правило, подписываются одним или несколькими связанными сертификатами, которые принадлежат организациям с заведомо высоким уровнем доверия. Например, в случае с сертификатом, который используется на этом сайте, в качестве удостоверяющих его используется набор из двух других — корневой, принадлежащий одному из наиболее уважаемых удостоверяющих центров IdenTrust (DST Root CA X3) и промежуточный, принадлежащий удостоверяющему центру Let's Encrypt (Let's Encrypt Authority X1) выдавшему сайту конечный сертификат.

kostikov.co certificates chain

Все эти сертификаты, которые называются публичными и являются доступными всем, подписаны, в свою очередь, закрытыми ключами, которые гарантируют защиту их от подделки.

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

Однако, как уже было упомянуто, такого способа выстраивания доверительных отношений может быть недостаточно ввиду возможной (и реально случавшейся в истории — см., например, взлом промежуточного сертификата StartCom в 2011 или уязвимость механизма выпуска сертификатов WoSign в 2016) компрометации системы выдачи сертификатов или даже выпуска поддельных сертификатов конечных пользователей и путём их подмены дешифрации трафика. Именно возможную проблему с подменой и призван решить HPKP.

Для этого на базе одного из сертификатов в используемой в защищённом соединении цепочки генерируется специальный цифровой отпечаток, который однозначно идентифицирует связку сайт - сертификат. При первом соединении этот отпечаток предоставляется сайтом - владельцем сертификата в специальном поле заголовка HTTP, запоминается клиентом и в последующем каждый раз в течении определённого времени проверяется на возможную подмену злоумышленником. Key pining для протокола HTTP напоминает аналогичный давно используемый механизм начальной идентификации при работе по SSH.

При этом для обеспечения дополнительной надёжности доступа к сайту, защищаемому при помощи key pining, в передаваемом наборе данных должно фигурировать, как минимум, два отпечатка — один из них должен соответствовать одному из используемых в цепочке сертификтов, а второй являться резервным на случай проблем с текущим набором удостоверяющих данных.

2. Практика

Отпечаток сертификата может быть создан на базе любого используемого в применяемой цепочке сертификата — корневого, промежуточного и конечного публичных, или закрытых, включая запрос на получение сертификата (CSR), ключей. Каждый из этих способов имеет как свои плюсы, так и минусы.

Например, выбор в качестве источника цифрового отпечатка корневого сертификата решает проблему с обновлением низлежащих сертификатов — промежуточный и конечный сертификаты, которые им подписаны, могут заменяться без потери доступа к сайту, защищаемого при помощи HPKP. Однако, это предполагает полное и безоговорочное доверие к выдавшей такой сертификат организации. Кроме того, как и любые, корневые сертификаты время от времени заменяются по истечению срока действия или, что тоже изредка случается, их компрометации.

Примерно те же преимущества и недостатки имеет и выбор для формирования отпечатка промежуточного сертификата. Такие сертификаты ротируются чаще, чем корневые, что повышает риск потери доступа к сайту в случае наступления такого события. По этой причине Let's Encrypt, к примеру, специально предостерегает от практики использования своих промежуточных сертификатов для key pining.

Использование конечных сертификатов — публичного, закрытого или CSR, снимает вопрос доверия и внезапной ротации сертификатов более высокой иерархии. При этом возникает несколько сопутствующих вопросов.

  • Использование в качестве источника отпечатка CSR выглядит наиболее предпочтительным, поскольку на практике такие сертификаты стабильны и, как правило, не обновляются на протяжении длительных интервалов времени. Но, во-первых, ряд удостоверяющих центров, не используют этот механизм для выпуска сертификатов по умолчанию. Например, для Let's Encrypt эта возможность появилась не так давно и требует специальных действий по её применению. Также, и во-вторых, компрометация запроса на получение сертификата открывает двери для генерации на его основе поддельного сертификата.
  • Формирование цифрового отпечатка на базе публичного или закрытого ключей выглядит наиболее безопасным вариантом, но, при этом, ставит вопрос сохранения доступности сайта при их ротации. Последний особенно актуален при использвании механизма автоматического обновления сертификатов при работе с тем же Let's Encrypt.

3. Методика

Принимая во внимание вышеизложенное, я решил остановиться на варианте использования механизма HPKP на базе публичного сертификата от Let's Encrypt.

Главным вопросом, который следует решить, является обеспечение доступности сайта при обновлении сертификатов. Для этого как нельзя кстати оказывается заложенное в стандарте требование по указанию как минимум одного резервного отпечатка, который должен принадлежать сертификату не участвующему в текущей сессии.

Хорошей идеей представляется использование двух действующих сертификатов — более старого, который будет использоваться в текущих соединениях, и более нового, который будет прописан в качестве резервного и будет использован взамен основного при очередной ротации. При этом обновлении заголовок HPKP также будет пересоздан и место старого сертификата займёт бывший до этого резервный, а резервным станет самый последний выпущенный.

Для иллюстрации см. отчёт о текущих отпечатках для этого сайта на момент публикации статьи.

kostikov.co HPKP keys

Здесь первый (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 заведомо перекрывает этот срок и гарантирует неразрывность цепочки безопасности при замене сертификатов на новые.

4. Средство

Рассмотрим сам процесс внедрения выбранного варианта с сохранением преимуществ автоматизации процесса обновления сертификатов, которые предоставляет 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 через запрос по имени домена на специальной сервисной странице.

kostikov.co chrome query

Готово!

5. Опасности

Несмотря на очевидные плюсы внедрения HTTP Public Key Pining, данная технология открывает и возможности "отстрелить себе ногу". Например, ошибка в конфигурировании заголовка HPKP или утрата сертификтов соответствующим указанных в нём цифровым отпечатками, может привести к тому, что ваш сайт может оказаться недоступным по защищённому каналу на длительное время.

Для отладки системы на начальном этапе внедрения хорошей практикой может быть указание малого значения параметра max-age пока не будет достаточной степени уверенности в том, что всё функционирует так, как это задумано. Также можно воспользоваться взамен основного заголовка специальный тестовый Public-Key-Pins-Report-Only, который при наличии внешнего обработчика отчётов из поля report-uri поможет отладить механизм работы HPKP.

Но самым большим кошмаром может оказаться взлом вашего сервера и указание в качестве резервных отпечатков заведомо несуществующих сертификатов при одновременном задании огромного значения max-age с последующим удалением ныне используемого сертификата. Это приведёт к длительной недоступности сайта для клиентов, которые его посещали между периодом замены резервных отпечатков на несуществующие и удаления действительного сертификата.

В этой связи я рекомендую дополнительно сгенерировать третий набор серификатов от другого поставщика с длительным сроком действия, прописать отпечаток в качестве резервного, а сами сертификаты перенести в защищённое хранилище, которое будет никак не связано с вашим сервером. Это позволит быстро и относительно безболезненно восстановить защищённый доступ к вашему сайту после взлома.

6. PROFIT!

Статья была полезной? Тогда прошу не стесняться и поддерживать деньгами через PayPal или Яндекс.Деньги.


dev  HTTP  letsencrypt  lighttpd  security  shell  SSL