воскресенье, 26 марта 2017 г.

Таймаут DNS в OpenNTPd

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

Стал разбираться, что же не так. Как оказалось, загрузка остановилась на этапе запуска OpenNTPd. Заметил, что сервер не был подключен к сети. Воткнул сеть - сервер стал загружаться дальше. В интернете нашёл описание этой ошибки в OpenNTPd: net-misc/openntpd - failed dns results in extended startup delay when -s option in use

Проблема заключается в том, что у OpenNTPd предусмотрен таймаут для NTP-серверов, но не предусмотрен таймаут для DNS-серверов, поэтому демон продолжает в бесконечном цикле выполнять DNS-запросы, пытаясь узнать имена NTP-серверов.

Проблему можно решить одним из способов:
  • Убрать из настроек опцию -s,
  • Прописать в конфигурацию IP-адреса NTP-серверов, а не доменные имена,
  • Пропатчить пакет патчем из этого отчёта об ошибке.
Я решил тогда пропатчить пакет. Потом я много раз пользовался этим пакетом, но описать его сборку всё забывал. Вот сейчас решил всё-таки задокументировать эту процедуру, хотя в новых версиях Debian она станет больше не нужной, т.к. в новых версиях OpenNTPd ошибка уже исправлена.

Для начала отредактируем файл /etc/apt/sources.list и добавим строчки с репозиториями deb-src с исходными текстами:
deb http://mirror.ufanet.ru/debian/ wheezy main contrib non-free
deb http://mirror.ufanet.ru/debian/ wheezy-updates main contrib non-free
deb http://mirror.ufanet.ru/debian/ wheezy-proposed-updates main contrib non-free
deb http://mirror.ufanet.ru/debian-security wheezy/updates main contrib non-free

deb-src http://mirror.ufanet.ru/debian/ wheezy main contrib non-free
deb-src http://mirror.ufanet.ru/debian/ wheezy-updates main contrib non-free
deb-src http://mirror.ufanet.ru/debian/ wheezy-proposed-updates main contrib non-free
deb-src http://mirror.ufanet.ru/debian-security wheezy/updates main contrib non-free
Установим инструменты, необходимые для сборки пакета:
# apt-get install dpkg-dev devscripts fakeroot
Установим пакет с исходными текстами openntpd:
# apt-get source openntpd
Скачаем подготовленный мной патч:
# wget http://stupin.su/files/openntpd-20080406p-dns-timeout.patch
Перейдём в каталог с исходными текстами пакета:
# cd openntpd-20080406p
Наложим скачанный патч:
# patch -Np0 < ../openntpd-20080406p-dns-timeout.patch
Установим пакеты, необходимые для сборки пакета openntpd:
# apt-get build-dep openntpd
Оформим изменения, сделанные в исходных текстах, в виде патча:
# dpkg-source --commit
В ответ на запрос имени заплатки введём dns-timeout.

Содержимое заголовка заплатки:
Description: DNS timeout added
 Added patch from https://bugs.gentoo.org/show_bug.cgi?id=493358
 .
 openntpd (20080406p-4) unstable; urgency=low
Author: Vladimir Stupin <vladimir@stupin.su>
Last-Update: <2017-02-09>

--- openntpd-20080406p.orig/ntpd.c
+++ openntpd-20080406p/ntpd.c
Теперь опишем сделанные изменения в журнале изменений:
# dch -i
Свежая запись в журнале будет выглядеть следующим образом:
openntpd (20080406p-4.1) UNRELEASED; urgency=low

  * DNS timeout added

 -- Vladimir Stupin <vladimir@stupin.su>  Thu, 09 Mar 2017 22:11:37 +0500
Соберём новый пакет с исходными текстами и двоичный пакет:
# dpkg-buildpackage -us -uc -rfakeroot
Теперь можно перейти на уровень выше и установить собранный пакет:
# cd ..
# dpkg -i openntpd_20080406p-4.1_amd64.deb
С новым пакетом пауза в процессе загрузки составляет 1 минуту 40 секунд, что вполне приемлемо, т.к. сервер в конце концов всё-таки загружается, а на фоне всего процесса загрузки эта дополнительная пауза почти не заметна.

воскресенье, 19 марта 2017 г.

Смена темы письма в Postfix

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

