Подключение к сайту бывает защищённым, а бывает нет — это надо знать всем детям. Только мало детей знают, что это значит и как работает.
Я, изучая веб-разработку, узнал об HTTP. Разобраться в нём несложно: в каждой статье о протоколе множество наглядных примеров запросов и ответов. Затем узнал о схеме HTTPS, с которой всё не так наглядно. В ней используется то ли SSL, то ли TLS, и что-то где-то шифруется, и зачем-то нужны какие-то сертификаты. Короче, всё расплывалось в тумане: где пример ответа, запроса, сертификата, как его создать, зачем он нужен и почему гайд по созданию http-сервера уже написал каждый школьник, а https-сервер — недоступная для начинающих разработчиков роскошь?
В связи с этим, предлагаю обсудить протокол TLS и его роль в вебе. Статья состоит из двух частей. В первой поговорим о защите соединения: от чего и как защищаемся, почему именно так, а не иначе, сколько и каких ключей для этого нужно, и разберёмся с системой сертификатов; а в конце создадим свой сертификат и посмотрим, как его использовать для разработки.
Во второй обсудим, как это дело реализуется в протоколе TLS и разберём формат TLS-пакетов по байтам. Ссылка на вторую часть будет здесь, как только она выйдет. Статьи рассчитаны в первую очередь на изучающих веб-разработку, знакомых с HTTP и жаждущих осознать, кто такое https. Но материал актуален для любых применений TLS, будь то веб или не веб.
Предполагается, что вы что-то знаете о симметричном, асимметричном шифровании и электронной подписи — их разбирать я не буду. Напомню лишь, что в симметричных шифрах есть один ключ, который и шифрует, и дешифрует; в асимметричных ключа два: открытый шифрует, закрытый дешифрует; в электронной подписи тоже два: вычисляется подпись с помощью закрытого, а проверяется открытым.
❯ О терминологии и версиях
HTTPS — это не протокол, а схема URI, то есть инструкция для браузера (или любой программы-клиента), как подключаться к серверу. Схема http://
значит, что браузер должен открыть TCP-соединение и отправлять по нему HTTP-сообщения. Схема https://
значит, что надо открыть TCP-соединение, затем TLS-соединение, и отправлять HTTP-сообщения по нему. Про схему подключения HTTPS говорить не очень интересно, поэтому мы будем обсуждать именно протокол, который обеспечивает защиту соединения — TLS.
В начале развития защищённых веб-соединений было слово — и слово было не TLS, а SSL. Так назывался протокол, который придумали в Netscape, чтобы шифровать HTTP-сообщения, и у него даже было три версии: 1.0, 2.0 и 3.0.
Затем развитие SSL перешло в руки IETF. Не то, чтобы протокол от этого радикально поменялся, но у SSL был фатальный недостаток, поэтому пришлось придумать новое название — TLS. Предыдущее же успело закрепиться, поэтому SSL и TLS сегодня часто используются как синонимы (например, в nginx поддержка TLS включается с помощью параметра ssl
). Но я буду использовать только актуальное название TLS.
На момент написания статьи у TLS есть четыре версии: 1.0, 1.1, 1.2, 1.3. Обсуждать и разбирать по байтам будем только последнюю (TLS 1.3), только для исторической справки пару раз заикнусь о предыдущих. Но новые версии TLS выходят не в целях прикола, а потому что в старых находят уязвимости. Поэтому использовать неактуальные версии TLS (как и любые версии SSL) настоятельно не рекомендуется.
❯ Что такое «защищённое соединение»?
В этом разделе много запутанных рассуждений, поэтому в ключевых местах будут краткие резюме. Спойлеры необязательны для понимания, но в них очень много полезного контекста.
Шифрование и MAC
Итак, ситуация: мне 7 лет. Завтра последним уроком физра: грех не прогулять. Поэтому я открываю сообщения в одноклассниках, и собираюсь предложить лучшему другу вместо физры полазить по гаражам. Для этого нужно всего лишь отправить POST-запрос с текстом сообщения к API одноклассников. Далее сообщение для друга буду называть просто «текстом», а под «сообщениями» подразумеваются сетевые: например, HTTP-сообщения.
Телефон у меня есть, но тариф без мобильного интернета. Дома есть вай-фай, но роутером заведует мать: она подписалась на дюжину IT-инфлюенсерш и научилась перехватывать сетевые пакеты. Если прочитает текст, вломит по самое не хочу. Кстати, пакеты она может не только читать, но и модифицировать: менять любые байты, либо вообще генерировать новые сообщения и отправлять мне под видом сервера и наоборот. В жаргоне такая ситуация называется «атака MITM» (Mother-in-the-middle attack).
Протоколы HTTP, TCP, IP (которые используются в схеме http) никак не скрывают передаваемые данные. Поэтому нужно как-то «защитить» соединение, то есть сделать так, чтобы мать не могла ни читать, ни изменять сообщения (по крайней мере, оставаясь незамеченной).
Отмечу идеологически важный момент: наладить защиту соединения выгодно не только мне, но и серверу: например, скорее всего тогда пользователи будут больше ему доверять. Отсюда следует второй момент: надо организовать защиту не для одного сервера на один раз, а придумать систему, которая будет работать со множеством сайтов.
О защите от изменений поговорим чуть позже, а пока сделам защиту от чтения: наладим шифрование данных.
Вообще, там содержится потенциально интересная информация: например, IP-адрес и порт назначения. То есть даже если использовать HTTPS, мать будет знать, к какому сайту мы подключаемся (по крайней мере, его IP-адрес). Для нас это не страшно, так как нам нужно, чтобы она не узнала текст для друга, а не то, что я в принципе пользуюсь одноклассниками. Но в других ситуациях может иметь значение: например, если интернет-провайдер хочет заблокировать IP-адрес, скажем, по требованию какой-нибудь организации, это не составит проблем.
Блокировать сайты по IP-адресу не очень практично. Для этого есть другие способы: например, можно фильтровать DNS-запросы, которые обычно не шифруются, но и этот вариант ненадёжный. Более рабочий способ следующий: во время TLS-рукопожатия клиент в незашифрованном виде в расширении SNI передаёт домен сервера, к которому хочет подключиться (что такое расширения, и почему приходится это делать, обсудим во второй статье). Соответственно можно блокировать сообщения на основе SNI. Однако там указан только домен: путь к конкретной странице же находится внутри HTTP-сообщения, которое TLS шифрует. Именно поэтому так легко заблокировать целый сайт, но так сложно заблокировать только одну страницу.
Но что-то я отвлёкся: TLS в схеме HTTPS защищает только HTTP-сообщения, мы поступим также.
Я буду иллюстрировать обмены сообщениями на таких схемах. Читающим на компьютере шалом, остальным соболезную — компактнее не вышло. Незащищённая передача данных выглядит так:
Итак, проблема: надо шифровать HTTP-сообщения. Решение: я вспомню, что лет через 11 в университете мне прожужжат все уши ассиметричной криптографией, в частности, RSA. Давайте использовать его.
На схеме показано только шифрование запросов. Естественно, чтобы шифровать и ответы, кроме ключей сервера, должна быть ещё одна пара моих ключей RSA. Но это неважно: мы всё равно откажемся от этой схемы, как только обсудим несколько неудобных моментов.
- Первый находится в той части, где «сервер отправляет открытый ключ RSA». Пока что читателю предлагается самостоятельно подумать, почему из-за этого защита накрывается медным тазом, а в подробностях обсудим позже. Спойлер: именно этот момент приводит к понятию сертификата.
- Второй возникает, если задать вопрос: как долго живут ключи RSA? Одна и та же пара используется для многих соединений или на каждое генерируется новая?
Первый вариант: одна для многих соединений. То есть сервер заранее создаёт пару ключей RSA, сохраняет, скажем, в файл на жёстком диске, и использует для всех соединений. В этом случае возникает неприятность: предположим, мать перехватила наш с сервером зашифрованный обмен сообщениями, и сохранила его себе. Она не может прочитать и расшифровать сообщения, но может сохранить прям в зашифрованном виде. Если теперь месяц спустя на сервере произойдёт утечка, и мать узнает закрытый ключ RSA — она сможет расшифровать всё, что сохранила.
Свойство протокола, что никакие сообщения нельзя расшифровать в будущем, даже зная долгосрочные ключи, которые использовались во время соединения, называется прямой секретностью (forward secrecy). Соответственно, если ключи RSA долговременные, прямая секретность не достигается — meh.
Второй вариант: новая пара ключей на каждое соединение. То есть сервер перед обменом сообщениями создаёт пару ключей, хранит в оперативной памяти, и удаляет по завершении обмена — такие ключи называются эфемерными. Раз ключи не долгоживущие, то прямая секретность есть. О том, чтобы не слить их из оперативной памяти за время соединения сервер уж как-нибудь позаботится.
- Третий момент банален: шифрование данных произвольной длины с помощью RSA — это головная боль. По сравнению с симметричными шифрами RSA медленный, как собака. Не настолько, конечно, чтобы не зашифровать мой текст для друга за секунду-другую, но мы же хотим безопасные соединения не только для чатов: ещё для видео, онлайн-игр и прочей чуши.
Сначала разберёмся с производительностью. Из-за неё использовать асимметричное шифрование сообщений непрактично, так что в TLS (и в похожих протоколах, например, SSH) используется симметричное.
Далее под «шифрованием» (в частности, во фразе «ключ шифрования») подразумевается именно симметричное, если не указано иначе.
Тут самое время поговорить о защите от изменений. Да, у нас будет шифрование: если мать не узнает ключ, она не сможет изменить сообщение каким-то осмысленным образом, то есть, например, не сможет заменить мой текст на какой-то свой. Но как минимум она сможет намусорить: поменять случайные или не очень байты запроса на невесть что. Тогда в лучшем случае сервер не поймёт, что от него требуется; а в худшем она намусорит там, где находится текст, и другу вместо моего текста придёт месиво из символов.
Два основных способа защиты от изменений — электронная подпись и MAC (Message Authentication Code, он же имитовстáвка). Первый вам уже должен быть известен: есть пара ключей, закрытым подпись создаётся, открытым проверяется. Во втором ключ всего один: им MAC создаётся, им же проверяется. Очень похоже на разницу между асимметричным и симметричным шифрованием. Похоже ещё и тем, что MAC производительнее ЭП, соответственно раз уж мы всё равно решили, что шифрование будет симметричным, то и смысла использовать ЭП вместо MAC особого нет — поэтому да будет MAC.
Но нам совершенно нет дела, кто создал MAC, главное чтобы не мать — а она не может, потому что (хочется верить) не знает ключ. Поэтому использовать ЭП вместо MAC нам нет смысла.
Ключ MAC ещё называют ключом целостности (integrity key), но я не буду из соображений компактности. Вообще, в разговорах о криптографии речь о MAC заходит намного реже, чем о шифровании, что несправедливо. До сих пор я неспроста говорил, что TLS не просто «шифрует», а «защищает» соединение — только шифрования недостаточно, чтобы назвать соединение «защищённым».
Читатель с орлиным глазом заметит, что на картинке загадочным образом отсутствует часть, где мы с сервером договорились об одинаковых ключах шифрования и MAC, при этом мать их не узнала. Это проблема.
Решение №1 (очевидное): мы заранее их выбрали по какому-то защищённому каналу — такие ключи называются PSK (Pre-Shared Keys). Очевидное, но глупое: мы же этот самый «защищённый канал» и пытаемся наладить. Или мне перед подключением к каждому сайту звонить/лично встречаться с разработчиками? Плюс PSK будет целая куча: свой для одноклассников, свой для ютьюба, свой для сайта моды майнкрафт — как за всеми уследить? О прямой секретности вообще молчу.
Решение №2 (простое): давайте я зашифрую ключи симметричного шифрования и MAC асимметричным шифром и передам их серверу (или сервер зашифрует и передаст мне). Проблем с производительностью RSA не возникнет, потому что ключи фиксированной длины и шифруем их всего один раз за соединение. Такая схема, где используется и симметричная, и асимметричная криптография, называется гибридной.
Что там с неудобными моментами? Первый всё ещё актуален: в том месте, где «сервер отправляет открытый ключ RSA» зияет дырень в безопасности. Второй тоже: прямая секретность получится только если ключи RSA генерируются по новой для каждого соединения и сразу же удаляются. С третьим разобрались.
Итого, с точностью до первого нюанса, схема рабочая — она даже поддерживалась вплоть до TLS 1.2. Только там ключи RSA не были эфемерными, а значит не было прямой секретности. Дело в том, что генерация безопасных ключей RSA требует сравнительно сложных вычислений, поэтому создавать новую пару на каждый чих непрактично. С этим можно было бы смириться, если бы не было альтернатив. Но, оказывается, алгебраисты придумали более простой с точки зрения вычислений способ выбрать ключи шифрования и MAC. Поэтому, начиная с TLS 1.3, использование RSA не поддерживается, а вместо него используется
❯ Обмен ключами Диффи-Хеллмана
Далее просто DH (Diffie-Hellman) — в нём заключается Решение №3 (альтернативное). Статья про TLS, а не про математику, так что в подробности не вдаюсь. Матчасть очень популярно описана, например, на английской википедии, да и на русской ничего так. Важно только следующее: клиент и сервер генерируют два значения, одно секретное, другое не очень (далее соответственно ключ DH и данные DH) и обмениваются последними по незащищённому соединению. Из чужих данных и своего ключа они получают одно и то же число, из которого каким-нибудь предсказуемым образом генерируют ключи шифрования и ключ MAC. При этом если знать только данные DH, но не ключи DH (которые хранятся в секрете), узнать это число практически невозможно.
Такая схема проще, чем RSA: ключи нужны менее длинные для того же уровня безопасности и генерируются легко (обычно это просто случайные числа, тогда как для RSA нужны безопасные простые). Круто! Что по неудобным моментам?
С третьим (производительность) проблем нет. Второй в силе: прямая секретность достигается только если ключи DH эфемерные: генерируются по новой на каждое соединение и удаляются после обмена. Тогда и весь обмен ключами называется эфемерным и обозначается аббревиатурой DHE (Diffie-Hellman Ephemeral). В TLS 1.3 поддерживаются только такие (ранее поддерживались и неэфемерные обмены ключами).
Алгоритмы DH математически обычно основаны либо на конечных полях, либо на эллиптических кривых. В первом случае алгоритмы называются либо просто DH(E), либо FFDH(E) (Finite Field DH(E)), и может быть указан размер ключа (например, FFDHE4096 — ключ длиной 4096 бит). Во втором — ECDH(E) (Elliptic Curve DH(E)) и отличаются тем, какая конкретно эллиптическая кривая используется. Например, алгоритм X25519 использует кривую Curve25519. Вы можете спросить: раз X25519 — это алгоритм DH, почему в его названии не фигурируют буквы DH? В таких моментах не стоит ничего говорить а только бросить загадочний взгляд в мекиканской шляпи.
Наконец, первый нюанс всё ещё актуален, пусть теперь по сети передаются не открытые ключи RSA, а не менее открытые данные DH. Пора обсудить его! Только после резюме.
Резюме 1
«Соединение защищено» значит, что к каждому сообщению применяются два механизма: шифрование и проверка целостности. Первый можно реализовать с помощью либо асимметричного, либо симметричного шифра; второй — либо электронной подписи, либо MAC.
Использовать асимметричные шифры и электронную подпись для сообщений произвольной длины непрактично из-за производительности, поэтому в TLS используются симметричный шифр и MAC. То бишь есть два ключа: ключ шифрования и ключ MAC, одинаковые на сервере и на клиенте. С помощью них можно зашифровать и посчитать MAC для любого сообщения перед отправкой, а по получении расшифровать и проверить MAC.
Выбор этих ключей происходит в начале соединения — во время так называемого рукопожатия TLS — с помощью алгоритма обмена ключами DHE. Клиент и сервер генерируют ключи DH и данные DH, обмениваются последними по незащищённому соединению. Зная свой ключ DH и чужие данные DH, они получают одно и то же число, которое никто больше не может узнать — из него генерируются ключ шифрования и ключ MAC. После этого ключи DH удаляются, чтобы их нельзя было слить в будущем, а значит и расшифровать прошедшие обмены сообщениями — достигается прямая секретность.
Обращаю внимание: для защиты сообщений используется только симметричная криптография (шифрование и MAC), а асимметричная (DH и, как увидим далее, ЭП) используются только во время рукопожатия. Асимметричное шифрование (в частности, RSA) в TLS 1.3 не используется вообще.
❯ Аутентификация
Итак, в обмене ключами есть серьёзная проблема: сервер передаёт мне свои данные DH по сети. Как я могу быть уверен, что их не подменили по пути? Никак. Возможно такое развитие событий:
- Мать получает на роутере мои данные DH и видит, что я пытаюсь произвести обмен ключами с сервером.
- Вместо того, чтобы честно перенаправить их на сервер, она завершает обмен ключами сама: то есть генерирует свой ключ DH и отправляет мне свои данные DH. В итоге получаются, назовём их, «левые» ключи шифрования и MAC — я думаю, что они общие у меня и сервера, но на самом деле они общие у меня и матери.
- Производит отдельный обмен ключами с сервером и получает «правые» ключи.
- Когда через роутер проходят мои HTTP-сообщения, расшифровывает их левым ключом и отбрасывает мой MAC; читает содержимое; с помощью правых ключей шифрует сообщение и считает свой MAC; и отправляет это дело серверу. Затем делает то же с ответами, только в обратном порядке.
Аналогичная ситуация могла быть с RSA: только мать бы генерировала не ключ и данные DH, а пару ключей RSA, и подменяла бы открытый ключ RSA сервера на свой. В любом случае, надо как-то с этим бороться.
Я должен убедиться, что данные DH по пути от сервера никто не изменил. Уже обсуждали, что есть два способа это сделать: электронная подпись и MAC. Для MAC нужен общий ключ, который мы как раз пытаемся организовать, так что MAC отпадает. Придётся использовать электронную подпись: у сервера будет пара ключей ЭП, закрытым он подпишет данные DH перед отправкой, а по получении я проверю подпись открытым.
Какой вариант выбирать не имеет значения, с точки зрения защиты от атаки MITM. В любом случае, если мать изменит чьи-то данные DH (но не изменит открытый ключ ЭП, об этом сейчас поговорим), хотя бы одна сторона это заметит и сможет разорвать соединение. Однако практическая разница возникнет, когда будем говорить о сертификатах: тогда и увидим, почему электронную подпись почти всегда делает именно сервер.
Вопрос: как мне узнать открытый ключ ЭП сервера, чтобы проверить подпись?
- Вариант 1: может быть, сервер просто отправит его мне? Так не получится, потому что возникает та же проблема: до этого мы надеялись, что мать не подменит данные DH, теперь надеемся, что она не подменит открытый ключ ЭП сервера: а она подменит.
Иллюстрация, почему сервер не может просто отправить свой открытый ключ ЭП
- Вариант 2: может быть, я храню его у себя заранее? Это опять получается PSK с теми же недостатками, что и раньше: я должен перед подключением к сайту звонить/встречаться с разработчиками, чтобы узнать ключ ЭП; непонятно как уследить за кучей ключей от множества сайтов.
Такая схема, однако, широко применяется в SSHТам доменам серверов в файле
.ssh/known_hosts
вручную сопосталяются их открытые ключи ЭП. У меня есть статья об SSH, может быть полезно почитать и обратить внимание, как SSH (не) отличается от TLS: всё что мы обсуждали до сих пор там работает точно так же.В SSH эта схема уместна, поскольку протоколом пользуются разработчики, у которых есть контакт с администраторами серверов. Плюс один разработчик подключается не к тысячам сайтов, а в худшем случае к десяткам серверов, поэтому уследить за ключами возможно.
Если бы данные DH были неэфемерными, можно было бы сопоставлять серверу не открытый ключ ЭП, а сами данные DH (либо вообще сразу ключи шифрования и MAC). Но тогда теряется прямая секретность, поэтому приходится делать данные DH эфемерными и использовать электронную подпись.
- Вариант 3: второй вариант был рабочий, но непрактичный, слишком много хлопот сваливается на пользователя (меня). Так давайте скинем их на кого-нибудь другого.
❯ Сертификаты
Итак, давайте сопоставлять серверу его открытый ключ ЭП будет какое-нибудь третье лицо, будем называть его CA (Certificate Authority, центр сертификации). Он будет решать проблемы, из-за которых мы забраковали PSK: налаживать с владельцем/разработчиком серверов защищённый канал связи, чтобы передать открытый ключ ЭП и следить за актуальностью ключей. Только CA один, а сайтов много, так что скорее не он будет связываться с владельцами серверов, а владельцы серверов, которые хотят поддерживать защищённые соединения, будут связываться с CA.
То есть владелец сервера связывается с CA, отправляет свой домен/IP-адрес и открытый ключ ЭП, а CA выдаёт ему так называемый «сертификат». Это файл, в котором записаны две главные вещи: домен/адрес сервера и его открытый ключ ЭП. Его сервер будет отправлять мне в начале соединения. По сути сертификат «сопоставляет» серверу его открытый ключ ЭП.
Чтобы от сертификата был смысл, мне (клиенту) нужен способ убедиться, что именно CA его выдал. С этим вновь поможет электронная подпись. У CA есть пара ключей ЭП, закрытым он подписывает сертификат. А теперь главный фикус: открытый хранится на клиенте заранее, и с помощью него я проверяю достоверность подписей всех сертификатов. Здесь, кстати, важно то самое свойство «невозможности отказа» (non-repudiation), которое есть у электронной подписи.
Важный шаг: перед тем, как выдать сертификат, CA должен проверить, что этот «владелец» правда владеет доменом/адресом. Иначе мать бы просто могла получить сертификат, сопоставляющий домену одноклассников её ключ. Такая проверка обычно заключается в том, чтобы запустить на домене HTTP-сервер, возвращающий какое-нибудь секретное значение, либо добавить DNS-запись типа TXT с секретным значением. Также важно, чтобы клиент проверял, что домен из сертификата совпадает с тем, к которому он подключается. Атака, которая возможна, если этого не делать, проиллюстрирована ниже.
Проверять, что «владелец» владеет закрытым ключом ЭП, соответствующим открытому из сертификата, вообще говоря, необязательно. Если у владельца его нет, мои поздравления: сертификат совершенно бесполезен. Но и мать не сможет его использовать для атаки. Тем не менее, обычно CA проверяют владение закрытым ключом на всякий случай (о процессе получения сертификата ещё поговорим).
По сути, схема с сертификатами принципиально не отличается от PSK, просто сопоставляю ключ серверу не я, а CA. Я лишь проверяю подпись на сертификате и убеждаюсь, что сертификат правда выдал CA, а не, скажем, мать сама себе. При этом я доверяю CA в том, что он не выдаёт кому попало сертификаты с какими попало ключами, а проверяет, что «владельцы», запрашивающие сертификаты для своих серверов, правда ими владеют.
Вы скажете: опять что ли мне заранее нужно что-то хранить на своём устройстве (открытый ключ ЭП CA)? Я отвечу: да. Это цена, которую приходится платить за защиту соединений. Совсем ничего не хранить заранее и получать все по сети не получится: ведь никаким незащищённым данным из сети доверять нельзя, их все можно подменить. Но с сертификатами эта цена совсем небольшая: нужно хранить всего один (или, как увидим далее, несколько, но совсем немного) ключей, которые обеспечивают защиту при подключении к потенциально миллионам сайтов.
Резюме 2
Самое главное действие, которое происходит во время рукопожатия TLS — обмен ключами шифрования и MAC. Как только он завершён, защищённое соединение по сути установлено.
Чтобы в обмен ключами нельзя было вмешаться, клиенту нужен способ убедиться, что данные DH сгенерировал именно сервер. Для этого последний их подписывает с помощью своей пары ключей ЭП.
Чтобы проверить подпись, я должен знать открытый ключ ЭП сервера. Его нельзя просто отправить по сети, потому что ключ можно подменить. Поэтому нужен более надёжный способ сопоставить серверу его открытый ключ ЭП.
Можно было бы заставить пользователя (клиента) делать это вручную (как в SSH), но у пользователя лапки. Поэтому это делает третье лицо — центр сертификации (CA). Он выдаёт владельцам серверов «сертификаты», которые ставят серверу в соответствие его открытый ключ ЭП — их сервер отправляет клиенту при каждом подключении. Сертификаты подписаны закрытым ключом ЭП CA, чтобы их нельзя было подделать. Открытый же ключ ЭП CA заранее хранится на устройстве пользователя, чтобы проверять достоверность сертификатов.
В TLS 1.3 открытый ключ из сертификата не используется для шифрования! Это ключ электронной подписи: с помощью него проверяется подпись данных DH.
Такая схема даже поддерживалась до TLS 1.3. Но мы опять упираемся в то, что для прямой секретности данные DH должны быть эфемерными. То есть в этой схеме пришлось бы получать новый сертификат на каждое соединение, что непрактично. А открытый ключ ЭП сервера не должен быть эфемерным, поэтому в сертификаты кладут его, а не данные DH. Малость запутано, зато практично, и есть прямая секретность.
Однако TLS поддерживает аутентификацию клиента по сертификату в качестве дополнительной меры безопасности. Как это работает, увидим во второй части. Так что сертификат может быть не только у сервера.
На данный момент мы по сути разобрались, как работает TLS. Последняя иллюстрация довольно точно отражает, что происходит во время TLS-рукопожатия. Оставшаяся часть статьи посвящена более подробному разбору сертификатов: мы обсудили только теоретические основы, но в реальности инфраструктура вокруг них довольно сложная.
С точки зрения TLS, сертификат — это просто набор байтов. Протокол не определяет ни их формат, ни алгоритм проверки на достоверность, он лишь предоставляет место, где их отправлять во время рукопожатия. Конкретный формат сертификатов, который чаще всего используется с TLS описан в стандарте X.509. Он намного более гибкий, чем наши сертификаты, но за счёт этого и более сложный — сейчас будем обсуждать.
Однако важно не забывать самое главное, зачем нужен сертификат — сопоставить серверу его открытый ключ ЭП. Сколько бы дополнительной ерунды не было в X.509, задача сертификата остаётся такой.
❯ X.509
Минутка терминологии: того, кому выдали сертификат, будем называть «субъектом» или «владельцем» сертификата; того, кто выдал — «издателем». Таким образом, наши сертификаты состояли из открытого ключа ЭП владельца, его домена/адреса и подписи издателя.
Формат
Сертификат X.509 представляет собой последовательность байтов, в которых каким-то образом закодирован список значений различных полей. Каким именно обсуждать не будем, только разберём основные поля и их назначения.
- Subject Public Key Info состоит из двух подполей: открытый ключ ЭП владельца (Subject Public Key) и используемый алгоритм ЭП (Algorithm). Этот момент у нас не был предусмотрен: так как алгоритмов ЭП множество, надо указывать, какой конкретно используется. А сам открытый ключ точно как было у нас.
- Issuer — кто издал. Состоит из нескольких подполей: Common Name (CN) — обычно название компании-CA; Country (C) — страна CA; Organization (O) — юридическое название организации; и др. Все подполя могут быть пустыми. Смысл поля Issuer в основном, чтобы человек мог узнать, кто выдал сертификат. Как ещё оно может использоваться, увидим дальше.
- Subject — кто владелец. Формат и смысл такие же как у Issuer, только если это сертификат сервера, в CN обычно пишут его домен (чей ещё может быть сертификат, сейчас увидим).
- Serial Number — серийный номер. Нужен, например, чтобы отзывать сертификаты. Как это делается описывать не буду: задача нетривиальная.
- Validity с подполями Not Before и Not After — срок действия сертификата, соответственно его начало и конец.
- Расширения — дополнительная информация о сертификате. Их много, но нам интересны только самые базовые:
- Subject Alt Name (не Names!) или SAN — список доменов/адресов сервера. Раньше эта информация бралась из подполя CN поля Subject, но там можно было указать всего один домен, поэтому придумали SAN. Сегодня же многие браузеры вообще не смотрят на CN, так что все домены нужно обязательно указывать в SAN.
- Key Usage — обсудим позже.
- Basic Constraints — обсудим позже.
- Ещё всякая дребедень, которая нам не интересна.
- Signature Value и Signature Algorithm — подпись издателя и алгоритм подписи. В X.509 подпись обычно считается не от всего сертификата, а от его хеша. То есть составляется сертификат без подписи, от него считается хеш, подпись от которого вставляется в Signature. Конкретная хеш-функция, как и алгоритм ЭП, указаны в Signature Algorithm (например,
ecdsa-with-SHA256
— хеш-функция SHA256, алгоритм подписи ECDSA).
Названия полей не общепринятые. Я пишу как в OpenSSL, но, например, в средстве просмотра сертификатов на винде они отличаются (Valid from/to вместо Not before/after, Public Key вместо Subject Public Key, и т.д.).
.crt
, .cer
, .pem
, .p12
, .pfx
, .der
и др. Некоторые из них бинарные, некоторые текстовые: там байты, составляющие сертификат закодированы в base64. По расширению не всегда понятно, что лежит в файле: в .pem
, например, может быть как сертификат, так и пара криптографических ключей, запрос на подпись (CSR, о них ещё поговорим), а то ещё чего. Но расширение погоды не делает. Я буду использовать .crt
потому что хочу, и потому что винда по умолчанию его признаёт.
Иерархия CA, промежуточные и корневые сертификаты
В нашей схеме есть очень непрактичный момент: CA всего один. Это проблема: во-первых, сайтов очень много, а сертификаты нужны всем. Во-вторых, получается своеобразная монополия. Нет конкуренции, а значит у CA меньше мотивации выдавать сертификаты честно.
Поэтому в реальности, конечно, CA не один. Во-первых, у каждого устройства (а точнее у каждой программы, работающей с TLS) есть свой список «доверенных» CA. Их открытые ключи хранятся на устройстве заранее. Этих CA чаще называют «корневыми», но мне не нравится это слово. Почему — напишу дальше в спойлере, а пока буду говорить только о «доверенных» CA.
Можно было бы хранить открытые ключи доверенных CA просто так, скажем, в массиве строк. Но в X.509 они хранятся в таких же сертификатах, как и открытые ключи серверов. То есть для каждого доверенного CA есть так называемый «доверенный» (или «корневой») сертификат: его субъектом является этот самый доверенный CA, а открытый ключ лежит в поле Subject Public Key.
Что лежит в остальных полях доверенного сертификата не очень важно, об этом ещё будет спойлер. Поле с подписью чаще всего CA заполнит сам: подпишет сертификат закрытым ключом из той же пары, из которой открытый лежит внутри. Такой сертификат называется «самоподписанным» (self-signed). Но доверенный сертификат не обязательно самоподписанный, а самоподписанный не обязательно доверенный, об этом тоже ещё скажу.
Вопрос: а откуда доверенные сертификаты берутся на устройствах пользователей? Ответ: поставляются с ПО. Обычно списки доверенных сертификатов в каждом приложении можно редактировать: удалять предустановленные и добавлять свои.
- У Microsoft есть свой список доверенных корневых CA. Он поставляется с Windows, можно посмотреть и изменить с помощью утилиты
certmgr.msc
(там вкладка «Доверенные корневые центры сертификации/Trusted Root Certification Authorities»). Им по совместительству пользуются приложения Microsoft, в частности, Edge. - Свой список есть у Apple, поставляется с iOS и macOS. На маке можно посмотреть и изменить в приложении «Связка ключей» (Keychain Access), на айфоне где-то в настройках; его использует Safari.
- На линуксе зависит от дистрибутива, читателю предлагается найти самостоятельно.
- Свой список есть у Mozilla. Его используют, в частности, Firefox и NodeJS (где он захардкожен). В Firefox посмотреть и изменить можно в настройках браузера. В NodeJS получить можно из переменной
tls.rootCertificates
пакетаnode:tls
. Чтобы изменить, можно передать функциям, работающим с TLS (таким какtls.connect
иtls.createServer
) свой список в параметреca
. - Chrome использует список из операционной системы, на которой запущен.
В примерах видно, что списки доверенных CA отличаются у разных программ. Поэтому понятие «доверенный сертификат» применимо только к конкретному устройству и конкретному приложению. Одному CA одно устройство может доверять, а другое нет. Даже на одном устройстве один браузер может доверять, а другой нет.
Но таких доверенных CA всё равно довольно мало (обычно пара сотен), а добавление нового в список доверенных — сложная и долгая процедура. Поэтому X.509 предусматривает некоторую делегацию ответственности.
Пусть есть CA «JabujRoot Llc», которому доверяет моё устройство (то есть на устройстве есть доверенный сертификат с его открытым ключом). Пусть есть другой CA «Jabuj Ltd», которому моё устройство не доверяет (его ключа у меня нет). Если Jabuj Ltd выдаст сертификат серверу одноклассников, моё устройство не сможет проверить его подпись, а значит не установит TLS-соединение. Выходов несколько.
- Я могу вручную добавить сертификат Jabuj Ltd в список доверенных на своём устройстве. Моё устройство станет ему доверять, но чужие нет: а кому нужен CA, которому доверяет всего одно устройство? Владельцы серверов не захотят получать у него сертификаты и пойдут к другим CA.
- Jabuj Ltd может подать заявку в списки доверенных CA поставщиков ПО. Но это сложный и долгий бюрократический процесс.
- Jabuj Ltd может обратиться к JabujRoot Llc, чтобы тот выдал так называемый «промежуточный» сертификат. У него в поле Subject Public Key указан открытый ключ Jabuj Ltd, а подписан сертификат закрытым ключом JabujRoot Llc.
Получается, сертификату Jabuj Ltd я не доверяю напрямую (не храню у себя заранее). Но он подписан JabujRoot Llc, которому я доверяю, поэтому и сертификаты от Jabuj Ltd считаю достоверными. Получается цепочка доверия (chain of trust), состоящая из доверенного сертификата (JabujRoot Llc), промежуточного (Jabuj Ltd) и листового сертификата одноклассников (в том смысле, что это лист дерева сертификатов, англ. end-entity certificate).
Промежуточных сертификатов может быть больше одного, а может не быть совсем. Чтобы проверить листовой сертификат, мне надо знать открытые ключи всех промежуточных CA. Так как заранее у меня на устройстве они не хранятся, то сервер во время рукопожатия должен отправить всю цепочку, а не только свой сертификат. Доверенный (корневой) сертификат отправлять не обязательно: он хранится у меня на устройстве. Если всё же отправить, скорее всего, он будет проигнорирован.
Проверять достоверность листового сертификата моё устройство будет так: оно увидит, что у сертификата одноклассников издатель Jabuj Ltd, возьмёт сертификат Jabuj Ltd, достанет оттуда открытый ключ и проверит подпись сертификата одноклассников. Затем увидит, что издателем сертификата Jabuj Ltd является JabujRoot Llc, возьмёт сертификат JabujRoot Llc, достанет открытый ключ и проверит им подпись на сертификате Jabuj Ltd. Затем увидит, что сертификат JabujRoot Llc доверенный, а значит сертификат одноклассников достоверен и можно начинать обмен ключами.
Как именно устройство ищет сертификат издателя? Один из способов: сопоставлять поля Subject и Issuer. Например, если устройство видит в сертификате одноклассников Issuer: CN=Jabuj Ltd
, оно ищет сертификат с Subject: CN=Jabuj Ltd
. Таким образом устройство идёт вверх по цепочке, пока не найдёт какой-нибудь доверенный. Есть и другие механизмы, например, расширения SKID/AKID, но о них, возможно, во второй части.
Пара слов о расширениях Basic Constraints и Key Usage. В первом указано, принадлежит ли сертификат серверу или CA (то есть является листовым или нет). В первом случае там пишут CA:false
, во втором CA:true
. Если это сертификат CA, можно также указать ограничение на количество промежуточных сертификатов (pathlen
). Например, CA:true; pathlen=2
значит, что в цепочке между данным сертификатом и листовым может быть не более двух промежуточных.
Key Usage определяет, как можно использовать ключ из Subject Public Key. Если это промежуточный сертификат, должно быть указано keyCertSign
, чтобы им можно было проверять подписи других сертификатов. Если листовой — digitalSignature
, чтобы можно было проверять подпись данных DH во время рукопожатия. Есть и другие значения, которые, например, могли использоваться в других схемах обмена ключами, но в TLS 1.3 они не слишком актуальны.
Никакие поля, кроме Subject Public Key в доверенном сертификате не имеют значения. Например, подпись: на промежуточных и листовых она нужна, чтобы их нельзя было изменить при или до передачи по сети. Но доверенные по сети не передаются, изменить их может только устройство пользователя. А в этом нет смысла: ведь оно же занимается проверкой достоверности. Если ему понадобится изменить сертификат, оно может просто проигнорировать подпись.
То же самое с самоподписанными сертификатами: можно было бы их не подписывать вовсе, но ради однообразия X.509 предписывает это делать. Многие программы (в частности, OpenSSL по умолчанию) вообще игнорируют подпись (и, кстати, расширения Basic Constraints и Key Usage) на доверенных самоподписанных сертификатах.
Доверенный сертификат необязательно самоподписан. Вы можете взять промежуточный сертификат (который не может быть самоподписан) и добавить в список доверенных: и вот он уже не промежуточный. Самоподписанный сертификат не обязательно доверенный, но тогда он просто бесполезен.
Поэтому мне не нравится слово «корневой». Под ним обычно подразумевается именно «хранящийся заранее на устройстве», но звучит словно «такой, что CA выдал сам себе». Как видно, это неверно: сертификат может быть выдан другим CA, при этом быть доверенным. Большая редкость, но такое возможно.
Ещё существует кросс-сертификация (cross-signed certificates): например, два CA могут подписать сертификаты друг друга, на каких-то устройствах доверенным будет один, а на других второй. Тогда самоподписанных сертификатов в цепочке вообще нету, и слово «корневой» мне кажется неуместным. Но оно хорошо закрепилось, поэтому приходится с ним жить.
Резюме 3
CA образуют иерархию: есть доверенные для данного устройства и приложения CA — те, которым оно доверяет, и чьи открытые ключи ЭП заранее хранит. Доверенные CA выдают сертификаты промежуточным, а промежуточные — владельцам серверов, такие сертификаты называются листовыми.
Открытые ключи корневых CA хранятся в корневых сертификатах — их списки поставляются вместе с ОС или ПО, их обычно можно редактировать. Во время рукопожатия сервер отправляет всю цепочку сертификатов вплоть до доверенного.
❯ Отойдём от технической части
Пара слов о философской стороне сертификатов.
- Как на этом заработать? В системе есть очевидное место, где можно срубить деньжат: то, где CA выдаёт сертификат серверу. За это можно потребовать скромное или не очень пожертвование. Большинство CA так и делает, но не все: например, Let's Encrypt преследует благородную цель принести шифрование в жизнь каждого, поэтому раздаёт сертификаты направо и налево за бесплатно. Достаточно подтвердить, что вы владеете доменом.
- Важно понимать, что сертификат НЕ делает: он НЕ подтверждает, что сайт не зловредный! Он лишь сопоставляет серверу открытый ключ ЭП.
Замочек в адресной строке браузера НЕ значит, что сайт безопасный! Замочек значит только, что соединение защищено от MITM. Он совершенно НЕ гарантирует, что сайт не мошеннический, например, не фишинговый.
Об уровнях валидацииВ статье до сих пор фигурировали только так называемые DV (Domain Validated) сертификаты — чтобы получить такой, достаточно подтвердить владение доменом. Кроме DV, существуют OV (Organisation Validated) и EV (Extended Validated) — для таких CA проверит, что вы являетесь юридическим лицом, и ещё что-нибудь.DV-сертификат получить очень просто (ещё и бесплатно с Let's Encrypt) — для мошенников труда не составит. А от OV и EV нет толку. Он бы был, если бы браузеры вели себя по-разному в зависимости от типа сертификата: например, если сервер отправил DV-сертификат, показывали огромное окно и надпись «Возможно, вас собираются обмануть!» цианом по магенте, а кнопку «Всё равно перейти» прятали мелким текстом за пятью кликами. Но так не происходит.
Была идея, чтобы если сервер отправил DV-сертификат, замочек был серый, а если OV/EV — зелёный и рядом с ним отображалось название организации. Но, только честно, хоть кто-то заметил? Поэтому и от практики менять цвет замочка в последнее время отказываются, и замочек теперь совершенно не значит, что сайт добросовестный: только что вы защищены от атак MITM. Хром недавно вообще перестал показывать замочек в рамках инициативы «HTTPS by default». Рекомендую на эту и смежные темы выступление Троя Ханта «I'm Pwned. You're Pwned. We're All Pwned».
Чтобы узнать тип листового сертификата, надо посмотреть в поле Subject: если там только домен, то это DV-сертификат, если ещё название организации, то OV/EV. Сегодня многие большие сайты не заморачиваются с OV/EV: на момент написания статьи DV-сертификаты используют, например, Хабр, StackOverflow и даже Гугл! Скрины прилагаются, для сравнения на последнем сертификат Яндекса, который на момент написания статьи то ли OV, то ли EV.
❯ Минутка практики
Практика будет во второй части, а сейчас посмотрим, как создаются CA и сертификаты. Для этого я буду использовать библиотеку OpenSSL — она содержит множество инструментов для работы с шифрами, ЭП и т.п. У меня уже работает команда openssl
, если у вас нет, установить предлагается самостоятельно.
Задача: выдать себе сертификат для домена localhost
, который можно использовать для разработки.
- Начнём с создания CA. Во-первых, нам нужна пара ключей электронной подписи.
Создаю ключ ЭП и кладу в файл root.keyOpenSSL умеет генерировать самые разные ключи. Обычно все создают ключи подписи RSA, но я сделаю вид, что весь такой оригинальный, и буду использовать ECDSA (Elliptic Curve Digital Signature Algorithm), он считается более безопасным, и ключи короче.
ECDSA работает с эллиптическими кривыми: чтобы сгенерировать ключ, нужно выбрать конкретную. Списки безопасных кривых умные криптографы уже составили за нас. Я возьму
prime256v1
(она жеsecp256r1
, она же P-256), потому что работу с ней поддерживают большинство браузеров.Кроме собственно генерации ключа, я сделаю дополнительный шаг: зашифрую его. Если бы мы генерировали ключ RSA, OpenSSL бы по умолчанию предложил указать пароль, с помощью которого его зашифровать. С ECDSA почему-то не так: ключ шифруется отдельной командой. Надо только указать конкретный шифр, я выберу AES256.
openssl ecparam -genkey -name prime256v1 | openssl ec -aes256 -out root.key
Команда сгенерирует ключ, попросит ввести пароль, чтобы его зашифровать (пароль запоминаем, он понадобится другим командам, чтобы расшифровать ключ), и запишет зашифрованный ключ в файл root.key.
Во-вторых, нужен корневой сертификат. Я сделаю самоподписанный.
Создаю корневой сертификат и кладу в root.crtСоздание сертификата в OpenSSL происходит в два шага. Сначала субъект создаёт «запрос на подпись» (CSR, Certificate Sign Request) — это файл с расширением.csr
, в котором указаны значения Subject и Subject Public Key Info для будущего сертификата. Субъект подписывает CSR своим закрытым ключом и отправляет к CA.CA проверяет, что субъект владеет доменом, проверяет подпись CSR (это необязательно с точки зрения безопасности, скорее на всякий случай), составляет сертификат (берёт данные субъекта из CSR, заполняет недостающие поля и подписывает) и отправляет назад субъекту.
Эти два шага в OpenSSL делаются отдельными командами. Для работы CSR есть команда
openssl req
. Создаю CSR: указываю значение Subject, и что ключи для Subject Public Key Info и подписи надо брать из root.key. В Subject желательно заполнить Common Name (CN), остальное не принципиально. Я укажу ещё Country (C) и Organization (O), чтоб неповадно было.openssl req -new -key root.key -subj "/CN=Jabuj Root CA/C=RU/O=JabujRoot Llc" -out root.csr
Команда попросит пароль от root.key и запишет CSR в root.csr. Так как я хочу самоподписанный сертификат, то отправлять CSR мне некому: я просто «удовлетворяю» запрос на подпись самостоятельно с помощью
openssl x509
.Только нужно указать недостающие данные: срок действия будет один год (
-days 365
), закрытый ключ для подписи тот же, который использовали для CSR (-signkey root.key
); указываю хеш-функцию для подписи (-sha256
). Это всё: OpenSSL умный, сам увидит, что мы создаём самоподписанный сертификат, и сам заполнит Issuer (скопирует из Subject).openssl x509 -req -sha256 -in root.csr -signkey root.key -days 365 -out root.crt
Ввожу пароль от root.key и получаю корневой сертификат root.crt. По-хорошему надо было бы к сертификату добавить расширения Basic Constraints и Key Usage: но уже обсуждали, что на корневых сертификатах они почти всегда игнорируются, так что чёрт с ними.
На самом деле, самоподписанный сертификат можно создать одной командой, которую любопытный читатель найдёт в интернете. Мне для ясности удобнее двумя.
- Корневой CA есть. Можно было бы сразу им подписать сертификат сервера, но чтобы жизнь мёдом не казалась я создам ещё промежуточный.
Генерирую пару ключей ЭП для промежуточного CA в intermediate.key
openssl ecparam -genkey -name prime256v1 | openssl ec -aes256 -out intermediate.key
Создаю сертификат промежуточного CA: он уже не самоподписан, а подписан корневым CA.
И кладу его в intermediate.crtГенерирую CSR в файл intermediate.csr (понадобится пароль от intermediate.key):openssl req -new -key intermediate.key -subj "/CN=Jabuj Intermediate CA/C=RU/O=Jabuj Ltd" -out intermediate.csr
Теперь подписываю: в этот раз сертификат не самоподписан, поэтому указываю закрытый ключ для подписи (
-CAkey root.key
) и сертификат издателя (то есть корневого CA,-CA root.crt
) — из него OpenSSL возьмёт значение для Issuer.Кроме того, для промежуточных сертификатов требования строже: там уже не отвертишься отсутствующими расширениями. Поэтому в Basic Constraints надо указать, что это сертификат CA (
basicConstraints=CA:true
), а в Key Usage — что ключ из сертификата будет использовать для проверки подписи других сертификатов (keyUsage=keyCertSign
).У OpenSSL нет возможности передать список расширений в командной строке — только в файле. Можно было бы создать файл extensions.ext, написать туда эти строчки и передать OpenSSL (
-extfile extensions.ext
), но я не хочу засорять компьютер временными файлами, поэтому использую конструкцию-extfile <(echo ...)
.openssl x509 -req -in intermediate.csr -days 365 -CA root.crt -CAkey root.key -extfile <(echo -e "basicConstraints=CA:truenkeyUsage=keyCertSign") -out intermediate.crt
Понадобится пароль от root.key.
- Наконец, создаю листовой сертификат.
Сначала пару ключей сервера в server.key
openssl ecparam -genkey -name prime256v1 | openssl ec -aes256 -out server.key
Затем сертификат в server.crtСоздаю CSR. В качестве CN обычно указывают домен сервера, но это не обязательно: его мы укажем в расширении Subject Alt Name. Поэтом в CN напишу какую-нибудь ерунду.openssl req -new -key server.key -subj "/CN=Jabuj WebSite" -out server.csr
Понадобится пароль от
server.key
. Наконец, создаю сам сертификат. Он листовой, поэтому в Basic Constraints пишемCA:FALSE
. Ключ из сертификата будет использоваться для подписи данных DH, поэтому в Key Usage пишемdigitalSignature
. Наконец, нужно обязательно указать домен сервера в SAN (subjectAltName=DNS:localhost
).openssl x509 -req -in server.csr -days 365 -CA intermediate.crt -CAkey intermediate.key -extfile <(echo -e "basicConstraints=CA:falsenkeyUsage=digitalSignaturensubjectAltName=DNS:localhost") -out server.crt
Если вы не создавали промежуточный сертификат, можно было подписать листовой корневым напрямую:
-CA root.crt -CAkey root.key
. - Не забываем, что чтобы проверить листовой сертификат, надо знать все промежуточные. Поэтому перед созданием сервера, нужно конкатенировать их в один файл (
cat server.crt intermediate.crt > chain.crt
).
Осталось добавить root.crt в список доверенных браузера (как это делается в вашем браузере, предлагаю найти самостоятельно) и можно запускать HTTPS-сервер.
import ... from 'node:https'
на require('https')
(то же самое для fs
) предлагается самостоятельно.
// Файл server.mjs
import https from 'node:https'
import fs from 'node:fs'
const server = https.createServer({
// Закрытый ключ, которым сервер будет подписывать данные DH
key: fs.readFileSync('./server.key'),
// Пароль от ключа
passphrase: '...',
// Цепочка сертификатов, которую сервер будет отправлять во время рукопожатия
cert: fs.readFileSync('./chain.crt'),
}, (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
// Текст подобран исключительно в образовательных целях
res.end('Вы наш миллионный посетитель! Перейдите по ссылке, чтобы получить приз.')
})
server.listen(8443, () => console.log("Server listening on port 8443"))
Запустите (node ./server.mjs
) и откройте https://localhost:8443. Вы будете бессовестно заскамлены, зато защищены от атак MITM.
Вот и получился https-сервер на коленке. В следующей статье посмотрим, что он делает втайне от нас: разберём подробнее рукопожатие и формат сообщений. Ссылка появится, как только вторая часть выйдет.
Автор:
jabuj