Выпуск 23. Январь 2015

Как нанять Perl-программиста | Содержание | Обзор CPAN за декабрь 2014 г.

Использование TLS в Perl

Ликбез по криптографии и практика применения TLS (Transport Layer Security — безопасность транспортного уровня) в Perl.

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

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

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

Прежде чем приступить к детальному рассмотрению протокола TLS, отметим самый главный недостаток TLS и криптографических алгоритмов — это невероятная сложность теории и математика 80-го уровня. Это отталкивает от изучения и приводит к заучиванию команд и строчек конфигураций протокола как магических мантр. В свою очередь, тотальное невежество приводит к выраженному консервативному развитию протоколов: не дай бог изменение сломает какую-нибудь древнюю систему на SSL 2.0, которую никто не умеет настраивать, давайте-ка оставим всё обратно-совместимым. Удивительно, но обратная совместимость — это именно то, что нужно злоумышленникам — вы можете использовать самые передовые технологии безопасности, но оставляете лазейку для старых и уязвимых протоколов и шифров. Косвенно на такое положение дел влияют государства, стандартизируя более слабые алгоритмы или параметры шифрования, чем существуют на сегодняшний день.

К счастью, браузерная гонка, развернувшаяся в последние годы, меняет положение к лучшему, предоставляя клиенту самые свежие и сильные протоколы шифрования, стимулируя и развитие серверных средств шифрования. Например, в Firefox при использовании протоколов SPDY/HTTP2 по умолчанию могут использоваться только самые строгие профили шифрования: TLS последней версии 1.2 и шифры с использованием эллиптических кривых. Поисковик Google стал учитывать при ранжировании сайтов наличие https, чтобы стимулировать веб-мастеров обзаводиться защищённым доступом к сайтам. Ну и новость последних дней — в 2015 г. браузер Chrome будет показывать обычные http-сайты как небезопасные так же, как показывает https-сайты с невалидным сертификатом.

Словарик криптографа

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

Алиса, Боб и другие персонажи

При построении схем обмена данными, криптографы придумали различные имена для различных участников обмена информации. Например, вместо указания, что участник А, передаёт данные стороне Б, им дали более благозвучные имена: Алиса и Боб. Поэтому если вы слышите упоминания об Алисе и Бобе, то на 100% речь идёт о процедуре передаче данных между двумя сторонами, без привязки к конкретным личностям.

Также могут упоминаться Ева (от слова eavesdropper — подслушивающий), участник, который может иметь возможность прослушивать трафик обмена между другими сторонами. Ну и Мэллори (от слова malicious — злонамеренный), который может не только прослушивать, но и изменять данные, передаваемые другими участниками; сама атака по подмене данных в этом случае называется MITM (man in the middle — человек посередине).

Шифрование

Шифрование — это процесс изменения открытого сообщения в шифрокод по определённому алгоритму. Дешифрование — это обратный процесс преобразования шифрокода в исходное сообщение.

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

Сами алгоритмы шифрования делятся на два вида: симметричные и асимметричные.

Симметричные алгоритмы

Симметричные используют один и тот же ключ для шифрования и дешифрования, этот ключ должен держаться сторонами обмена в секрете. Также различают потоковые и блочные шифры. Потоковые шифры могут кодировать данные побайтно, например алгоритм RC4. Блочные шифры кодируют данные блоками, например по 64, 128 бит. Один из самых первых стандартизированных и широко применяемых алгоритмов блочного шифрования стал алгоритм DES, который использовал ключи длиной 56 бит и кодировал блоки в 64 бита. В 2001 году был стандартизирован алгоритм AES, также известный под именем Rijndael, который и сейчас активно используется.

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

Асимметричные алгоритмы, или алгоритмы с открытым ключом

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

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

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

Один из первых и остроумных алгоритмов с открытым ключом, который до сих пор широко используется является алгоритм RSA.

Ключи RSA генерируются по следующему алгоритму:

  • Выбираются два больших простых числа p и q.
  • Вычисляется произведение n = p * q.
  • Вычисляется φ(n) = (p-1)(q-1).
  • Выбирают публичную экспоненту e, взаимно простую с φ(n) (например, 3, 257, 65537).
  • Находят секретную экспоненту d, такую что d * e ≡ 1 (mod φ(n)).

Теперь значения e и n публикуются как публичный ключ, а значения d, n становятся секретным ключом.

Чтобы зашифровать сообщение m, число m возводится в степень e и находится остаток от деления на число n:

m^e mod n = c

Чтобы расшифровать сообщение, шифротекст возводится в секретную экспоненту d и находится остаток от деления на n:

c^d mod n = (m^e mod n)^d mod n = m^(e*d) mod n = m

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

Вся сложность расшифровки посторонним лицом сводится к сложности вычисления чисел p и q по известному n, т.е. нахождения разложения на множители очень большого числа. Известны успешные операции по взлому RSA-ключей длиной в 768 бит, не за горами и взлом 1024-битного ключа, поэтому на сегодняшний день минимальный рекомендуемый размер ключа RSA — 2048 бит.

Но увеличение ключа приводит к дополнительной проблеме — требуется большие вычислительные ресурсы для шифрования. Например, на типичном современном процессоре (Intel Xeon X5460) можно выполнить порядка 500 операций шифрования в секунду ключом RSA 2048 бит, в то время как AES-128 может шифровать поток в 100 Мбайт/сек. Поэтому обычно RSA используют только для передачи секретного сеансового ключа симметричного шифрования, который затем уже используется для шифрования данных.