Часто письма с разных серверов приходят от одного и того же отправителя, с одной и той же темой. Если ни в теме письма, ни в его теле явным образом не упоминается сервер, с которого было отправлено письмо, то обычно приходится смотреть заголовки письма, чтобы определить, с какого из серверов оно на самом деле было отправлено.

Чтобы решить эту проблему и облегчить себе работу, я решил настроить все свои серверы так, чтобы Postfix добавлял в тему отправляемого письма имя сервера.

Сделать это просто. Для начала нужно установить пакет postfix-pcre:
# apt-get install postfix-pcre
Далее, добавим в файл /etc/postfix/main.cf одну строчку:
header_checks = pcre:/etc/postfix/rewrite_subject
Теперь нужно создать файл /etc/postfix/rewrite_subject и поместить в него правило, которое будет добавлять в тему письма дополнительный текст с именем сервера. Например, вот так:
/^Subject: (.*)$/ REPLACE Subject: $1 (from server.domain.tld)
Текст "/^Subject: (.*)$/" является регулярным выражением в стиле Perl. Это регулярное выражение будет совпадать со строкой заголовка, начинающейся с текста "Subject: ". Текст "(.*)" совпадает с любой темой письма и будет запомнен в переменной $1.

Действие "REPLACE" говорит о том, что нужно произвести замену текста.

Строка "Subject: $1 (from server.domain.tld)" содержит в текст, который заменит совпавшую строчку. При этом вместо переменной $1 будет подставлено её значение. Если имя переменной не отделено от остального текста пробельными символами, тогда её имя следует записывать в виде ${1}

Итак, осталось перезапустить почтовый сервер:
# /etc/init.d/postfix restart
Проверить, добавляется ли текст к теме письма можно следующим образом:
# mail -s test box@mailserver.tld
test
.
При этом в почтовом ящике должно появиться письмо с темой "test (from server.domain.tld)".

воскресенье, 12 марта 2017 г.

Использование urllib2 в Python

Как известно, Python поставляется с "батарейками" - в комплекте идёт масса различных модулей. Одним из таких модулей является модуль urllib2, позволяющий выполнять веб-запросы. К сожалению, этот модуль не блещет наглядностью, потому что для выполнения запроса зачастую требуется соединить между собой несколько объектов. Это может быть привычным по идеологии для программистов, пишущих на Java, но вот у большинства программистов, использующих скриптовые языки, этот подход напрочь отбивает всё желание пользоваться этим модулем. В результате я наблюдаю, например, что коллеги для выполнения веб-запросов часто пользуются сторонними, более дружелюбными модулями. Трое из них пользовались pycurl, а один - requests. Кстати, девиз модуля requests звучит как "Python HTTP для людей", что как бы намекает на то, что по мнению авторов модуля requests модуль urllib2 сделан для инопланетян. Пожалуй, в этом есть доля истины.

Не знаю почему, но я старался пользоваться родным модулем из Python. Возможно потому, что его использование позволяет обойтись без дополнительных зависимостей. Возможно также, что я пытался пользоваться родным модулем, потому что ранее в скриптах на Perl пользовался родным для него модулем LWP и у меня с ним не возникало никаких проблем. Я ожидал, что с родным модулем из Python тоже не должно возникнуть никаких проблем. Мне было трудно, но, к счастью, разбираться с модулем мне пришлось поэтапно, так что со временем в моих скриптах и программах набралось достаточное количество примеров на самые разные случаи с обработкой всех встречавшихся мне исключений. Я решил собрать все эти примеры в одном месте и поделиться ими в этой статье.

1. GET-запрос

Начнём с самого простого - с выполнения GET-запроса. Нужно просто скачать страницу.
import warnings, urllib, urllib2, socket, ssl
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')

def http_get(str_url, dict_params, dict_headers, float_timeout):
    query = urllib.urlencode(dict_params)
    req = urllib2.Request(str_url + '?' + query, headers=dict_headers)
    
    opener = urllib2.build_opener()
    
    try:
        res = opener.open(req, timeout=float_timeout)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Здесь можно увидеть функцию http_get, которая принимает параметры:
  • str_url - строка с URL страницы, которую нужно скачать,
  • dict_params - словарь с параметрами и их значениями, из которых будет сформирована строка запроса. Эта строка запроса потом будет добавлена к строке URL после знака вопроса. Ключи словаря являются именами параметров, а значения - соответственно значениями,
  • dict_headers - словарь с дополнительными заголовками, которые будут добавлены в GET-запрос. Ключи словаря являются именами заголовков, а значения - их значениями,
  • float_timeout - число с плавающей запятой, задающее таймаут ожидания запроса.
