Ресурсы: техническое описание TLS, LaTeX - в картинки (img), криптографическая библиотека Arduino, шифр "Кузнечик" на ассемблере AMD64/AVX и ARM64
Метаинформация о TLS-соединении. TLS-клиенты часто могут быть классифицированы (с точностью до типа и, реже, версии) по начальному сообщению TLS-сессии. Это позволяет при пассивном прослушивании узнавать трафик конкретных программ-клиентов (и не только, но сейчас речь только про клиентов и начало соединения).
Дело в том, что начальное сообщение, отправляемое клиентом, – ClientHello, – содержит много параметров и, соответственно, имеет структуру, которой хватает для эффективного построения отпечатков. Например, весьма подробное представление о том, как такой классификатор действует, можно составить, если внимательно посмотреть на выдачу моего тестового сервера TLS 1.3. Посмотреть можно непосредственно веб-браузером. При успешном соединении сервер вернёт страницу, где в подробностях показано то самое CLientHello, которое сервер получил от клиента. Вы можете подключиться браузером, обновить страницу несколько раз и увидеть, какие блоки данных не изменяются и, поэтому, служат основой для построения сигнатур.
Посмотрим на ситуацию чуть подробнее (рассматриваем случай TLS поверх TCP). Основу для построения сигнатур (отпечатков) может составлять последовательность байтов, которые представляют структуру TLS-сообщений. То есть, сообщения строятся из некоторого набора полей, а этим полям обязательно соответствуют конкретные заголовки.
Прежде всего, структура базового уровня: для TLS это будут TLS-записи, которые имеют свой простой заголовок фиксированного формата. Этот заголовок содержит обозначение типа и обозначение версии, а также – запись длины. С первыми параметрами (тип, версия) – всё очевидно, однако они, сами по себе, не дают нужной избирательности. Но уже сопоставление длины с соответствующей частью потока – даёт неплохой дополнительный признак: можно проверить, что заданное количество байтов укладывается в общий состав разбираемого потока.
Эта же идея важна и для разбора самого сообщения ClientHello, где она позволяет анализировать структуру следующего уровня, так как ClientHello вложено в TLS-запись. А состоит идея в том, что значения байтов, кодирующих предполагаемую длину, интерпретируются как смещение; полученное значение прибавляется к текущему адресу (в предварительно собранном потоке байтов) и значения байтов по вычисленному адресу сравниваются с ожидаемыми значениями, которые там оказались бы, если это действительно анализируется ClientHello. И если TLS-записи тут не добавляют много нового (они просто следуют одна за одной), то в ClientHello появляются вложенные поля данных (расширения) и более богатая структура, которая, тем не менее, устроена точно так же: значения байтов, интерпретируемые как целое число, задающее длину, должны строго сходиться, когда рассматриваются в виде цепочки. Заметьте, что до разбора самих значений полей данных дело ещё даже не дошло. Это важная особенность TLS: данный протокол не обладает скрытностью. Скрытный протокол выглядел бы случайным набором байтов со случайными же значениями. TLS, – особенно, на начальном этапе соединения, – выглядит как связанная строгими параметрами длины и типов байтовая структура (так, впрочем, и должно быть).
Для машины, разбирающей трафик, все эти записи “структур в байты” представляют собой наборы битов. Так что грамотно построенная ASIC-система может сопоставлять шаблоны с имеющимся потоком чрезвычайно быстро (ASIC – это аппаратная, интегральная схема, построенная под конкретную задачу; например, ASIC-и давно используются в производительных маршрутизаторах, потому что никакой универсальный процессор за ASIC-системой не угонится).
Итак, вернёмся к ClientHello. Помимо верхнеуровневой структуры из вложенных блоков, есть и содержание этих блоков. Например, сообщение ClientHello, в самом начале, после указания версии, полей Random и SessionID, содержит перечень шифронаборов, которые поддерживает клиент. (Это всё хорошо видно в выдаче тестового сервера.) Сокращённый пример: 0x1301, 0x1303, 0x1302, 0xC02B, 0xC02F… На уровне потока данных это всё просто последовательные значения байтов, по два байта на каждый идентификатор шифронабора (понятно, что данному блоку предшествуют байты с записью длины, что добавляет “узнаваемости”).
Так, для конкретной версии браузера перечень шифронаборов – узнаваем. В принципе, ничто не мешает клиенту менять состав и порядок пар байтов, обозначающих шифронаборы, но, например, Firefox передаёт фиксированный набор. Понятно, что строгая последовательность из 36 байтов – это уже достаточный идентифицирующий признак. (36 байтов получается так: 17 идентификаторов шифронаборов от Firefox, по два байта каждый, плюс два байта на запись длины.) Чуть более подробный пример, начальная часть ClientHello TLS 1.3:
Type (тип сообщения, 0x01, один байт - подходит для сигнатуры); Len (длина сообщения, три байта - подходит для сигнатуры: должно соответствовать общей структуре); Version (версия, 0x0303, два байта - подходит для сигнатуры); Random ([...], 32 "случайных" байта - не подходит для сигнатуры); SessionID_Len (длина данных Session_ID, один байт, для TLS 1.3 - подходит для сигнатуры); SessionID (обычно, для TLS 1.3 от браузера тут будет 32 байта - не подходит для сигнатуры). CS_Len (длина данных списка шифронаборов, два байта - подходит для сигнатуры); CS (список двухбайтовых идентификаторов шифронаборов - подходит для сигнатуры). [...]
При этом, скажем, браузер Chromium/Chrome добавляет к списку шифронаборов так называемые значения GREASE (для тестирования реализаций TLS), которые меняются от сеанса к сеансу. Но значения GRASE имеют строго определённый формат, так что их несложно распознать автоматом. Если взять такой клиент, как cURL, то состав ClientHello будет другим, в частности, другим будет список шифронаборов. Более того, по составу ClientHello нередко можно определить даже используемую библиотеку, реализующую TLS.
Естественно, не только шифронаборы подходят для построения сигнатуры. В ClientHello современных браузеров присутствует разнообразный набор расширений – это дописываемые в сообщение поля со своей структурой: они содержат заголовок с записью длины, дополнительные блоки внутри. Для TLS 1.3 расширения имеют определяющее значение. Внутри расширений передаются различные необходимые параметры, в частности, открытая часть протокола Диффи-Хеллмана и открытый ключ постквантовой криптосистемы X25519Kyber768. По составу и количеству расширений можно построить дополнительную сигнатуру. Так, уже наличие X25519Kyber768 едва ли не однозначно выдаёт современный веб-браузер. А ведь современный браузер ещё и укажет расширение ECH (Encrypted ClientHello). Firefox использует зафиксированный порядок расширений ClientHello, а браузер Chromium/Chrome – порядок следования расширений изменяет между сессиями. Однако даже в случае, когда порядок расширений разный, всё равно можно использовать перечень имеющихся расширений для построения сигнатуры.
Комментировать »
Предположим, имеется TLS-сертификат на веб-сайте, – то есть, выпущенный для доменного имени, – и соответствующий секретный ключ. Можно ли использовать этот секретный ключ для подписывания каких-то произвольных файлов, например, текстовых сообщений?
Да, это возможно. Технически, ключ вовсе не зафиксирован в роли “только для сайта”, так что ничего тут не мешает: достаточно взять какую-нибудь подходящую утилиту, которая может прочитать секретный ключ из файла (OpenSSL или другие варианты), и можно начать подписывать произвольные электронные документы. В процессе подписывания – открытый ключ и сертификат вообще не требуются. Естественно, каждое использование ключа (буквально – каждая операция) несёт с собой дополнительный риск утечки этого ключа. Однако, при штатной работе TLS, ключ “от сертификата” уже постоянно задействован именно для получения электронной подписи, так что тут нет места для каких-нибудь опасений относительно того, что ключ будет “использован не по назначению”. Однако для хитрых ошибок – место, всё же, появляется.
Так, если для подписи файлов применяется новая утилита, отличная от библиотеки, используемой TLS-реализацией веб-сервера, то возникает дополнительная вероятность утечки: либо непосредственно через эту утилиту, либо в процессе её вызова. Если этот дополнительный, относительно веб-сервера, процесс вдруг так устроен, что позволяет третьей стороне непосредственно подписывать произвольные данные, то даже без утечки ключа это ведёт к компрометации TLS-сервера: третья сторона может подменить сессию, перехватив соединение и подписав нужные параметры, которые подаст в виде файла на вход подписывающей утилиты. Так что осторожность – не помешает, а утечки ключей от одного TLS-сервера через другой, сконфигурированный с уязвимостью, – уже случались.
Проверка подписи для рассматриваемого способа использования проводится при помощи открытого ключа, опубликованного в TLS-сертификате. Тут возникает административный момент, связанный с политикой управления доверием, используемой приложением на проверяющей стороне. Это приложение может “верить в сертификат”, а может – “верить в сам ключ”.
В первом случае – допустимый контекст использования ключа задаёт сертификат и, так сказать, семантика применения этого сертификата. Например, приложение может в принципе принимать TLS-сертификаты (и ключи из них) только в контексте подписывания TLS-параметров при HTTPS-доступе к веб-серверу, и всё. Естественно, в сертификате предусмотрены дополнительные флаги и параметры, определяющие допустимые способы использования ключа. Их приложение тоже может учитывать каким-то своим способом (но, заметьте, использование ключа для подписи – в сертификате “от сайта” должно быть разрешено и так; см. выше).
Во втором случае, когда приложение “верит в сам ключ”, сертификат служит лишь контейнером для доставки этого ключа. “Верить в сам ключ” – означает, что доверие распространяется прежде всего на конкретное значение ключа, но при этом сам сертификат из рассмотрения может и не исключаться. Например, это обычная практика для корневых ключей в TLS для веб-сайтов и браузеров.
Впрочем, “побочное” использование ключей “от сайтов” для подписывания может подразумевать и то, что открытый ключ тоже используется вообще без учёта сертификата. Технически, опять же, ничего этому не мешает.
Комментировать »
Если (вдруг) появился универсальный “квантовый компьютер” нужной мощности, то как именно будут выглядеть технические шаги по раскрытию ранее записанного TLS-трафика? Под “квантовым компьютером” здесь подразумевается аппарат, выполняющий алгоритм Шора. А в роли взламываемой криптосистемы выступает алгоритм Диффи-Хеллмана на эллиптической кривой (ECDH – типовой алгоритм для современного состояния TLS). Шаги потребуются следующие.
1.
В записи трафика присутствуют начальные TLS-сообщения, обеспечивающие распределение общего секрета. Если таких сообщений нет, то раскрыть трафик рассматриваемым способом – не выйдет. Выделить начальные сообщения нетрудно: они начинаются с байтового префикса с зафиксированным значением и имеют строгую структуру. В трафике всегда остаются ключи, согласованные сторонами по протоколу Диффи-Хеллмана (равно как и с использованием RSA или другого метода инкапсуляции). Я писал об этом ранее. Заметьте, что записываются и ключи, согласованные с использованием современных криптосистем, обладающих постквантовой стойкостью. Просто, считается, что их сложнее восстановить даже с использованием гипотетического квантового компьютера.
Из начального сообщения клиента выделяется блок с открытой частью клиентского обмена ECDH. Например, если используется ECDH на кривой P-256, то в специальном и легко обнаруживаемом блоке данных будет содержаться пара координат точки P на кривой P-256, представляющей собой открытую часть обмена ECDH. Это 64 байта (плюс дополнительные байты с указанием на используемый формат), то есть, два блока по 32 байта: координаты X и Y.
2.
Клиентская часть ECDH представляет собой результат умножения базовой точки на секретный скаляр – значение скаляра и есть секретная часть ECDH клиента: это натуральное число n. Открытая часть ECDH, точка P, это результат [n]G, где G – точка-генератор, заданная в параметрах криптосистемы.
Полученные координаты точки P “загружаются” в квантовый компьютер, который уже должен быть настроен на параметры соответствующей криптосистемы, то есть, содержать схему, реализующую алгоритм Шора (нахождения периода функции) для подгруппы кривой P-256, образованной точкой G. Как именно загружаются значения в квантовый компьютер – пока что не известно, поскольку никаких практических квантовых компьютеров создано пока что не было (ну или, если хотите, не было опубликовано их содержательных описаний). Вероятно, у этого компьютера должен быть некоторый классический интерфейс, воспринимающий значения байтов координат точек, и переходные схемы, которые преобразуют эти значения в коммутацию и состояния квантовых “гейтов/триггеров” и кубитов. Неизвестно даже, сколько этих “кубитов и гейтов” будет нужно: несколько миллионов или несколько триллионов. Что уж там говорить про схемы коммутации.
(Заметьте, впрочем, что так как задействованный математический аппарат квантовой механики манипулирует комплексными числами – или, что эквивалентно, синусами и косинусами, – то все открытые тексты и решения можно уместить в пространстве состояний одного “непрерывного” кубита. Континуум, свернувшийся окружностью за привычным синусом, позволяет это легко проделать. Ещё и места останется, как минимум, в два раза больше, чем займут все решения всех проблем со всеми возможными шифрами в степени всех ключей. Это, впрочем, другая история.)
После того, как квантовый компьютер настроен, запускается итерация квантовых вычислений. Если итерация успешна, то результат позволяет быстро вычислить n на классическом компьютере. Использование классического компьютера тут означает, что у компьютера квантового, – а он, даже если создан, то бесполезен без классического, – есть и другой интерфейс, который выводит итоговое измеренное состояние квантовых схем в виде набора байтов, интерпретируемого как целое число. В случае с ECDH это будет пара чисел, по которым классический компьютер быстро вычислит секретный клиентский параметр n.
Аналогичная схема сработает и для открытого параметра ECDH, который отправил сервер. Для раскрытия ключей защиты трафика – достаточно знать один из секретов: клиентский или серверный.
3.
Общий секрет в ECDH образуется путём умножения точки-генератора на произведение секретов сторон. В рассмотренном только что случае, достаточно умножить открытый параметр DH сервера Q на секрет клиента, вот так: [n]Q (именно эту операцию проделывает клиент в ходе штатного процесса установления TLS-соединения). Секрет клиента, как мы только что выяснили, был раскрыт при помощи квантового компьютера. Результат умножения даёт точку на кривой, координата X которой используется в качестве задающего значения для генерирования секретных симметричных сеансовых ключей защиты трафика TLS. Функция, генерирующая сеансовые ключи, известна. Так как защищённые записи содержат коды аутентификации, сторона, взломавшая секрет ECDH, может тут же легко проверить, что найденный секрет соответствует записанному трафику. Симметричные ключи позволяют расшифровать трафик сеанса, используя шифр, номер которого тоже записан в начальных сообщениях: эти сообщения как раз и нужны для того, чтобы, кроме прочего, стороны согласовали используемый шифр.
*
Несомненно, прогресс в разработке систем “квантовых вычислений” есть. Однако вот уже несколько лет существенная часть прогресса сосредоточена на генерировании хайпа, поэтому даже до минимально практических схем не только далеко, но даже и не ясно, возможны ли такие схемы в принципе. В описанной выше схеме квантовый компьютер используется в роли “оракула”. Так же будет происходить и на практике: в рассматриваемой схеме для работы квантового компьютера не нужны какие-то дополнительные байты TLS-трафика, какие-то шифры или, условно, “части ключей”. С тем же успехом секретный параметр Диффи-Хеллмана можно просто угадать: запись трафика позволяет однозначно определить правильность угаданного значения.
Комментировать »
В продолжение предыдущей записки, про имена и TLS на веб-серверах, – хороший практический пример. Веб-сервер Nginx использует, обычно, OpenSSL в качестве библиотеки, реализующей TLS. Из-за особенностей кода и библиотечных вызовов, данный веб-сервер не может применять ограничения по версиям TLS для виртуальных хостов, сконфигурированных по SNI в блоках server. Это буквально происходит потому, что не согласованы области видимости имён и статусы TLS-соединения. Другими словами: для того, чтобы начать сопоставлять описания TLS-конфигураций из блоков server, Nginx должен знать имя хоста, по которому выбирается блок. Данное имя передаётся в ClientHello (SNI). Однако к моменту, когда Nginx это имя получает через соответствующий (обратный) вызов функции, в OpenSSL уже создан контекст с поддерживаемой версией TLS – ведь эта версия и выбирается по ClientHello.
Что это означает? Например, означает, что с именами в TLS нужно тщательно всё проверить. Так, если TLS-клиент (браузер) приходит с указанием поддерживаемой версии TLS 1.3 и такая версия указана для сервера по умолчанию, то отключить эту версию директивами ssl_protocol для прочих виртуальных хостов не выйдет – будет согласовываться TLS 1.3, а если согласовать 1.3 не получится, то возникнет ошибка. При этом сама директива ssl_protocol допускается в блоке server, однако в документации её низкая избирательность и, таким образом, относительная бесполезность, тоже отражены: указывать ssl_protocol нужно только для сервера по умолчанию (default/default_server).
Предположим, что для сервера Nginx указана полезная опция ssl_reject_handshake в блоке виртуального хоста по умолчанию (default_server). Это предотвращает успешные TLS-подключения с неверными именами. Соответственно, если в такой конфигурации попытаться указать для прочих виртуальных хостов какие-то иные наборы допустимых версий TLS в ssl_protocol, то применяться они не будут: сервер всегда раньше упрётся в default_server, хоть для этого варианта и прямо указана отмена TLS-хендшейка, не указано никаких уникальных имён хостов и, предположим, даже, – как обычно, – не указана директива ssl_protocol, которая будет инициализирована значениями по умолчанию. Это, конечно, выглядит не слишком логично, но происходит потому, что данный веб-сервер и не пытается обрабатывать имена из контекста TLS-сессии, “выгружая” задачу в сторону TLS-библиотеки (OpenSSL – см. выше).
Вернёмся к конкретной версии TLS – к 1.3. Если при наличии default_server с ssl_reject_handshake в конфигурации присутствуют виртуальные хосты, для которых поддержка 1.3 невозможна (например, с ГОСТ-TLS без совместимых шифронаборов), то, даже если в блоке server для этих имён прямо указаны только версии ниже 1.3, всё равно возникнет довольно занимательная проблема: некоторые клиенты-браузеры смогут подключаться к серверу по проблемному имени хоста, а некоторые – нет. Потому что с клиентом, который заявляет в ClientHello поддержку 1.3, сервер всё равно будет пытаться установить соединение 1.3 и, определив недоступность этой версии, возвратит ошибку (Alert в терминах TLS). А с клиентом, который 1.3 не заявляет, соединение будет успешно и без всяких проблем устанавливаться по версии 1.2 (например). Всё по причине игнорирования блока ssl_protocol веб-сервером. Решением является отключение 1.3 для сервера по умолчанию, то есть – для всех имён хостов сразу.
Комментировать »
В TLS для веба важны имена. Например, при подключении по TLS для HTTPS серверу нужно отправить клиенту TLS-сертификат сервера. Если серверных сертификатов несколько, то сервер должен как-то выбрать подходящий по имени. Тут речь не про промежуточные сертификаты, которые добавляются в набор (bundle), а именно про оконечные сертификаты. Если TLS-cертификат не соответствует имени, которое ожидает клиент-браузер, то сессия не будет корректной на стороне клиента. Данный момент, вместе с желанием держать много виртуальных хостов на одном IP-адресе и под одним веб-сервером, является постоянным источником ошибок администрирования.
Посмотрим, что вообще происходит в ходе обработки HTTPS-сеанса и как сервер может определить соответствие имён. В начале TLS-сессии клиент (браузер) обычно передаёт имя сервера в специальном блоке данных – SNI (Server Name Indication). Сервер видит это имя и может использовать его для поиска нужного сертификата и нужного виртуального хоста. Однако клиент определяет имя из своих собственных соображений.
Типовой вариант: в контекст TLS-сессии имя узла SNI приходит из запроса пользователя через DNS. Почему “через DNS”? Потому что браузер должен был найти IP-адрес, к которому он подключается по TCP. При прочих равных, браузер получил IP-адрес из DNS по имени хоста из URL, заданного пользователем. Это действительно важный момент, который, почему-то, инженеры DevOps регулярно выкидывают из области рассмотрения, посчитав “элементарным”. Однако, во-первых, если пользователь ввёл имя, которое не удалось “разрезолвить” в IP-адрес, то и штатного TLS-соединения не случится, хоть бы такое имя и было указано в сертификате (такое вполне возможно). Это, предположим, “элементарная часть”, которая, тем не менее, никак не отменяет возможности указания в SNI имени без DNS-адреса, то есть, без A-, AAAA-записей. Во-вторых, кто угодно может настроить какое угодно допустимое DNS-имя так, чтобы оно показывало на заданный IP-адрес сервера, а потом отправить ссылки с этими именем хоста ничего не подозревающим пользователям. В этом случае, пользователь, штатно набрав это самое “какое угодно” имя хоста, успешно направит браузер на заданный сервер, но браузер обратится туда с “неожиданным” именем в SNI. Если это имя вдруг совпадёт с указанным в предоставленном по умолчанию валидном TLS-сертификате с успешным подтверждением, то браузер даже не выдаст предупреждения, а в сертификате может быть очень много разных имён. (Но, во многих случаях, можно смело считать валидный серверный TLS-сертификат с верным именем достаточной мерой защиты от “ложных” хостов для HTTPS. Особенно, если веб-сервер основного имени хоста доступен по выделенному IP-адресу.)
Основной вывод предыдущего абзаца такой: сервер ориентируется на SNI, но нельзя считать, что SNI, присланное клиентом, как-то привязано к серверным настройкам, соответствует DNS, “защищено сертификатом” и вообще укладывается в формат и ожидания, отражённые в конфигурационном файле веб-сервера. И SNI вообще может отсутствовать. (Кроме того, тут не рассматривается случай, когда данные SNI скрываются от третьей стороны – метод ECH и пр.) На стороне веб-сервера бывает возможность отклонить TLS-соединение, использующее “неожиданное” имя хоста.
На начальном этапе соединения, до получения HTTP-запроса, сервер видит только имя из сеанса TLS. Но это не означает, что и сами HTTP-запросы будут адресованы этому же имени хоста – в HTTP-запрос тоже можно вписать что угодно, TLS никак семантику HTTP не фиксирует. Конечно, ситуация не является типичной для браузеров, но она вполне возможна. Это тоже нужно учитывать при обработке разных внутренних переменных на серверной стороне: контекст TLS, контекст HTTP – разные контексты.
Комментировать »
Как ни странно, но продолжает развиваться тестовый сервер TLS 1.3 – добавил поддержку ещё пары гибридных криптосистем с постквантовой стойкостью: P256Kyber768 и P384Kyber768. До этого сервер поддерживал только X25519Kyber768, которая есть в Chrome/Chromium (и на веб-серверах Google, например). Два новых варианта – совсем редкие.
Удивительно, но P384Kyber768, тем не менее, заявлена в свежих версиях браузера Microsoft Edge под Windows, где включение данной криптосистемы спрятано за флаг (chrome://flags). Да, Edge построен на базе Chrome/Chromium, однако в TLS-стеке там есть дополнительная постквантовая криптосистема. При этом, у P384Kyber768 пока нет индекса в реестре IANA, а у ещё более редкой, в плане поддержки, P256Kyber768 (то же самое, но кривая P-256) – индекс (0x639A) уже имеется.
Вообще, гибридные P256Kyber768 и P384Kyber768 устроены аналогично X25519Kyber768: тут часть Kyber768 вообще совпадает, а в качестве присоединяемого протокола Диффи-Хеллмана (DH) выступают, вместо X25519, ECDH/P-256 и ECDH/P-384.
На сервере я попутно исправил старую ошибку с обработкой точек кривой P-521, из-за которой поддержка DH на этой кривой была давно выключена (это не связано с постквантовыми криптосистемами). Теперь она снова включена – такую версию ECDH использует Firefox.
Изменена обработка TLS-записей, составляющих ответ. Теперь веб-страница с сервера, в большинстве случаев, приходит в нескольких TLS-записях. Это довольно занятный момент: из-за использования постквантовых криптосистем, HTML-ответ, в котором перечисляются ключи и сообщения, теперь может быть довольно большим. Представьте, что клиент приходит с заявленной поддержкой X25519Kyber768 и P384Kyber768, при этом сразу присылает ключи в X25519Kyber768 (1216 байтов). Однако сервер предлагает перейти к P384Kyber768, отправляя HelloRetryRequest, после чего клиент присылает уже ключи P384Kyber768 (ещё 1249 байтов). Всё это должно отразиться в результатах на странице. Чтобы, так сказать, выровнять результаты с постквантовыми криптосистемами и без них – я уменьшил максимальную используемую длину TLS-записи.
Сомневаюсь, что можно найти типовой популярный браузер с большим разнообразием поддерживаемых постквантовых “гибридов”, но вот рабочую сборку curl в контейнере – найти нетрудно (там же, кстати, есть и пара специальных сборок браузеров).
Комментировать »
Кстати, интересуются примером того, как, более или менее конкретно, может что-то сломать сообщение TLS ClientHello от браузера, если в этом сообщении добавлены параметры Kyber768.
Вообще, нетрудно представить следующую ситуацию. Предположим, есть некоторый диспетчер соединений, работающий в паре с пакетным фильтром. Этот фильтр анализирует пакет данных (то есть, последовательность байтов, имеющую конкретную длину) по заданным правилам на уровне значений байтов внутри пакета и расстояний между байтами с заданными значениями. Например, определяет, что пакет представляет собой начало TLS-соединения с некоторыми свойствами и – перенаправляет этот пакет, как и всю сессию, на какой-то заданный входной узел. Этот узел – уже может быть элементом балансировщика трафика, или входить в состав какого-то механизма DPI, предназначенного для борьбы с возможными атаками, или играть какую-то ещё роль. Базовые правила для фильтра позволяют узнавать заявленные версии TLS (это значения полей в сообщении), определять предлагаемые клиентом криптосистемы – всё по значениям байтов на определённых позициях. Скажем, таким способом можно детектировать начало TLS-сеанса с поддержкой ГОСТ-TLS, со стороны клиента, чтобы на стороне сервиса перенаправить соответствующие сессии на сервер, настроенный конкретно для ГОСТ.
Реализация фильтра не только не проводит разбора сессии, но даже не анализирует пакет с ClientHello как содержащий TLS-сообщение ClientHello – фильтр работает только с байтами и их примитивными индексами. Более того, так как фильтр пакетный, то правила индексирования в нём написаны от значения длины пакета, которое взято из каких-то “максимально обобщённых” соображений. Эти соображения, определяющие длину, не учитывают формата ClientHello и возможного увеличения размеров этого сообщения сразу на несколько сотен байтов. В результате – ожидаемые индексы сдвигаются за допустимые границы, даже в следующий пакет, фильтр перестаёт срабатывать так, как срабатывал раньше, но начинает новые сообщения от браузера Chrome считать дефектными, сбрасывая соответствующее TCP-соединение.
Комментировать »
Снова пишут про то, что в TLS браузера Chrome длинное ClientHello с ключом Kyber768 ломает обработчики TLS-трафика на каких-то “специальных” веб-серверах и на промежуточных фильтрах. Между тем, чтобы разобраться, как там и что происходит, достаточно посмотреть на конкретное ClientHello, но в некоторых деталях. В качестве примера я взял сообщение от браузера Chrome 124 в том виде, как оно отображается на тестовом сервере TLS. Чтобы просмотр дампа не был скучным, листинг перебивается техническими пояснениями.
Итак, ClientHello TLS 1.3 с постквантовой криптосистемой Kyber768 внутри. CH здесь уже извлечено из TLS-записей и разобранно на элементы: то есть, здесь не учитываются и не упоминаются дополнительные байты, которые расходуются внутри структуры CH на представление полей – идентификаторы, длина записи; обычно – это несколько дополнительных байтов на один элемент структуры. Если бы разбирать и эти байты, то описание излишне усложнилось бы (подробности – можно прочитать в техническом описании TLS).
Version: 0x0303 Cipher suites: 0xBABA,0x1301,0x1302,0x1303,0xC02B,0xC02F,0xC02C,0xC030 0xCCA9,0xCCA8,0xC013,0xC014,0x009C,0x009D,0x002F,0x0035
Начальная часть сообщения ClientHello (далее – CH) содержит “исторический” номер версии (здесь указано 0x0303) и перечень шифронаборов. Полезных данных: два байта – номер версии, 32 байта (16*2) – идентификаторы шифронаборов.
Client Random: 85:32:22:08:24:07:B2:98:F3:23:08:4D:A5:FB:E4:59:41:93:13:88:AA:58:53:4E:B4:EF:E7:DA:E8:78:FE:71 Client SessionID: C0:39:BC:CE:BB:60:5F:76:D8:75:F9:84:69:E9:9F:C2:AE:7B:8F:D5:9C:E8:01:51:71:9D:21:D2:13:26:DB:5A
Случайное значение от клиента (Client Random). А также клиентский “идентификатор сессии” – в TLS 1.3 этот идентификатор играет роль простого сигнала и ни на какую сессию не указывает.
Здесь всего 64 байта данных, по 32 на каждое поле.
Далее идёт важная часть, которая, собственно, определяет значимые параметры TLS-соединения – набор расширений CH.
Extensions: Type: 14906 (0x3A3A); "_undef_ (GREASE)" Type: 35 (0x0023); /non-TLS-1.3/ Type: 23 (0x0017); /non-TLS-1.3/ Type: 16 (0x0010); "application_layer_protocol_negotiation" 00:0C:02:68:32:08:68:74:74:70:2F:31:2E:31 Type: 65281 (0xFF01); /non-TLS-1.3/ 00
Здесь 0x3A3A (тип 14906, первое расширение) – это пример того, что называется GREASE, а именно – специальных, заведомо неподдерживаемых, значений и идентификаторов, которые Chrome дописывает в TLS-сообщения с целью (как заявлено) проверки корректной работы парсеров и прочего программного обеспечения, задействованного в обработке TLS. Такие обработчики, согласно спецификации, должны успешно игнорировать неизвестные расширения и параметры (если контекст позволяет, конечно). Так что, вообще говоря, добавление разных там новых “постквантовых ключей” не является чем-то особенным – всякие фиктивные элементы писались в браузерные сообщения и раньше, именно для того, чтобы обновления не оказывались полной неожиданностью (ниже нам встретятся и другие GREASE). Другие расширения из этого фрагмента, с типами 35 (session_ticket), 23 (extended_master_secret), 16 (ALPN) и 65281 можно не рассматривать: они небольшие по объёму (например, extended_master_secret – это всего лишь сигнал, вообще не содержащий байтов данных), часть из них не поддерживается в TLS 1.3. Поэтому – переходим дальше.
Как говорится, а вот и он – постквантовый элемент. Клиентский открытый ключ Kyber768 содержится уже в следующем расширении – key_share. Оцените огромный размер данных, которые тут приводятся без сокращений.
Type: 51 (0x0033); "key_share"
_undef_ (GREASE) (0xAAAA)
Unsupported group: 0xAAAA (_undef_ (GREASE))
X25519Kyber768Draft00 (0x6399)
Hybrid: X25519 + Kyber
Len (octets, dec.): 1216
Key share data:
X25519
x = 0xF2C744A07B3BC36F6C427620351A009D29BB36E6EF259A177A3881ECEB22100D
Kyber768 client public key:
Vpoly_1 (packed):
6CA39FB332275395989A56958E6B61A7B3B447D6A2C9062837E39695B44E4A5B
36C9A7B06F4A912ED6ABC2E3BD2FD8584BE42F5822A3A54A4CC14812F3D87A05
5371F2E1222817A63FAA531CD7691DE59694F9B4ACD3B8CD638A44B9B566C094
3A95B7DCB186CC840C53A90041FB15FFBB214E3BCE8AB0C3D278897A61992601
BB88427608A8AEC6385C0C6319A56B8B47CA240F3A134DD6AF4B56A6333158A9
D391B495A76EA0C5F67C49457042AFFB508766414C88A8EA23B1328723FC9199
8BA78414527F2DC234C0213CF5DAB976910EA3DC641C8B818FF73F4CEB53C929
C03F61CFD8A04CE88A3EB16CCAE02C9541438A16D4A7FE8129F7F04C0C0A9E14
60ACA4049F84C2C4E5DB1B4185CF0FE8C305333CBC7B3970A27E42C23F47F003
E3815FDE20830B912501E388BEB9B14399ADD2EA174E3C040810917E555084E1
5B75B9599914523799B6885721D08A46741501EBACA747C905B7AA31E3D3137C
718B50A908DC111E49161448B102D3F0411EA70D9F91CFF6F50D5D54B9AB7B11
Vpoly_2 (packed):
1356BAA03CAD6E2730C334C24D492E6A11088F7B036A7ACBEF26407C1A9DCDFA
473D677C3447915AE28EB094326093668FC27DCB85086219A1A0E6A6FC367CE0
9A4396EA75FAFA1CD336311DE505BE5075A36C436B58673CF6387AF45BDBD523
C082864EB6BD20E6A587C99976FA04C9548FB9731BC5B80D483984D8E84D89A3
8A0D210D58C22C9DA0316D9214B45B46C7353228C67A62F101DD79267C071CEE
9C1FA2DA976294AD6031C245397BDFB11A3AF012DF154B6D126439F579F85C38
77AB844586159ECCAD0D468A3D94A5DABA43B26722B4C24FEFDC19FC62355341
29B1FBABC7D242DDEB59DB00C564546DBD384653CCB55436BCCA4B7C4BA585E3
50B078A48B8F02C9BA79154DF463E42538DB7616AC04C9C1284F0E2918009CAE
49EB18708A2BD322A3A613CCCF43B3CA288780546FEBF03B6FC3394816359406
5642DC4930B4B36D5B5122A90871006975496515650608E632414B5054019BB5
CB280DF34DA9087AB2FA2ADB94588AB2A8C8D3CA3DAB7248357001D0C175269C
Vpoly_3 (packed):
AAF30D9EA1ADEF802799581653A0A0D4D692EB2B644F6C84059388947838138A
1D24464054A9A3E9385FD3E4A6C0C56D09D80F984705B2E4B4244A4D67C3CD58
7739688170695AC9AA4C354A349AB04ACBD4753E6B519468EB900A290C3AD255
A004307B1572C0072946B73639F897D434086C515F6BBA53BB10B6A3D4B4AC86
58E4A4C3EE452C3D7442536756D4F7041775897C2240C4B437CC97CC5E4270FD
986DEE9842831B1F233B543F724A9E5B0446BB22CF6C8AC278CD4E275482F61B
A3461582291443967ED0AA78723A2EBE448AC24726D8EA8F5264BC45A4A7D8B4
C04EFC7A7271AA361B454EE43365644E5DA9A5F8A5A838D4C93EB6970716B98D
5617F4E4829E93A816432C9B7621ECF2CB5A75BDD4446F58BC57F1A36215467F
1722A31FFA2432574F028848A92A0FC5C48C4F391FDF2214429454A6015DD9E6
496A3B1D5F9768C5326AB2997E038CA5E7E34347D5C5C8E7AE00A9CB48FA4A43
54768F3C586430B8C4E8C3CAE13EE0265910A0B01FA037CAE333B13B63601397
Rho (bytes):
44D2E20D0D57AA501ED3DE916106FF5DA95E4BE2AD4335034F2C757352305980
X25519 (0x001D)
x = 0xFB6C61B9A2E5323674FD2C49FA02DF5E1335F52EFBF984C9FC2401F01B1F7C5E
В TLS 1.3 стороны уже на самом первом этапе вычисляют общие секретные ключи, чтобы перейти на защищённый обмен данными. Для этого свою часть открытых ключей (или открытых значений протокола Диффи-Хеллмана, что здесь эквивалентно) должен прислать клиент. Клиент присылает эти ключи в расширении key_share.
В данном случае браузер Chrome прислал нам значение X25519 (внизу) и гибридный набор из X25519 и Kyber768 – то есть, X25519Kyber768draft0 (идентификатор 0x6399 – в начале). Важно не запутаться: криптосистема с постквантовой стойкостью тут одна, она называется Kyber768 (“на научном языке” это KEM – Key Encapsulation Mechanism). Однако в TLS эта криптосистема внедряется в гибридном варианте, то есть, на прицепе к классической, хорошо отработанной, криптосистеме X25519, которая есть разновидность реализации протокола Диффи-Хеллмана. Данный гибрид всё ещё считается “черновым вариантом” – draft. Поэтому полное название X25519Kyber768draft0.
Если приглядеться, то в этом же расширении есть ещё один, не столь массивный, блок, названный X25519. Это другое, отдельное открытое значение X25519, которое может быть использовано сервером, если сервер не поддерживает X25519Kyber768draft0. К входящему в гибридный вариант значению X25519 свободный вариант отношения не имеет. Это та же криптосистема X25519, в гибриде она работает абсолютно и точно так же, как и без “гибридизации”, но название отличается, как отличаются и значения ключей (обозначены “x”).
Постквантовая часть ключей указана в блоке “Kyber768 client public key”. Ключ Kyber768 состоит из четырёх обособленных значений: это три полинома (Vpoly_N) и параметр Rho. И только лишь ключ Kyber768 занимает уже 1216 байтов. X25519 добавляет лишь 32 байта (в составе гибрида).
Другие элементы расширения key_share, которые тоже отмечены GREASE, это, опять, фиктивные значения, как и в следующем расширении – supported_groups.
Type: 10 (0x000A); "supported_groups" 0xAAAA (_undef_ (GREASE)) 0x6399 (X25519Kyber768Draft00) 0x001D (X25519) 0x0017 (Secp256r1) 0x0018 (Secp384r1)
В этом расширении – поддерживаемые клиентом схемы криптосистем. Расширение называется “Поддерживаемые группы” (supported_groups). “Группы” здесь – это технический термин, обозначающий математическую структуру (группу), которая применяется в той или иной криптосистеме (кстати, можно сказать, что “схема”, с математической точки зрения, даже более технический термин, который вполне можно так применить, но, конечно, это лишь случайное совпадение).
Данное расширение всего лишь перечисляет идентификаторы параметров, которые клиент готов использовать. Часть ключей – уже передана в составе key_share. Поэтому тут и присутствуют X25519Kyber768Draft00 и X25519. Однако сервер может не поддерживать ни X25519Kyber768Draft00, ни X25519. В таком случае предстоит выбрать что-то из оставшихся вариантов: Secp256r1 и Secp384r1, под которыми подразумевается протокол Диффи-Хеллмана (DH) на кривых P-256 и P-384. А GREASE – выбрать нельзя. Для перехода к другой криптосистеме сервер, в ответ на CH, отправляет сигнальное сообщение HRR (HelloRetryRequest), указывая в расширениях к нему предпочитаемую криптосистему. Пересогласование криптосистемы для DH поддерживается на тестовом сервере tls13.1d.pw – так что эффект нетрудно увидеть, понажимав Ctrl+F5 (Refresh) в браузере: при пересогласовании клиент пришлёт новое CH, уже с другим составом key_share.
Возникает резонный вопрос: поддержка Kyber768 на серверной стороне не очень-то распространена, нужно ли пересогласование при каждом CH от Chrome с Kyber768? Нет, в типовом случае, серверу не нужно ничего пересогласовывать – сервер может проигнорировать гибридный постквантовый набор и сразу использовать X25519 (поддержка есть практически повсеместно), потому что ключ этой криптосистемы указан в CH от Chrome (не гибридный, а отдельный). Если, конечно, сервер не поддерживает X25519, тогда пересогласовать криптосистему потребуется, но не из-за наличия Kyber768.
Следующее расширение – это фиктивное ECH (Encrypted Client Hello).
Type: 65037 (0xFE0D); "ECH /DRAFT/" Type: 0x00 (Outer) HKDF: 0x0001 (HKDF-SHA256) AEAD: 0x0001 (AES-128-GCM) Config id: 0x04 Encapsulated key (bytes): 08FC1E6C6A441874D38689375F9C906827516F2F306BBD8B2376569F99CF0069 Payload (dummy): ECADC2C291D6311FCD925306392EE88CD057D42178C0B60EF5B14814F2CF669E 05230A0F3D6AABADA0A3EB78D240E22BE2DE5893A50B3AEC4056B77B5CACF7C9 07F5E6EE853268C23595E22F59E9D190BE9ECCD05B76092CBA00FCCE1C072283 7CBE371C871817540D8E9429CB10F4CCA3089AB36154D7AE2578FF5D12884F3D 437AB7A97E0FDA19DCC4B423A4DFD2CF
ECH служит для доступа к скрытым сервисам. Базовое применение: сокрытие имени узла, с которым устанавливается TLS-соединение. Chrome включает фиктивное расширение в CH даже для узлов, которые ECH не объявляли. Это метод придания единообразия TLS-трафику. Расширение ECH устроено довольно сложно, содержит внутри дополнительную структуру и большое по объёму данных поле Payload, которое, в случае фиктивного расширения, имитирует полезные данные ECH (зашифрованное CH). ECH, в данном случае, добавляет ещё 182 байта.
Type: 00 (0x0000); "server_name" 00:00:0E:00:00:0B:74:6C:73:31:33:2E:31:64:2E:70:77 hostname: tls13.1d.pw
Расширение SNI, содержащее имя узла, с которым устанавливается TLS-соединение. Именно данные этого расширения и должно скрывать ECH (в данном случае – ECH не поддерживается). Обычно, это расширение не очень большое по объёму (но зависит от используемого имени).
Type: 45 (0x002D); "psk_key_exchange_modes" 01:01 Type: 17513 (0x4469); /non-TLS-1.3/ 00:03:02:68:32 Type: 43 (0x002B); "supported_versions" 0x8A8A (_undef_ (GREASE)) 0x0304 (TLS 1.3) 0x0303 (TLS 1.2) Type: 05 (0x0005); "status_request" 01:00:00:00:00 Type: 13 (0x000D); "signature_algorithms" 00:10:04:03:08:04:04:01:05:03:08:05:05:01:08:06 06:01 Type: 11 (0x000B); /non-TLS-1.3/ 01:00 Type: 27 (0x001B); /non-TLS-1.3/ 02:00:02 Type: 18 (0x0012); "signed_certificate_timestamp" Type: 10794 (0x2A2A); "_undef_ (GREASE)" 00
Из оставшихся расширений в контексте этой записки интерес представляет только supported_versions (тип 43): в TLS 1.3 именно это расширение содержит настоящий список поддерживаемых клиентом версий TLS. Chrome, помимо очередного GREASE, передаёт здесь идентификаторы версий 1.3 и 1.2. Наличие supported_versions и делает это CH сообщением версии TLS 1.3.
А заканчивается набор расширений всё тем же GREASE.
Итак, на примере конкретного ClientHello хорошо видно, что ключ Kyber768, действительно, занимает основную часть сообщения. Обычный вариант CH, без “постквантовой стойкости”, будет отличаться только отсутствием ключа X25519Kyber768Draft00, всё остальные поля – применяются точно так же.
Комментировать »
Пишут, что поддержка постквантовой криптосистемы X25519Kyber768 в Chrome свежей версии ломает TLS в каких-то серверных реализациях или на промежуточных узлах-фильтрах, а происходит это из-за того, что начальное сообщение ClientHello получается слишком длинным.
Вообще, X25519Kyber768 добавляет 1216 байтов, что, действительно, сравнимо с размером типичного ClientHello (без “постквантовых” данных, но с разными расширениями). Скажем, даже длинный FFDHE3072 (“мультипликативный” вариант Диффи-Хеллмана в TLS 1.3) использует только 384 байта. Chrome, надо сказать, FFDHE3072 не поддерживает, но зато дописывает фиктивное ECH-расширение (это не связано с постквантовой криптосистемой).
То есть, вовсе не трудно поверить, что в каких-то, – не самых качественных, – реализациях TLS, “постквантовое” ClientHello может читаться лишь частично, потому что оно не влезло в один ответ функции чтения TCP-сокета. Такое нередко приключается в системах инспекции трафика, разных “корпоративных” сетевых экранах (как бы, stateful), да и в прочих инструментах. С другой стороны, пытаться читать TLS-сообщения только по данным TCP-пакетов, игнорируя заголовки самих TLS-записей, в которых указана полная длина, – не самый лучший способ реализации TLS. Особенно, если речь про некоторый “продвинутый” механизм “обеспечения безопасности”, типа брандмауэра.
Комментировать »
Кстати, серверы Google (например) уже поддерживают X25519Kyber768 – см. скриншот ниже, – а это означает, что можно найти новые уязвимости, связанные с поддержкой этой криптосистемы.

Комментировать »
В свежих версиях веб-сервера Nginx есть полезная (и довольно экзотическая) опция для настройки TLS: ssl_reject_handshake (on | off), о которой, оказывается, мало кто знает. Опция позволяет управлять прерыванием процесса установления TLS-соединения (“хендшейка”) на стороне сервера. С помощью “ssl_reject_handshake on;” можно отклонить TLS-соединение, если в клиентском ClientHello указано имя хоста (поле SNI), которого нет на стороне сервера.
Почему это удобно. Штатная работа веб-сервера по HTTPS подразумевает, что при установлении TLS-соединения клиент передаёт имя хоста, соответствующее DNS-имени веб-узла; например, для https://test.ru/ это будет test.ru. Однако никто не запрещает клиенту обратиться с другим именем хоста в SNI. Более того, на тот же самый IP-адрес, который соответствует веб-узлу под именем test.ru, может быть направлено другое DNS-имя (другой домен). Веб-браузеры, обращающиеся по такому имени, будут указывать его в SNI, а сервер, при прочих равных, не сможет найти не только соответствующий “виртуальный хост” в конфигурации, но и подобрать корректный TLS-сертификат. Однако какой-то TLS-сертификат для установления TLS-соединения серверу нужен и, без дополнительных мер, это приводит к тому, что сервер отвечает с “первым попавшимся” (буквально) серверным TLS-сертификатом из сконфигурированных.
Если на сервере всего один виртуальный хост/веб-узел (или настроены отдельные IP-адреса для отдельных веб-узлов), то ничего нового клиент не узнаёт. Однако если виртуальных хостов много, то ответ с произвольным сертификатом приводит к нежелательным утечкам имён: клиент получит сертификат, в котором будет указано имя другого сервиса, доступного на этом же узле (или несколько имён). Администраторы и даже DevOps об этом постоянно забывают.
Сделать тут что-то корректное на уровне HTTP невозможно в принципе, так как TLS-соединение устанавливается раньше, а для каждого HTTP-ответа (403 и пр.) потребуется подходящий по имени доверенный сертификат. Да, возможны различные “костыли”, вроде использования заведомо несовпадающего сертификата-заглушки, но все они подразумевают некорректный ответ.
Указание “ssl_reject_handshake on;” для default_server (для имени хоста по умолчанию) позволяет побороть только что описанную проблему: Nginx будет прерывать именно TLS-соединение, если клиент обращается с неверным именем хоста в SNI – это, что называется, технически строгий способ. Браузер, в таком случае, выведет сообщение о недоступности узла, а не предупреждение о несовпадении имён, которое пользователи привыкли преодолевать при помощи кнопки “Всё равно продолжить”.
Комментировать »
Новый