Аутентификация и целостность

Аутентификация, в широком смысле, — это процедура проверки подлинности. Может проверяться подлинность одной или обеих сторон обмена — в этом случае в данной статье и будет использоваться данный термин. Может проверяться подлинность сообщений, когда к оригинальному сообщению добавляется код MAC (message authentication code — код аутентификации сообщения), в этом случае будет применяться термин целостность сообщения, чтобы не запутаться.

Цифровая подпись

Цифровая подпись — это, в принципе, всё тот же MAC — код, который подтверждает авторство данного сообщения и его целостность.

Например, если Боб решит передать открытое сообщение Алисе и подтвердить, что это именно его сообщение. Он шифрует сообщение с помощью закрытого ключа и добавляет полученный шифротекст к оригинальному сообщению. Теперь Алиса (и вообще кто-угодно) может расшифровать код с помощью публичного ключа Боба и убедиться, что полученный текст совпадает с оригиналом.

Основные используемые алгоритмы цифровой подписи на сегодняшний день — это RSA, DSA и ECDSA.

Криптографическая хеш-функция

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

Ранее широко используемые алгоритмы хеш-функции MD5 и SHA-1 теперь не рекомендуются. Предпочтение отдаётся SHA-2, который включает вариации SHA-224, SHA-256, SHA-384 и SHA-512 (по длине ключа). Также недавно был выбран претендент на звание SHA-3, которым стала хеш-функция Keccak. Скоро ожидается завершение процедуры его стандартизации.

Сертификат, центры сертификации и инфраструктура публичных ключей (PKI)

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

Чтобы решить проблему с доверием к сертификатам, была создана целая инфраструктура по поддержке обмена публичными ключами, так называемые центры сертификации. Центр сертификации создаёт свою пару ключей (публичный и приватный), пользователи получают публичный ключ центра сертификации и считают его доверенным. Например, во многих дистрибутивах Linux есть пакет ca-certificates, который содержит сертификаты с публичными ключами всех доверенных центров сертификации. Каждый веб-браузер, как правило, также несёт с собой узелок с сертификатами, которым он доверяет.

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

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

Например, цепочка доверия для сайта google.ru:

Equifax Secure Certificate Authority
|
++ GeoTrust Global CA
 |
 ++ Google Internet Authority G2
  |
  + *.google.com.ru

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

Основными характеристиками криптостойкости сертификата являются выбранный алгоритм и длина публичного ключа, а также криптографическая функция, которая была выбрана для цифровой подписи сертификата. Можно выбрать хороший ключ RSA-2048, но если цифровая подпись сертификата выполнена с применением алгоритма SHA-1, то сертификат, по современным меркам, слабо защищён. Google и Microsoft заявили, что с 2016 года сайты с сертификатами SHA-1 будут считаться невалидными, а большинство центров сертификации начали бесплатный перевыпуск существующих сертификатов с использованием SHA-2.

Отзыв сертификата, CRL

Сертификаты могут отзываться в случае, если произойдёт, например, компрометация закрытого ключа. Для этих целей создаётся файл в формате CRL, в который добавляются серийные номера отозванных сертификатов. Файл подписывается цифровой подписью центра сертификации.

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

OCSP, OCSP stapling

Чтобы решить проблему с огромными CRL-списками, которые надо каким-то образом регулярно обновлять, был создан протокол OCSP (Online Certificate Status Protocol — протокол онлайн запросов статуса сертификата). Теперь Алисе достаточно выполнить запрос с серийным номером сертификата Боба к нужному центру сертификации, чтобы получить информацию о статусе сертификата. OCSP-ответы всегда подписаны цифровой подписью для защиты от подделки.

OCSP-запрос вносит дополнительную задержку в процедуре обмена, поэтому для оптимизации может применяться техника OCSP stapling, когда вместе со своим сертификатом Боб может отправить прикреплённый (stapled) ответ OCSP-сервера. Такой ответ всегда содержит время, когда был сделан запрос и время истечения валидности ответа и заверен цифровой подписью центра сертификации, поэтому ему можно доверять.

Протокол TLS

После того, как базовые понятия определены, можно приступить к рассмотрению работы протокола TLS. Протокол TLS традиционно относят к сессионному уровню в сетевой модели OSI, между транспортным уровнем (tcp) и уровнем приложения (http, smtp, imap, …). Действительно, до начала передачи данных уровня приложения стороны должны согласовать параметры протокола: используемая версия, шифр, сжатие и т.д. После выполнения согласования (TLS Handshake) данные приложения передаются в рамках определённых правил — TLS-сессии.

Существуют две процедуры согласования: полная и сокращённая.

Полная процедура согласования

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

Теперь клиент проверяет сертификат сервера (можно ли ему доверять, подписан ли он доверенным центром), если используется алгоритм RSA, то клиент извлекает из сертификата публичный ключ и с его помощью шифрует некоторый случайный секрет. Этот случайный секрет затем используется для создания ключей шифрования и контроля целостности для данной сессии. После чего клиент отправляет этот зашифрованный секрет серверу в сообщении ClientKeyExchаnge. Далее с помощью сообщения ChangeChiperSpec сигнализирует серверу, что он начал применять новые ключи шифрования и контроля целостности, и самое первое зашифрованное сообщение Finished содержит в себе хеш от всех предыдущих сообщений согласования в данной сессии.

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