Функция возвращает кортеж из двух значений: первое значение содержит код ошибки от веб-сервера, а второе значение содержит тело ответа. Если в строке URL содержится ошибка, не отвечает DNS-сервер, DNS-сервер сообщает об ошибке поиска имени или если соединиться с веб-сервером не удалось, будет возвращён кортеж с двумя значениями None. Если вам нужно различать ошибки разного рода, переделайте обработку ошибок под свои нужды.

Если вам нужно узнать заголовки из ответа, то имейте в виду, что объект res принадлежит классу httplib.HTTPResponse. У объектов этого класса есть методы getheader и getheaders. Метод getheader вернёт значение заголовка с указанным именем или значение по умолчанию. Метод getheaders вернёт список кортежей с именами заголовков и их значениями. Например, словарь из заголовков и их значений (исключая заголовки с несколькими значениями) можно было бы получить следующим образом:
headers = dict(res.getheaders())

2. POST-запрос enctype=application/x-www-form-urlencoded

Можно сказать, что это "обычный" POST-запрос. В таких POST-запросах применяется кодирование параметров, аналогичное тому, которое используется для кодирования параметров в GET-запросах. В случае с GET-запросами строка с параметрами добавляется после вопросительного знака к строке URL запрашиваемого ресурса. В случае с POST-запросом эта строка с параметрами помещается в тело запроса. В запросах такого рода нельзя передать на сервер файл.
import warnings, urllib, urllib2, socket, ssl
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')

def http_post(str_url, dict_params, dict_headers, float_timeout):
    query = urllib.urlencode(dict_params)
    dict_headers['Content-type'] = 'application/x-www-form-urlencoded'
    req = urllib2.Request(str_url, data=query, headers=dict_headers)
    
    opener = urllib2.build_opener()
    
    try:
        res = opener.open(req, timeout=float_timeout)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Функция http_post по входным и выходным параметрам полностью аналогична функции http_get, только выполняет POST-запрос.

3. POST-запрос enctype=multipart/form-data

Если в запросе нужно отправить много данных, то они могут оказаться слишком большими, чтобы уместиться в одной строке. В таких случаях, например если нужно приложить файл, в веб-приложениях используются формы с типом "multipart/form-data". Для отправки подобных запросов переделаем предыдущую функцию, воспользовавшись модулем poster.
import warnings, urllib2, socket, ssl
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')
from poster.encode import multipart_encode

def http_post_multipart(str_url, dict_params, dict_headers, float_timeout):
    body, headers = multipart_encode(dict_params)
    dict_headers.update(headers)
    req = urllib2.Request(str_url, data=body, headers=dict_headers)
    
    opener = urllib2.build_opener()
    
    try:
        res = opener.open(req, timeout=float_timeout)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Функция http_post_multipart по входным и выходным параметрам полностью аналогична функциям http_get и http_post, только выполняет POST-запрос, закодировав параметры способом, пригодным для передачи данных большого объёма, в том числе - файлов.

4. GET-запрос с аутентификацией

Продемонстрирую аутентификацию на примере с GET-запросом:
import warnings, urllib, urllib2, socket, ssl
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')

def http_get_auth(str_url, dict_params, dict_headers, float_timeout, str_user, str_password):
    query = urllib.urlencode(dict_params)
    req = urllib2.Request(str_url + '?' + query, headers=dict_headers)
    
    passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
    passman.add_password(None, str_url, str_user, str_password)
    authhandler = urllib2.HTTPBasicAuthHandler(passman)
    
    opener = urllib2.build_opener(authhandler)
    
    try:
        res = opener.open(req, timeout=float_timeout)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Как видно, функция http_get_auth по выходным параметрам совпадает со всеми предыдущими функциями, но среди входных параметров появилось два новых:
  • str_user - строка, содержащая имя пользователя,
  • str_password - строка, содержащая пароль пользователя.