Клиент расшифровывает и проверят хеш от сервера — если всё совпало, то начинают передаваться данные приложения, зашифрованные и подписанные сгенерированными при согласовании ключами.

       ..........                     ..........
       . Client .                     . Server .
       ..........                     ..........
                                         ____ 
          ____                          |====|
         |    |                         |    |
         |____|                         |    |
         /::::/                         |____|

       .--------------.
       . Client Hello .---------->.-----------------.
       '--------------'           . ServerHello     .
                                 /. Certificate     .
 .--------------------.         / . ServerHelloDone .
 .  ClientKeyExcahnge . <------'  '-----------------'
 . [ChangeChiperSpec] .\
 .           Finished . \         .--------------------.
 '--------------------'  '------> . [ChangeCipherSpec] .
                                 /. Finished           .
   .------------------.         / '--------------------'
   . Application Data .<-------'
   '------------------'\          .------------------.
                        '-------->. Application Data .
                                  '------------------'

Как видно, протокол обеспечивает три вида защиты:

  1. Аутентичность. Сервер отправляет сертификат в формате X.509, который клиент проверяет на доверие. Зашифровав некий секретный ключ, клиент может быть уверен, что расшифровать его сможет только легитимный сервер, имеющий закрытый ключ сертификата.

  2. Конфиденциальность. После выработки общего секрета по известному алгоритму формируются ключи для шифрования. Алгоритм шифрования выбирается из того, что согласовали клиент и сервер. Это может быть потоковый шифр RC4 или блочный 3DES или AES. Блочные шифры как правило используют режим CBC — блочное сцепление, когда следующий блок XOR’ится с предыдущим зашифрованным блоком, чтобы повысить стойкость шифрования. Данные приложения передаются зашифрованными, и нет никакой возможности третьей стороне их расшифровать.

  3. Целостность. Клиент и сервер согласуют функцию для хеширования данных. Это могут быть функции HMAC_SHA или HMAC_MD5, когда генерируется хеш от защищаемого текста и некой секретной соли. Таким образом гарантируется, что сообщение не было искажено при передаче. Кроме того, защищена от подмены и начальная стадия согласования (когда ещё не применяется шифрование) благодаря тому, что в сообщении Finished стороны обмениваются хешем всех предыдущих сообщений, чтобы убедиться, что каждая сторона получила одинаковые данные.

Здесь видна и основная проблема полной процедуры согласования: требуется два полных цикла отправки-приёма. Таким образом, протокол добавляет задержку при подключении, равную двум RTT.

Сокращённая процедура согласования

Чтобы решить проблему с задержкой, была создана сокращённая процедура согласования. При первом соединении клиента и сервера происходит обычная полная процедура, при этом сервер в сообщении ServerHello сообщает клиенту уникальный номер сессии и сохраняет контекст (главный секрет, версия протокола, шифр и т.д.). Клиент может запомнить номер сессии и связанный контекст и при следующем подключении в ClientHello сразу передать номер сессии. Если сервер сохранил данные об этой сессии, то он уведомляет об этом клиента в ServerHello и сразу же применяет этот контекст, отправляя ChangeCipherSpec и Finished клиенту. Клиент также применяет контекст шифрования и отправляет свои ChangeCipherSpec и Finished. Сразу же могут начать передаваться данные приложения.

       ..........                     ..........
       . Client .                     . Server .
       ..........                     ..........
                                         ____
          ____                          |====|
         |    |                         |    |
         |____|                         |    |
         /::::/                         |____|

       .--------------.
       . Client Hello .---------->.--------------------.
       '--------------'           . ServerHello        .
                                 /. [ChangeCipherSpec] .
 .--------------------.         / . Finished           .
 . [ChangeChiperSpec] . <------'  '--------------------'
 .           Finished .\
 .   Application Data . \         .------------------.
 '--------------------'  '------> . Application Data .
                                  '------------------'

Как видно, в сокращённой схеме согласования дополнительная задержка составляет всего один RTT.

С другой стороны, это накладывает требования к серверу для хранения контекста сессий для каждого подключаемого клиента. К счастью, на этот случай было разработано специальное расширение для протокола Session Ticket (RFC5077). Сервер отправляет клиенту не только номер сессии, но и билет, в котором только серверу известным способом зашифрованы параметры сессии. Если клиент хочет начать сокращённую процедуру согласования, то с ClientHello отправляется билет, который сервер расшифровывает и использует. Это аналог зашифрованных cookie для хранения данных сессии в HTTP-протоколе.

Совершенная прямая секретность (Perfect forward secrecy, PFS)

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

Алгоритм Диффи-Хеллмана

Один из самых первых и популярных алгоритмов, обеспечивающих обмен ключами по незащищённым каналам, стал алгоритм Диффи-Хеллмана. Вкратце суть алгоритма такова:

Алиса и Боб выбирают числа g и p (они публичны). Затем Алиса формирует секретное число a, а Боб число b. Затем они обмениваются числами A и B — результатами следующих вычислений:

A = g^a mod p
B = g^b mod p

Теперь если Алиса возведёт число Боба B в степень a и найдёт остаток от деления на p, то получит число K:

B^a mod p = (g^b mod p)^a mod p = g^(b*a) mod p = K

Нетрудно заметить, что если Боб возьмёт число Алисы A, возведёт его в степень b и вычислит остаток от деления на p, то получит тоже самое число K:

A^b mod p = (g^a mod p)^b mod p = g^(a*b) mod p = K

Это и будет общим секретным ключом. При этом Ева, даже имея возможность наблюдать за обменом Алисы и Боба, не сможет рассчитать число К, поскольку перед ней возникает задача вычисления дискретного логарифма, и в зависимости от выбранных чисел p, a, b это может представлять собой неразрешимую вычислительную задачу. На практике, в качестве g обычно берут число 2, поскольку это основание степени удобно для вычислений на компьютерах, ну а число p должно быть простым, и его размер является определяющим для стойкости алгоритма, как правило минимальное приемлемое значение имеет длину 1024 бит.

Алгоритм Диффи-Хеллмана на эллиптических кривых

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

y^2=x^3+аx+b

Мы определяем поле как конечный набор точек, удовлетворяющих уравнению кривой на эллиптической кривой по модулю p, где p — это простое число:

y^2=x^3+аx+b (mod p)

На эллиптической кривой определены операции сложения и умножения точек.

Таким образом, Алиса передаёт Бобу известные параметры: выбранное уравнение эллиптической кривой, число p и некоторую базовую координатную точку G на кривой. Алиса выбирает некоторое случайное целое число (не превышающее число элементов в поле) da и передаёт результат умножения:

А=da*G

Боб выполняет ту же операцию:

B=db*G

Теперь общий секрет вычисляется как

B*da = db*G*da = K
A*db = da*G*db = K

Существуют множество вариантов выбора эллиптических кривых; помимо описанного поля нечётной характеристики используются также поля характеристики 2. Но смысл алгоритма остаётся прежним, и криптографическая стойкость достигается за счёт вычислительной сложности дискретного логарифма на эллиптической кривой.

Основное достоинство эллиптических кривых — это значительно меньше требования к размеру ключа. Например, 2048-битный ключ RSA соответствует по стойкости ключу в 224 бита для эллиптических кривых. Кроме того, некоторые подобранные кривые требует меньше вычислительных мощностей по сравнению с классическими RSA/DSA. Например, nistp224 (ECDSA 224 бита) выдаёт порядка 10000 операций шифр/сек против 500 у RSA-2048 на современном процессоре.

Диффи-Хеллман в TLS

Различные модификация алгоритма Диффи-Хеллмана получили соответствующие обозначения DHE и ECDHE. Использовать их самостоятельно нельзя, поскольку они уязвимы к атакам MITM. Поэтому их используют совместно с RSA/DSA/ECDSA. В этом случае схема согласования немного изменена. После отправки сертификата сервер также отправляет сообщение ServerKeyExchange, в котором передаёт параметры Диффи-Хеллмана и подписывает их с помощью цифровой подписи RSA/DSA/ECDSA. Т.е. клиент обязательно проверяет валидность цифровой подписи по присланному сертификату сервера, убеждаясь, что он получил корректные параметры, и они не были подделаны по пути к нему:

       ..........                     ..........
       . Client .                     . Server .
       ..........                     ..........
                                         ____ 
          ____                          |====|
         |    |                         |    |
         |____|                         |    |
         /::::/                         |____|

       .--------------.
       . Client Hello .---------->.-------------------.
       '--------------'           . ServerHello       .
                                  . Certificate       .
 .--------------------.          /. ServerKeyExchange .
 .  ClientKeyExcahnge . <-------' . ServerHelloDone   .
 . [ChangeChiperSpec] .\          '-------------------'
 .           Finished . \         .--------------------.
 '--------------------'  '------> . [ChangeCipherSpec] .
                                 /. Finished           .
   .------------------.         / '--------------------'
   . Application Data .<-------'
   '------------------'\          .------------------.
                        '-------->. Application Data .
                                  '------------------'

Таким образом, секретный ключ, который будет использоваться в TLS-сессии, всегда будет различный, и его невозможно будет восстановить, даже имея на руках приватный ключ сертификата сервера, поскольку сервер не сохраняет параметры DH сессии (отсюда и буква E в названиях DHE/ECDHE — эфемерные).

Шифры

Как уже было выяснено, безопасность протокола TLS базируется на четырёх компонентах: аутентичность, конфиденциальность, целостность и совершенная прямая секретность. Каждый из этих компонентов может обеспечиваться различными криптографическими алгоритмами, например, шифрование данных может выполняться с помощью алгоритма AES, а целостность — алгоритмом SHA. Таким образом, шифр, используемый в TLS-сессии, в свою очередь задан из четырёх компонент. Один или несколько компонентов могут быть пустыми, т.е. соответствующий алгоритм не используется, но это снижает (и зачастую делает бесполезной) защищённость.

Например, рассмотрим название шифра, определяемого IANA как TLS_DHE_RSA_WITH_AES_128_CBC_SHA256. Как видно, название шифра включает в себя комбинацию из четырёх алгоритмов, каждый из которых используется для своих задач. Фрагмент TLS обозначает, что это шифр протокола TLS, далее строка DHE указывает на алгоритм PFS, RSA — алгоритм публичного ключа, используемый в сертификате сервера. После секции WITH следуют название алгоритма блочного шифрования: AES, указывается также длина ключа — 128 бит, подстрока CBC уточняет режим шифрования — сцепление блоков шифротекста для повышения стойкости ко взлому. Последним следует алгоритм криптографической хеш-функции SHA256.

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

  • TLS_NULL_WITH_NULL_NULL — никакой защиты нет;
  • TLS_RSA_WITH_NULL_SHA — есть аутентификация и проверка целостности, но контент открыт и не шифруется;
  • TLS_ECDH_anon_WITH_AES_128_CBC_SHA — отсутствует аутентификация, шифр уязвим к атаке MITM.

Поэтому основные рекомендацию по выбору шифрования на сегодняшний день такие:

  • Использовать только протокол TLS, SSL должен быть отключён.
  • Для обеспечения секретности рекомендуется использование ECDHE (с меньшим приоритетом — DHE).
  • Шифрование — использование AEAD шифров, т.е. шифров, обеспечивающих одновременно и целостность, и секретность, например AES с длиной ключа 128 бит и режимом GCM: AES_128_GCM_SHA256. Ни при каких обстоятельствах не использовать NULL, RC4, DES.
  • Аутентификация — использование сертификатов RSA с ключом 2048 бит с алгоритмом хеша SHA256. Использование цифровой подписи ECDSA пока ещё не очень популярно. В случае клиента — всегда обязательно проверять сертификат на валидность.
  • Возобновляемые сессии удобны для сокращения задержки, но для улучшения секретности рекомендуется устанавливать небольшое время жизни для сессий (несколько минут).

Расширения TLS-протокола

В протоколе TLS предусмотрена возможность для создания и использования расширений, которые могут так или иначе влиять как на саму процедуру согласования, так и на передачу данных в рамках TLS-сессии. Выбор используемых расширений определяется клиентом, который в ClientHello передаёт список поддерживаемых расширений. Это касается даже серверных расширений, чтобы дать понять серверу, какие расширения поддерживаются клиентом. Сервер может начать использовать только те расширения, которые поддерживает клиент. Рассмотрим некоторые популярные расширения.

Сессионные билеты

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

Указание имени сервера (SNI)

Server Name Indication — указание имени сервера определено в RFC6066. Как известно, протокол HTTP 1.1 позволял клиенту путём указания HTTP-заголовка Host сообщить серверу, к какому именно сайту нужно выполнить подключение. Это позволило в условиях дефицита ipv4-адресов размещать на одном ip-адресе десятки и даже сотни виртуальных сайтов. Но с применением шифрования TLS такая возможность была потеряна, поскольку на этапе TLS-согласования сервер не знал сертификат какого виртуального сайта следует направить клиенту.

Расширение SNI даёт возможность клиенту в ClientHello обозначить, по какому доменному имени он хочет получить доступ. Таким образом, сервер на этапе согласования имеет возможность отправить нужный сертификат виртуального сайта.

Запрос статуса сертификата

Certificate Status Request или более известный как OCSP stapling, также уже был описан выше. Позволяет клиенту попросить сервер приложить ответ OCSP-сервера о статусе сертификата, чтобы сэкономить на времени одного запроса.

Поддержка шифров на эллиптических кривых (EC)

Действительно, протокол TLS изначально не содержал поддержки EC, поэтому, чтобы клиент и сервер могли использовать шифры с применением криптографии на эллиптических кривых, должно быть задействовано расширение, описанное в RFC4492.

ALPN и NPN

Application-Layer Protocol Negotiation — согласование протокола на уровне приложения. Расширение определено в RFC7301 и позволяет клиенту и серверу согласовать, какой протокол будет использоваться на уровне приложения. Например, клиент может отправить список протоколов уровня приложения в порядке приоритета, которые он поддерживает: “h2-16”, “http/1.1”. А сервер может выбрать из этого списка один и указать его в ServerHello. Данное расширение было специально создано для новой версии протокола http2, чтобы на переходном периоде клиент и сервер могли договориться, какую именно версию они хотят использовать. Данное расширение позволит отказаться от использования механизма http Upgrade.

Расширение ALPN было создано на основе TLS-расширения NPN, которое было разработано в Google для протокола, предшественника HTTP2 — SPDY.

Стоит отметить, что ALPN и NPN достаточно новые расширения. NPN появился в openssl 1.0.0d, а поддержка ALPN появится только в версии 1.0.2, которая ещё в статусе беты.

Heartbeat

Heartbeat — это расширение, опубликованное в RFC6520, реализующее функциональность keep-alive в TLS. Большую известность расширение получило благодаря багу Heartbleed в реализации openssl.

Использования сжатия в TLS

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

Но крест на использовании сжатия в TLS поставила успешная атака CRIME, которая позволяла путём отправки запросов и наблюдению за изменением длины передаваемого шифротекста определять части передаваемых данных, например, http-заголовка Cookie.

Таким образом, сжатие должно быть отключено, и не только для клиента, но и для сервера, чтобы исключить возможность применения подобных атак.

Обзор модулей для использования TLS в Perl