Внутри функции появились три новые строчки, выделенные жирным шрифтом. Эти строчки создают менеджер паролей и дополнительный обработчик ответа от веб-сервера. В четвёртой выделенной строчке создаётся объект, который будет выполнять запрос с использованием этого дополнительного обработчика. Задача обработчика простая - если при запросе без аутентификации будет возвращён код ошибки 401, то в запрос будет добавлен дополнительный заголовок, содержащий имя пользователя и его пароль, после чего запрос будет повторён.

Менеджер паролей может предоставлять обработчику разные пароли, в зависимости от области доступа (часто называемой realm'ом), которую сообщит веб-сервер или в зависимости от URL запрашиваемого ресурса.

5. GET-запрос с безусловной аутентификацией

Обработчик HTTPBasicAuthHandler устроен таким образом, что сначала всегда делает запрос без передачи логина и пароля. И только если получен ответ с кодом 401, уже пытается выполнить запрос с аутентификацией.

При попытке выполнить запрос к API, реализованному на основе библиотеки swagger, возникают некоторые трудности с аутентификацией. При обращении к API без аутентификации, API возвращает HTML-справку по использованию API с кодом ответа 403, поэтому дополнительных попыток получить страницу с аутентификацией не предпринимается.

Чтобы исправить эту ситуацию, я воспользовался ответом, подсмотренным здесь: does urllib2 support preemptive authentication authentication?
import warnings, urllib, urllib2, socket, ssl, base64
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')

class PreemptiveBasicAuthHandler(urllib2.HTTPBasicAuthHandler):
    '''Preemptive basic auth.

    Instead of waiting for a 403 to then retry with the credentials,
    send the credentials if the url is handled by the password manager.
    Note: please use realm=None when calling add_password.'''
    def http_request(self, req):
        url = req.get_full_url()
        realm = None
        # this is very similar to the code from retry_http_basic_auth()
        # but returns a request object.
        user, pw = self.passwd.find_user_password(realm, url)
        if pw:
            raw = "%s:%s" % (user, pw)
            auth = 'Basic %s' % base64.b64encode(raw).strip()
            req.add_unredirected_header(self.auth_header, auth)
        return req

    https_request = http_request

def http_get_preemptive_auth(str_url, dict_params, dict_headers, float_timeout, str_user, str_password):
    query = urllib.urlencode(dict_params)
    req = urllib2.Request(str_url + '?' + query, headers=dict_headers)
    
    passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
    passman.add_password(None, str_url, str_user, str_password)
    authhandler = PreemptiveBasicAuthHandler(passman)
    
    opener = urllib2.build_opener(authhandler)
    
    try:
        res = opener.open(req, timeout=float_timeout)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Функция http_get_preemptive_auth по входным и выходным параметрам полностью аналогична функции http_get_auth, но в ней используется другой дополнительный обработчик - PreemptiveBasicAuthHandler, который отличается от обработчика HTTPBasicAuthHandler тем, что не выполняет запрос без аутентификации, ожидая получить ошибку 401, а сразу отправляет логин и пароль, соответствующие запрашиваемому URL.

6. GET-запрос через прокси

Смотрим пример:
import warnings, urllib, urllib2, socket, ssl
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')

def http_get_proxy(str_url, dict_params, dict_headers, float_timeout, str_proxy):
    query = urllib.urlencode(dict_params)
    req = urllib2.Request(url + '?' + query, headers=dict_headers)
    
    proxyhandler = urllib2.ProxyHandler(str_proxy)

    opener = urllib2.build_opener(proxyhandler)
    
    try:
        res = opener.open(req, timeout=float_timeout)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Функция http_get_proxy по выходным параметрам полностью соответствует функции http_get, а среди входных параметров имеется один дополнительный:
  • str_proxy - строка с адресом веб-прокси. Имеет вид http://proxy.domain.tld:3128
В этом примере используется дополнительный обработчик запросов ProxyHandler, который позволяет отправлять запросы через веб-прокси. На самом деле, этому обработчику можно передать не строку с одним веб-прокси, а передать словарь, в котором для разных протоколов будут указаны разные прокси:
{
 'http': 'http://proxy-http.domain.tld:3128',
 'https': 'http://proxy-https.domain.tld:3128',
 'ftp': 'http://proxy-ftp.domain.tld:3128'
}

7. Использование нескольких обработчиков запросов одновременно

Когда в запросе нужно использовать несколько обработчиков одновременно, то можно растеряться, т.к. в примерах выше каждый раз используется только один обработчик для аутентификации или для прокси. Но использовать несколько обработчиков совсем не трудно. Это можно сделать, указав их через запятую, вот так:
opener = urllib2.build_opener(authhandler, proxyhandler)
Но и в этом случае можно растеряться, если список обработчиков нужно формировать динамически, так что каждый конкретный обработчик может отсутствовать. Тут тоже всё просто. Можно добавлять обработчики в массив, а затем использовать этот массив как список аргументов:
# В начале список обработчиков пуст
handlers = []

# Если нужна аутентификация, добавляем обработчик в список
if ...:
    ...
    authhandler = ...
    handlers.append(authhandler)

# Если запрос нужно выполнить через прокси, добавляем обработчик в список
if ...:
    ...
    proxyhandler = ...
    handlers.append(proxyhandler)

# Выполняем запрос, используя все обработчики из списка
opener = urllib2.build_opener(*handlers)
Если нужно пройти аутентификацию и на прокси и на веб-ресурсе, то для аутентификации на прокси можно использовать обработчик ProxyBasicAuthHandler, так что всего будет использоваться аж сразу три обработчика запросов.

Этих примеров достаточно для того, чтобы научиться пользоваться модулем urllib2 и понимать, в каком направлении надо копать, чтобы сделать что-то такое, что здесь не описано. Если у вас есть чем дополнить эту статью, прошу написать мне об этом в комментариях.

Дополнение от 4 сентября 2017 года: Исправил место указания таймаута. Судя по тому, что за почти полгода никто не указал на ошибку, либо статья не очень востребована, либо людям лень указывать на ошибку и они просто переходят к другой статье на ту же тему :)

Дополнение от 21 декабря 2017 года: Добавил импорт пропущенных модулей, в которых определяются объекты исключений, перехватываемых в примерах.

воскресенье, 5 марта 2017 г.

HKPK - расширение для фиксации публичного ключа HTTP в Apache, nginx и Lighttpd

Перевод: HTTP Public Key Pinning Extension HPKP for Apache, NGINX and Lighttpd
Автор: Реми ван Элст (Remy van Elst)

Содержание
  1. HPKP - расширение для фиксации публичного ключа HTTP
  2. Отпечаток SPKI - теория
  3. Что фиксировать
  4. Отпечаток SPKI
  5. Неполадки
  6. Настройка веб-сервера
    1. Apache
    2. Lighttpd
    3. nginx
  7. Отчёты
  8. Не фиксировать, только отчитываться
Фиксация публичного ключа означает, что цепочка сертификатов должна включать публичный ключ из разрешённого списка. Это даёт гарантии того, что только удостоверяющий центр из разрешённого списка может подписывать сертификаты для *.example.com, но не какой-то другой удостоверяющий центр, сертификат которого имеется в хранилище браузера. В этой статье объясняется теория и приводятся примеры настройки Apache, Lighttpd и nginx.

HPKP - Расширение для фиксации публичного ключа HTTP

Допустим, что имеется банк, который всегда использует сертификаты, выпущенные удостоверяющим центром компании A. При нынешней системе сертификации удостоверяющие центры компаний B, C и удостоверяющий центр АНБ могут создать сертификат этого банка, который будет приниматься, потому что эти компании также являются доверенными корневыми удостоверяющими центрами.

Если банк воспользуется расширением для фиксации публичного ключа - HPKP и зафиксирует первый промежуточный сертификат (от удостоверяющего центра компании A), браузеры не будут принимать сертификаты от удостоверяющих центров компаний B и C, даже если цепочка доверия верна. HPKP также позволяет браузеру отправлять в банк отчёты о попытках подмены, чтобы банк знал о проводящейся атаке.

Расширение для фиксации публичного ключа HTTP - Public Key Pinning Extension for HTTP (HPKP) - это стандарт пользовательских агентов HTTP, которые были разработаны начиная с 2011 года. Начало стандарту было положено в Google, когда фиксация была реализована в Chrome. Однако, впоследствии разработчики поняли, что ручное поддержание списка фиксированных сайтов не масштабируемо.

Вот краткий обзор возможностей HPKP:
  • HPKP задаётся на уровне HTTP с помощью заголовка Public-Key-Pins в ответах.
  • Политика срока хранения задаётся при помощи параметра max-age, который указывает длительность в секундах.
  • Заголовок Public-Key-Pins может использоваться только в успешном безопасном зашифрованном подключении.
  • При наличии нескольких заголовков обрабатывается только первый.
  • Фиксация может распространяться на поддомены при использовании параметра includeSubDomains.
  • При получении нового заголовка Public-Key-Pins, он заменяет ранее сохранённую фиксацию и метаданные.
  • Фиксация состоит из алгоритма хэширования и отпечатка Subject Public Key Info - информации публичного ключа субъекта.
В этой статье сначала рассматриваются принципы работы HPKP, а далее можно найти часть, где рассказывается, как получить необходимые отпечатки и настроить веб-сервер.

Отпечаток SPKI - теория

Как объясняет в своей статье Адам Лэнгли (Adam Langley), хэшируется публичный ключ, а не сертификат:
В общем, хэширование сертификатов - это очевидное решение, но не правильное. Проблема в том, что сертификаты удостоверяющего центра часто перевыпускаются: может быть несколько сертификатов с одним и тем же публичным ключом, именем в Subject и т.д., но с разными расширениями или сроками годности. Браузеры строят цепочки сертификатов из пула сертификатов снизу вверх, и альтернативная версия сертификата может заместить ожидаемую.

Например, у StartSSL имеется два корневых сертификата: один подписан SHA1, а другой - SHA256. Если нужно зафиксировать StartSSL как удостоверяющий центр, то какой из сертификатов следует использовать? Нужно использовать оба, но как узнать что есть второй корень, если об этом не сообщается?

В то же время, для этой цели годятся хэши публичных ключей:

Браузеры считают, что сертификат веб-сервера неизменен: он всегда является началом цепочки. Сертификат сервера содержит подпись родительского сертификата, которая должна быть верной. Подразумевается, что публичный ключ родителя неизменен для дочернего сертификата. И так далее, вся цепочка публичных ключей оказывается неизменной.

Единственный тонкий момент заключается в том, что не стоит фиксировать кросс-сертифицированные корневые сертификаты. Например, корневой сертификат GoDaddy подписан сертификатом Valicert, так что старые клиенты, не знающие корневой сертификат GoDaddy всё-таки доверяют таким сертификатам. Однако, не стоит фиксировать сертификат Valicert, потому что более новые клиенты завершают цепочку сертификатом GoDaddy.

Также хэшируется SubjectPublicKeyInfo, но не двоичная последовательность публичного ключа. SPKI, наряду с самим публичным ключом, включает в себя тип публичного ключа и некоторые параметры. Это важно, потому что простое хэширование публичного ключа оставляет возможность для атаки из-за неправильной интерпретации. Рассмотрим публичный ключ Диффи-Хеллмана: если хэшируется только публичный ключ, а не SPKI полностью, то атакующий может воспользоваться тем же публичным ключом, но заставить клиента интерпретировать его как другую группу Диффи-Хеллмана. Таким же образом можно заставить браузер интерпретировать ключ RSA как ключ DSA и т.п.

Что фиксировать

Что нужно фиксировать? Не стоит фиксировать собственный публичный ключ. Ключ может смениться или оказаться скомпрометированным. Может понадобиться использовать несколько сертификатов, а ключ может смениться из-за слишком частой ротации сертификатов. Ключ может оказаться скомпрометированным, если веб-сервер взломают.

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

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

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

Альтернативный и более безопасный способ решения этой проблемы - создать заблаговременно по меньшей мере три разных публичных ключа (с помощью OpenSSL, обратитесь к странице с Javascript, которая генерирует команду OpenSSL для генерации) и хранить два из этих ключей как резерв в безопасном месте, вне сети и в другом помещении.

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

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

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

Отпечаток SPKI

Для получения отпечатка SPKI из сертификата можно воспользоваться командой OpenSSL, которая указана в черновике RFC:
openssl x509 -noout -in certificate.pem -pubkey | \
openssl asn1parse -noout -inform pem -out public.key;
openssl dgst -sha256 -binary public.key | openssl enc -base64
Результат:
klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=
Файл certificate.pem, подаваемый на вход команды - это первый сертификат в цепочке для этого веб-сайта. На момент написания - сервер удостоверяющего центра для безопасной проверки доменов COMODO RSA (COMODO RSA Domain Validation Secure Server CA), серийный номер - 2B:2E:6E:EA:D9:75:36:6C:14:8A:6E:DB:A3:7C:8C:07.

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

Неполадки

На момент написания этой статьи (январь 2015 года) HPKP поддерживает только один браузер (Chrome), и у него имеется важная проблема, которая заключается в том, что Chrome понимает директивы max-age и includeSubdomains из заголовков HSTS и HPKP как взаимно исключающие. Это означает, что если имеется HSTS и HPKP с разными политиками max-age или includeSubdomains, они будут перетасовываться. Обратитесь к описанию этой неполадки за более подробной информацией: HPKP принудительно использует includeSubDomains даже когда директива не указана. Благодарю Скотта Хелми (Scott Helme) из https://scotthelme.co.uk за исследования и за то, что сообщил о проблеме мне и в проект Chromium.

Настройка веб-сервера

Далее приведены инструкции по настройке трёх наиболее популярных веб-серверов. Поскольку HPKP - это просто заголовок HTTP, большинство веб-серверов позволяют задать его. Заголовок нужно настраивать на веб-сайте HTTPS.

Ниже указан пример фиксации сервера удостоверяющего центра для безопасной проверки доменов COMODO RSA (COMODO RSA Domain Validation Secure Server CA) и запасного удостоверяющего центра 2 Comodo PositiveSSL (Comodo PositiveSSL CA 2), со сроком годности в 30 дней, включая все поддомены.

Apache

Отредактируйте файл конфигурации Apache (например, /etc/apache2/sites-enabled/website.conf или /etc/apache2/httpd.conf) и добавьте следующие строки в секцию VirtualHost:
# Загрузка не обязательного модуля headers:
LoadModule headers_module modules/mod_headers.so

Header set Public-Key-Pins "pin-sha256=\"klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=\"; pin-sha256=\"633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q=\"; max-age=2592000; includeSubDomains"
Lighttpd

В случае Lighttpd всё очень просто. Добавим в файл конфигурации Lighttpd (например, в /etc/lighttpd/lighttpd.conf) следующие строки:
server.modules += ( "mod_setenv" )
$HTTP["scheme"] == "https" {
  setenv.add-response-header = ( "Public-Key-Pins" => "pin-sha256=\"klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=\"; pin-sha256=\"633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q=\"; max-age=2592000; includeSubDomains")
}
nginx

Настройка nginx даже ещё короче. Добавьте следующую строку в блок server, относящийся к настройке HTTPS:
add_header Public-Key-Pins 'pin-sha256="klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY="; pin-sha256="633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q="; max-age=2592000; includeSubDomains';
Отчёты

Отчёты HPKP позволяют пользовательским агентам отправлять вам отчёты о проблемах.

Если добавить к заголовку дополнительный параметр report-uri="http://example.org/hpkp-report" и настроить по этой ссылке прослушиватель, клиенты будут отправлять отчёты при возникновении проблем. Отчёт отправляет JSON-данные на адрес из report-uri в теле POST-запроса:
{
    "date-time": "2014-12-26T11:52:10Z",
    "hostname": "www.example.org",
    "port": 443,
    "effective-expiration-date": "2014-12-31T12:59:59",
    "include-subdomains": true,
    "served-certificate-chain": [
        "-----BEGINCERTIFICATE-----\nMIIAuyg[...]tqU0CkVDNx\n-----ENDCERTIFICATE-----"
    ],
    "validated-certificate-chain": [
        "-----BEGINCERTIFICATE-----\nEBDCCygAwIBA[...]PX4WecNx\n-----ENDCERTIFICATE-----"
    ],
    "known-pins": [
        "pin-sha256=\"dUezRu9zOECb901Md727xWltNsj0e6qzGk\"",
        "pin-sha256=\"E9CqVKB9+xZ9INDbd+2eRQozqbQ2yXLYc\""
    ]
}
Не фиксировать, только отчитываться

С помощью заголовка Public-Key-Pins-Report-Only можно настроить HPKP в режиме отправки отчётов без принуждения соблюдать фиксацию.

Этот подход позволяет настроить фиксацию так, что сайт не станет недостижимым, если HPKP настроен неправильно. Позже можно включить принудительное использование фиксации заменой заголовка обратно на Public-Key-Pins.