На сегодняшний день свободной, популярной и полной реализацией TLS-протокола и сопутствующих криптофункций является библиотека OpenSSL. Нельзя сказать, что это идеальная реализация: код безумно сложен, очень характерная история безопасности и неповоротливость развития. Но это лучше, чем ничего. Для работы с openssl в Perl разрабатывается и активно развивается низкоуровневая обертка — модуль Net::SSLeay.

Помимо Net::SSLeay cуществуют и другие обертки к OpenSSL, например Crypt::SSLeay. Но на сегодняшний день он не развивается и его использование не рекомендуется, поскольку в нём используется только протокол SSLv3. Также входящий в состав дистрибутива модуль Net::SSL не умеет проводить верификацию сертификата сервера.

Существуют также экзотические имплементации других SSL/TLS-библиотек: Crypt::NSS — обертка к крипто-библиотеке NSS, используемой в Firefox, Crypt::MatrixSSL — обертка к крипто-библиотеке MatrixSSL. Но их возможности достаточно ограниченные, на CPAN нет зависимых от них модулей, т.е. по всей видимости их никто не использует.

Таким образом, большинство модулей на CPAN, которые используют сокеты с использованием шифрования TLS, базируются на Net::SSLeay или производных от него модулях. Поскольку интерфейс библиотеки довольно низкоуровневый и тяжеловат для понимания, существует модуль-обёртка IO::Socket::SSL, который имеет более привычный интерфейс IO::Socket::INET. Кроме IO::Socket::SSL существуют модули AnyEvent::TLS, IO::Async::SSL, POE::Filter::SSL и т.п., которые применяются для работы с TLS-сокетами в соответствующих IO-фреймворках.

На практике, Net::SSLeay напрямую используется не часто. Детальному разбору API модуля можно посвятить отдельную статью. Гораздо интереснее с практической точки зрения модули, построенные на его основе.

IO::Socket::SSL

IO::Socket::SSL — это удобный и отличный выбор для применения. Основное его достоинство конечно же в простом интерфейсе, который наследуется от знакомого и привычного IO::Socket::INET. Второе достоинство — активная поддержка и развитие.

Клиент

Рассмотрим пример тривиального https-клиента:

my $cl = IO::Socket::SSL->new('www.google.com:443')
    or die "error=$!, ssl_error=$SSL_ERROR";
print $cl "GET / HTTP/1.0\r\n\r\n";
print <$cl>;

В зависимости от версий IO::Socket::SSL/Net::SSLeay этот код может вести себя по-разному. Начиная с версии 1.950 IO::Socket::SSL стал проверять сертификат сервера по умолчанию. В версии 1.971 также начинает использоваться TLS-расширение SNI для того, чтобы передать требуемое имя сервера. В версии 1.984 появилась поддержка OCSP stapling, по умолчанию анонсируется поддержка расширения и запрашивается статус сертификата. В версии 2.000 исключается использование версии протокола SSLv3 и SSLv2. Ну и наконец в версии 2.001 по умолчанию включается поддержка ECDHE/DHE для активации PFS.

Конструктор с опциями по умолчанию для последней версии модуля выглядит так:

my $cl = IO::Socket::SSL->new(
    'www.google.com:443',

    # Список шифров в порядке предпочтения
    # Шифры с предшествующим знаком '!' --- запрещены
    SSL_cipher_list => '
        ECDHE-ECDSA-AES128-GCM-SHA256
        ECDHE-ECDSA-AES128-SHA256
        ECDHE-ECDSA-AES256-GCM-SHA384
        ECDHE-ECDSA-AES256-SHA384
        ECDHE-ECDSA-AES128-SHA
        ECDHE-ECDSA-AES256-SHA
        ECDHE-RSA-AES128-SHA256
        ECDHE-RSA-AES128-SHA
        ECDHE-RSA-AES256-SHA
        DHE-DSS-AES128-SHA256
        DHE-DSS-AES128-SHA
        DHE-DSS-AES256-SHA256
        DHE-DSS-AES256-SHA
        AES128-SHA256
        AES128-SHA
        AES256-SHA256
        AES256-SHA
        EDH-DSS-DES-CBC3-SHA
        DES-CBC3-SHA
        RC4-SHA
        !EXP !LOW !eNULL !aNULL !DES !MD5 !PSK !SRP',

    # Полностью исключены SSLv3 и SSLv2
    SSL_version => 'SSLv23:!SSLv3:!SSLv2',

    # Обязательная проверка сертификата сервера
    SSL_verify_mode => SSL_VERIFY_PEER,

    # SNI имя сервера
    SSL_hostname => 'www.google.com',

    # запрос статуса сертификата
    SSL_ocsp_mode => SSL_OCSP_TRY_STAPLE
)

Таким образом, рекомендуемая для работы клиента версия IO::Socket::SSL должна быть не меньше 2.001. Конечно, такая политика изменения опций по умолчанию ломает обратную совместимость, зато обеспечивает высокий уровень безопасности. Поэтому рекомендуется без необходимости не указывать явно параметры, связанные с безопасностью, например, список шифров, т.к. в будущем эти параметры могут оказаться слабыми, а параметры по умолчанию всегда будут изменяться в пользу большей защищённости.

Cервер

Рассмотрим пример простого TLS-сервера.

my $srv = IO::Socket::SSL->new(
    LocalAddr     => '0.0.0.0:1234',
    Listen        => 10,
    SSL_cert_file => 'server-cert.pem',
    SSL_key_file  => 'server-key.pem',
);
$srv->accept;

TLS-сервер должен обязательно иметь сертификат и соответствующий приватный ключ. Для включения PFS IO::Socket::SSL активирует параметры DHE и ECDHE в Net::SSLeay. Кроме того, при выборе шифра сервер будут ориентироваться не по приоритетам, заданным клиентом, а по собственным приоритетам. Таким образом, по умолчанию добавляются следующие опции:

my $srv = IO::Socket::SSL->new(
    ...,

    # Группы шифров в порядке предпочтения
    # Шифры с предшествующим знаком '!' --- запрещены
    SSL_cipher_list => '
        EECDH+AESGCM+ECDSA EECDH+AESGCM
        EECDH+ECDSA +AES256 EECDH
        EDH+AESGCM EDH ALL +SHA +3DES +RC4
        !LOW !EXP !eNULL !aNULL !DES !MD5 !PSK !SRP',

    # Сервер использует свои приоритеты при выборе шифра
    SSL_honor_cipher_order => 1,

    # Заранее сгенированные параметры Диффи-Хеллмана
    # длиной 2048 бит
    SSL_dh => "...",

    # Задаются параметры эллиптической кривой для ECDHE
    # длина 256 бит
    SSL_ecdh_curve => 'prime256v1',
);

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

my $srv = IO::Socket::SSL->new(
    LocalAddr     => '0.0.0.0:1234',
    Listen        => 10,
    SSL_cert_file => {
        'www1.example.com' => 'server-cert-www1.pem',
        'www2.example.com' => 'server-cert-www2.pem',
        ''                 => 'default-cert.pem',
    },
    SSL_key_file => {
        'www1.example.com' => 'server-key-www1.pem',
        'www2.example.com' => 'server-key-www2.pem',
        ''                 => 'default-key.pem',
    },
);

Для каждого хоста задаётся своя пара сертификатов/ключей, а также указывается хост по-умолчанию в случае, если клиент не поддерживает SNI или указывает имя, отсутствующее в списке.

Инъекция параметров IO::Socket::SSL

Возможно вы никогда не используете IO::Socket::SSL напрямую, но вам приходится сталкиваться с модулями, которые используют этот модуль. Если данные модули не предоставляют возможности изменить параметры подключения, но при этом вам требуется их изменить, то для этих случаев создана специальная функция set_args_filter_hack.

Рассмотрим, для примера, модуль Mail::POP3Client, который служит для подключения к POP3-серверу, в том числе и с использованием TLS-подключения. Если POP3-сервер находится в локальной сети и использует сертификат, подписанный неизвестным центром CA или вообще самоподписанный сертификат, то при использовании последней версии IO::Socket::SSL модуль больше не сможет осуществлять подключение. Как один из вариантов решения, можно повлиять на параметры создаваемого сокета с помощью set_args_filter_hack:

use Mail::POP3Client;
use IO::Socket::SSL;

# установка хака
IO::Socket::SSL::set_args_filter_hack( sub {
    my ($is_server, $args) = @_;

    # Явное указание отпечатка сертификата сервера
    $args->{SSL_fingerprint} = 'SHA1$BE:EF:CA:FE:CO:DE:...';
} );

my $pop = Mail::POP3Client->new(
    HOST     => "pop3.server.local",
    USER     => "user",
    PASSWORD => "password",
    USESSL   => 1,
);

printf "Count: %d\n", $pop->Count();

В данном примере мы задаём функцию, которая будет вызываться при создании сокета. В этой функции имеется возможность переопределить аргументы, которые были переданы в конструктор IO::Socket::SSL. Здесь мы указываем ожидаемый отпечаток сертификата в опции SSL_fingerprint. Отпечаток сертификата можно получить, например, с помощью команды openssl:

$ openssl x509 -noout -fingerprint < pop3.server.cert

SHA1 Fingerprint=BE:EF:CA:FE:CO:DE:BE:EF:CA:FE:CO:DE:BE:EF:CA:FE

Это лишь один из вариантов, который можно использовать для самоподписанных сертификатов. Если сертификат заверен неизвестным центром сертификации, можно указать путь к файлу сертификата этого центра в опции SSL_ca_file.

Таким образом можно переопределить и другие опции, которые по тем или иным причинам вас не устраивают. Функция set_args_filter_hack появилась в версии IO::Socket::SSL 1.969.

AnyEvent::TLS

Модуль AnyEvent::TLS может неявно использоваться в других модулях пространства имён AnyEvent, если у сокета указываются параметры TLS.

Клиент

Например, при создании объекта AnyEvent::Handle клиента TLS:

my $h = AnyEvent::Handle->new(
    fh  => $fh,
    tls => 'connect',
);

В этом случае автоматически создаётся объект AnyEvent::TLS, который загружает и инициирует модуль Net::SSLeay. К сожалению, автор AnyEvent::TLS не так активно следит за прогрессом Net::SSLeay по сравнению с разработчиками IO::Socket::SSL, поэтому чтобы получить сравнимый уровень защищённости, необходимо задавать некоторые параметры. Например, для клиента:

my $h = AnyEvent::Handle->new(
    fh      => $fh,
    tls     => 'connect',
    tls_ctx => {

        # Отключаем поддержку SSLv3
        sslv3 => 0,

        # Включаем проверку сертификата сервера
        verify => 1,

        # Включаем поддержку сессионых билетов
        session_ticket => 1,
    }
);

К сожалению, ни OCSP stapling, ни SNI включить невозможно без переопределения метода AnyEvent::TLS::_get_session.

Сервер

В случае TLS-сервера создаётся такой конструктор:

use AnyEvent;
use Net::SSLeay;

my $h = AnyEvent::Handle->new(
    fh      => $fh,
    tls     => 'accept',
    tls_ctx => {

        cert_file => 'server-cert.pem',
        key_file  => 'server-key.pem',

        # Отключаем поддержку SSLv3
        sslv3 => 0,

        # Включаем поддержку сессионых билетов
        session_ticket => 1,

        # Функция вызываемая после создания
        # объекта AnyEvent::TLS
        prepare => sub {
            my $tls = shift;

            # Контекст Net::SSLeay
            my $ctx = $tls->ctx;

            # Инициализация ECDHE
            # Требуется Net-SSLeay >= 1.56 и openssl >= 1.0.0
            if ( exists &Net::SSLeay::CTX_set_tmp_ecdh ) {
                my $curve = Net::SSLeay::OBJ_txt2nid('prime256v1');
                my $ecdh  = Net::SSLeay::EC_KEY_new_by_curve_name($curve);
                Net::SSLeay::CTX_set_tmp_ecdh( $tls->ctx, $ecdh );
                Net::SSLeay::EC_KEY_free($ecdh);
            }
        }
    }
);

В функции-колбеке prepare можно получить контекст Net::SSLeay и произвести с ним некоторые необходимые манипуляции. Например, установить параметры для алгоритма Диффи-Хеллмана на эллиптических кривых. Как мы уже знаем, это позволит использовать шифры с эллиптическими кривыми, которые снижают процессорную нагрузку и обеспечивают лучшую прямую секретность.

По умолчанию AnyEvent::TLS использует заранее сгенерированные параметры обычного DH длиной 1539 бит. Если требуется более надёжное значение, сгенерируйте параметры DH с помощью openssl:

$ openssl dhparam -out dh-2048.pem 2048

Или выберите заранее сгенированные значения в AnyEvent::TLS нужной длины:

tls_ctx => {
    ...
    # Путь к файлу с DH
    dh_file => 'dh-2048.pem',

    # Или встроенные значения со странными названиями
    # skip512, skip1024, skip2048, skip4096
    # schmorp1024, schmorp1539, schmorp2048, schmorp4096, schmorp8192

    dh => 'schmorp2048',
}

Настроить поддержку SNI в AnyEvent простыми методами пока затруднительно.

Заключение

Надеюсь, что эта сжатая статья поможет заполнить пробелы в знаниях о защищённом протоколе TLS и тех принципах безопасности, на которых он основан. Для подробного изучения темы рекомендую следующие источники:

P.S. Protocol::TLS

В качестве Post Scriptum информация к размышлению о небольшом, но амбициозном проекте Protocol::TLS.

Рассмотрев работу протокола TLS, можно сделать достаточно простой вывод: логика протокола (машина состояний/конечный автомат) отделена от криптографических функций. Именно поэтому и возникла идея создания модуля Protocol::TLS, который должен стать реализацией протокола TLS на чистом Perl. При этом реализация набора криптографических функций может выделена в отдельные плагины, которые могут быть основаны на различных криптобекендах в зависимости от требуемого функционала или каких-то других требований. Своеобразный аналог DBI, который подключает нужный драйвер СУБД при создании подключения, при этом предоставляя единый интерфейс приложениям.

На сегодняшний день Protocol::TLS частично реализует RFC5246 (TLS v1.2), поддерживает несколько шифров, в том числе обязательный TLS_RSA_WITH_AES_128_CBC_SHA. В качестве пока единственного криптографического плагина используется модуль CryptX, который является самодостаточным модулем без внешних зависимостей, который несёт в себе C-библиотеку libtomcrypt. Данный криптографический бекенд содержит практически всю необходимую базу современных криптографических алгоритмов, включаю криптографию эллиптических кривых. Библиотека LibTomCrypt имеет двойную лицензию: общественное достояние (Public Domain) и DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE (делайте, что вам вздумается). Модули CryptX и Protocol::TLS имеют обычную Perl-лицензию (Аrtistic || GPL). Поэтому с точки зрения лицензирования такой модуль имеет преимущество по сравнению с OpenSSL, лицензия которого несовместима с GPL.

Также, в качестве эксперимента, создан модуль IO::Socket::TLS, который эмулирует API IO::Socket::SSL, но при этом под капотом использует модуль Protocol::TLS. Такая обёртка может быть удобна для тестирования существующих приложений, основанных на IO::Socket::SSL. Модуль пока не на CPAN из-за неполноты реализации и довольно наглого названия.

В планах реализация поддержки TLS 1.1/1.0, дополнительного криптобекенда на основе модулей на чистом Perl, чтобы обеспечить возможность fatpack-упаковки модуля и всех его зависимостей.

Владимир Леттиев


Как нанять Perl-программиста | Содержание | Обзор CPAN за декабрь 2014 г.
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье