Можно ли “смешать” TLS-сертификаты для IP-адресов и для хостнеймов? Например, если говорить о TLS для HTTPS, то тут используются и адреса, и имена: поиск сайта, обычно, происходит по имени хоста (по доменному имени), но само соединение устанавливается по IP-адресу. Соответственно, TLS-клиент, – пусть это будет браузер, – на момент отправки первого TLS-сообщения серверу знает доменное имя и IP-адрес, который сам браузер поставил в соответствие этому имени (в процессе обнаружения адреса использовалась DNS, это понятно). Обычно, чтобы признать сертификат валидным, браузер ожидает, что в TLS-сертификате указано имя, соответствующее ожидаемому имени хоста – это может быть одно из нескольких имён в сертификате, может быть результат “раскрытия” wildcard-имени (“со звёздочкой”).

Технически, в TLS-сертификате, вместе с именами хостов, можно указать и IP-адреса, форматом допускается. За примерами не нужно далеко ходить – сертификат на веб-сервере dns.google содержит и DNS-имена, и IP-адреса:

X509v3 Subject Alternative Name: 
 DNS:dns.google, DNS:dns.google.com, DNS:*.dns.google.com,
 DNS:8888.google, DNS:dns64.dns.google,
 IP Address:8.8.8.8, IP Address:8.8.4.4,
 IP Address:2001:4860:4860:0:0:0:0:8888,
 IP Address:2001:4860:4860:0:0:0:0:8844,
 IP Address:2001:4860:4860:0:0:0:0:6464,
 IP Address:2001:4860:4860:0:0:0:0:64

При этом dns.google показывает на те же IP-адреса, которые перечислены в сертификате. Это, конечно, не означает, что IP-адреса из сертификата должны быть связаны с именами в том же сертификате через DNS – просто, сертификат будет валиден и для любого из указанных IP-адресов отдельно (при совпадении подписей, конечно).

Однако в теории тот же браузер может потребовать, чтобы в сертификате и имя хоста совпадало, и IP-адрес, по которому установлено TCP-подключение. Двойная проверка.

Чем такая схема, если бы её реализовать, грозит? С одной стороны, схема неожиданным образом защищает от использования секретного ключа другим сервером, если таковой сервер использует другой IP-адрес. Подключение к подставному серверу можно реализовать подменой DNS, так что имя – совпадёт. Но не IP-адрес. Может ли атакующий, вооружённый секретным серверным ключом, соответствующим ключу в сертификате, подменить и IP-узел? То есть, сделать так, чтобы перехватывающий, подменный узел стал доступен для атакуемого клиента по тому же IP-адресу, который указан в сертификате? Как ни странно, не факт – владение секретным ключом от сертификата никак не помогает в решении сетевой задачи подмены IP-узлов. При этом, если в сертификате сверяется только имя хоста, то атака сработает уже и при подмене DNS. С другой стороны, тот, кто может подменить IP-узел и DNS, может попытаться выпустить сертификат для этих реквизитов. Однако такая подмена уже потребует атаки на системы УЦ, а не на обычного клиента веб-узла.

На одном IP-адресе может размещаться большое количество веб-узлов с разными именами. Но это не является препятствием для того, чтобы вписать для всех этих узлов один и тот же IP-адрес в сертификат. Сертификат может быть выпущен только для IP-адреса. Например, такие сертификаты обещает даже Let’s Encrypt. Но если обращение к узлу происходит в контексте, где нет DNS-имён, то можно использовать сертификат только с IP-адресом, и если бы был возможен контекст, в котором есть только DNS-имя, то сгодился бы сертификат только с именем, как сейчас. Так что тут проблемы нет. Тем более, если сертификаты начинают выпускаться всего на несколько дней.

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



Комментировать »

“Яндекс”, у которого недавно приключилась авария с полным обесточиванием одного из дата-центров, публикует разбор произошедшего. Пишут, что отключились сразу обе из имевшихся двух вводных линий, которые, как выясняется, шли от одной подстанции. Цитата:

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

Как-то потерян тот факт, что подключение к разным подстанциям (“теоретическое”), имеет ещё один важный аспект: источник – источником, он может быть и общий, но если точки подключения разные, то и разные независимые линии могут идти максимально обособленным образом. То есть, пути доставки будут защищены лучше. Опять же, в статье “Яндекса” по ссылке написано, что “опорная подстанция немного остаётся для нас «чёрным ящиком»”. Понятно, конечно, что для электрических линий реализовать такое возможно далеко не всегда, но тут-то речь даже про теоретическую ситуацию, которая, видимо, всё же сыграла на практике и общее подключение вылетело по двум линиям сразу.

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

Автономного питания, конечно, в дата-центре “Яндекса” тоже не хватило, потому что оно же не на случай полного отключения проектировалось. Цитата:

В 12:27 главный инженер обслуживающей организации связался с дата‑центром и сообщил, что на подстанции отключились обе линии 110 кВ, но причина пока неизвестна. А значит, у нас Проблема № 1: сразу две точки отказа по питанию с непонятным прогнозом, а дизель‑генераторы просто не рассчитаны на то, чтобы принять такую нагрузку.

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

Печально тут то, что во все эти “облачные сервисы”, работающие в дата-центрах с таким подходом, усиленно загоняют информационные системы, какие только можно и какие только нельзя. Не ровён час, окажется в подобном “облаке” и система управления всем прочим энергоснабжением. “Под контролем ИИ”, конечно. Времена меняются. Угроза ИИ крепнет.



Комментировать »

Кстати, вот пара сервисов, позволяющих просматривать информацию из российских логов Certificate Transparency через веб:

ct.tlscc.ru – экземпляр crt.sh, но с российскими логами (используется TLS-сертификат ТЦИ);
precert.ru – весьма удобный самостоятельный сервис, отличается от crt.sh веб-интерфейсом, форматом вывода и возможностями расширенного поиска.



Комментировать »

Certificate Transparency (CT) это технология публикации сведений о сертификатах, выпускаемых Удостоверяющими Центрами (УЦ). TLS-сертификатов для веба в Интернете выпускается очень много. Чтобы отдельные CT-логи не разрастались чрезмерно – придумали “таймшардинг” (от англ. time и sharding: “временно́е разбиение” (нередко также называют “сегментацией” в русскоязычных текстах)).

При “таймшардинге” отдельный лог заводится для определённого периода, например, на год. Как и везде в Certificate Transparency, тут алгоритм тоже не самый очевидный: подходящий лог выбирается по времени окончания действия сертификата (а не начала, как можно подумать). Если сертификат выпускается на 12 месяцев (365 дней) 05.03.2025 (пятого марта 2025 года), то публиковать (пре)сертификат нужно в тот лог, в интервал которого попадает дата окончания действия в марте 2026 года, например, интервал действия подходящего лога может начинаться 03.03.2026 и оканчиваться 03.10.2026.

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

Схемы “таймшардинга” используются (и использовались) разные, не обязательно разбивать всё на годовые интервалы. Например, сейчас Google публикует в качестве собственных доверенных восемь логов (блок “Google” по ссылке) и все эти логи имеют интервал в шесть месяцев.

В российском варианте Certificate Transparency, который используется для сертификатов собственных удостоверяющих центров (сейчас это НУЦ и ЦС ТЦИ), интервал действия для “свежих” логов установлен в один год. Раньше некоторые российские логи имели интервал в 13 месяцев, да ещё и с перекрывающимися датами, что облегчало тестирование при добавлении новых логов. Конечно, при том объёме сертификатов, которые сейчас попадают в российские логи CT, использование “таймшардинга” выглядит несколько странно: данный метод должен применяться там, где количество сертификатов, принимаемых ежемесячно, измеряется сотнями тысяч и более, а не сотнями. Возможно, при запуске учитывали риск “внезапного роста”.

Заметьте, кстати, что с целью предотвращения замусоривания публичные CT-логи обычно принимают только сертификаты, выпущенные от закрытого списка корневых ключей, то есть, только от некоторых УЦ. Поэтому разместить произвольный сертификат в произвольный лог – не получится: нужно, чтобы лог “верил” в соответствующий корень (промежуточные сертификаты можно подставить вместе с публикуемым оконечным). Поэтому, например, российские CT-логи “Яндекса” не содержат сертификатов от Let’s Encrypt.



Комментировать »

Пишут (The Register, англ.), что “топ-менеджмент” администрации президента США обсуждал “секретные” военные планы через мессенджер Signal, да ещё и в групповом чате, в который был добавлен журналист (видимо, по ошибке). Журналист про это и рассказал.

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

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



Комментировать »

ML-KEM/Kyber – быстрая криптосистема. На типовой современной аппаратуре она быстрее и чем X25519, и чем ECDH, и чем RSA. К тому же – с заявленной постквантовой стойкостью. Почему подобную криптосистему не внедрили для Интернета раньше, и в качестве основной? Можно же предположить, что вместо RSA и классического протокола Диффи-Хеллмана разработали бы что-то сразу с постквантовой стойкостью?

Публикация алгоритма Шора тут, очевидно, играет важную роль. Но криптосистемы с постквантовой стойкостью предлагались и раньше, до публикации Шора. Да, современного понятия о “постквантовой стойкости” тогда не было, но неверно и считать, что алгоритм Шора взламывает всё, что было предложено раньше этого алгоритма. Например, криптосистема McEliece, в исходном варианте, предложена в 1978 году, за 16 лет до алгоритма Шора. Но алгоритм Шора не позволяет взломать McEliece, а постквантовая стойкость может идти в качестве автоматического бонуса. Однако современные варианты McEliece стали внедрять на практике недавно, уже после того, как “постквантовый шум” набрал обороты, и как раз по причине стойкости к алгоритму Шора. Так что, пусть постквантовая стойкость и возможна без алгоритма Шора, но само по себе это ничего не гарантирует в плане внедрения криптосистем.

Кстати, что касается алгоритмов ML-KEM/Kyber, то соответствующая сложная задача (LWE) была сформулирована в 2005 году, но исходные вычислительные проблемы на решётках (SVP и др.) – тоже изучались заметно раньше, с начала 80-х годов прошлого века.

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

Во-первых, в конце 70-х и начале 80-х годов прошлого века криптография, как ни крути, являлась даже более эзотерической областью, чем она есть сейчас. Тут, несомненно, важен вопрос регулирования и “криптовойн”. То есть, вот мы разбираем достаточно узкий вопрос: почему придумали, разработали и внедрили именно RSA с FFDH (классический вариант Диффи-Хеллмана, в конечном поле), а не что-то вроде McEliece/Kyber – но предположим, на минуточку, что регулирование в отношении “понижения стойкости” тут сыграло важную роль: в каких-то криптосистемах стойкость снижать проще, чем в других. Естественно, этот процесс напрямую связан с алгоритмическими особенностями криптосистем. В той же ML-KEM контролируемо снизить стойкость, оставаясь на уровне компьютерных систем начала 80-х годов прошлого века, несколько сложнее, чем реализовать то же самое для RSA, которая тут более “гладкая”. Речь, конечно, не идёт о планах грубо сломать криптосистему, выбрав нестойкие параметры – это не сложно сделать и для ML-KEM/Kyber. Под “контролируемым снижением” тут надо понимать такое снижение стойкости, которое выполняется из предположения, что реализация не обваливается совсем, становясь игрушечной. Конечно, это больше похоже на попытку выдать желаемое за действительное. Для процесса запрета стойкой криптографии сохранение стойкости просто не могло рассматриваться в качестве первостепенного критерия. Более того, та же исходная версия McEliece предоставляла лишь что-то около 64-битов стойкости. С другой стороны, как теперь понятно, странное и строгое регулирование внедрению конкретно RSA не помешало, но даже поспособствовало, выделив эту криптосистему при помощи административных и социальных рычагов. В момент резкого роста масштабов практического применения RSA, другие криптосистемы, ещё и не реализованные на практике, тут же оказались заведомо надолго заперты на периферии технологии, в области теоретической криптографии.

Во-вторых, использование “криптографических преобразований” для передачи данных в вычислительной сети 80-х годов не просто выглядело излишним, но и вызвало обоснованные опасения относительно неоптимального расхода драгоценной пропускной способности: тут каждый байт на счету, а предлагается нарастить данные ключами на много килобит. И когда RSA оказалась вне конкуренции, те же эллиптические криптосистемы довольно долго пробирались на уровень практики, опираясь именно на короткие, если сравнивать с RSA, ключи. Короткие ключи очень удобны. В криптосистемах типа ML-KEM – и, тем более, McElice, – ключи очень длинные. Даже если предположить, что вместо ECDSA/ECDH развивалось бы что-то подобное Kyber, то килобитные ключи однозначно бы закрыли путь к внедрению: потому что – какой смысл жертвовать так много байтов? Сейчас этот смысл полностью сводится к желаемой постквантовой стойкости.

Но если бы в качестве массовых криптосистем с 70-х годов прошлого века использовались криптосистемы, стойкие к алгоритму Шора, то этот алгоритм вряд ли стал бы настолько популярным и известным. А ведь сейчас универсальная слава алгоритма Шора является единственным локомотивом, тянущим за собой в массы популярность “квантовых компьютеров”. Сама концепция квантовых вычислений – всё равно развивалась бы, поскольку она не привязана к криптографии. Не отменяет это и интереса к задаче быстрой факторизации, которая тоже важна и без криптографии. Но если не стала бы RSA популярной, если бы использовалась вместо неё стойкая к алгоритму Шора криптосистема, то не сложилось бы и “хайпа” вокруг “квантовых компьютеров”. Получается, тут есть и некоторая “обратная причинность”: постквантовые криптосистемы не внедрили потому, что спустя три десятка лет возник “квантовый хайп”.

Странная история.



Комментировать »

Популярная тема – оценивать “успехи ИИ” в процентах “программного кода”, который эти ИИ-системы сгенерировали. Вот утверждают, что Anthropic ожидает “100% кода”, но через год. А в “Ведомостях” более “реалистичные” оценки: 25% кода (да и только в отдельно взятой финансовой структуре “Т-технологии”).

“Проценты сгенерированного программного кода” – очень занимательный показатель. И не так важно, в чём его взвешивать – в строках или килобайтах командных слов. Например, можно генерировать “тавтологические строки”, которые компилятор будет просто выкидывать: if(a == b){a = b}…; или for i in [0,100]: b = 10; и т.д. Какая доля таких строк может быть в тексте программы? Сколь угодно близкая к 100% – чтобы программа хоть что-то делала, конечно, придётся написать пару вызовов, условно, каких-нибудь print “Hello!”. Заметьте, что даже корректную реализацию всякого алгоритма на ЯВУ нетрудно растянуть по “строкам кода”, что уж говорить про некорректные реализации.

Получается, что программный код должен быть “оптимальным”, в каком-то смысле – только тогда его можно учитывать в качестве показателя. А оптимизация, как процесс, строго связана с алгоритмом. А алгоритм не понятен даже компилятору, что уж там ожидать от перебора внутри LLM. Возникают, как говорится, “некоторые трудности”.

Скажем, если пытаться считать в строках, то нужно учитывать, что разным бывает не только программный код (выбранный язык, способ записи), но и сами строки, на которые этот код разбивается. Возможно, предполагается, что итоговый алгоритм, соответствующий записи в коде, будет исполнять вычислительная машина – то есть, аппаратура. Понятно, что записи машинных команд в памяти этой аппаратуры нельзя считать строками (там нет нужной семантической структуры, а если выражаться простыми ESC-последовательностями, то отсутствуют “\r\n” в нужной интерпретации). Конечно, можно посчитать строки в коде на ассемблере. Здесь генераторы кода могут создавать NOP-последовательности (то есть, вызовы “пустых” команд). Ну и пустые по результату наборы эффективных команд тоже нельзя сбрасывать со счёта – проблем с их генерированием тут меньше, чем в случае, когда на страже путей преобразования стоит компилятор.

Я не так давно писал про нашумевшую публикацию Situational Awareness, где количество строк программного кода тоже используется в качестве некой меры уровня “суперинтеллекта” (AGI и далее). Там ожидается, что “суперинтеллект” будет писать “триллионы строк кода”. А самое главное – человек не сможет этот код понять, “даже если ИИ потратит годы на объяснения”. И эти вот годы, впустую потраченные суперинтеллектом, имеют тут интересное применение: могут ведь заявить, – как сейчас нередко и происходит, – что эти пустые строки – они “вовсе не пустые, а это вы просто не понимаете, что там написано суперинтеллектом”.

Насколько человек, – специально обученный, – может в принципе не понимать программный код? Этими человеками созданы сами используемые языки. Хорошо. Есть методы обфускации, которые понимание записи затрудняют: я тут обычно привожу в пример известный в узких кругах конкурс IOCCC, предмет которого и состоит в изяществе обфускации (кстати, конкурс недавно возобновили). Заметьте, что тут даже не важно, что программы на конкурс подают люди, важно, что авторы программ могут объяснить, что именно программа делает, на случай, если вдруг конкурсная комиссия не смогла разобраться своими силами.

Конечно, скорее всего, именно трудности с выявлением алгоритма по записи этого алгоритма прямо приводят к постановке проблемы P≟NP. Тем не менее, люди смогли сформулировать понятие об алгоритмической неразрешимости, и найти алгоритмически неразрешимые проблемы, объяснив, со строгими доказательствами, почему эти проблемы алгоритмически неразрешимы. При этом исходные объяснения с доказательствами получены, так сказать, без “искусственных интеллектов”. К этой области напрямую и относится вопрос о том, как можно было бы реально взвешивать ИИ по “доле программного кода”, им сгенерированного. И максимально упрощенный ответ очень простой: никак.



Комментировать »

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

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

Антивирусы могут использоваться в корпоративной среде, да и не только. Вот поэтому-то нужно правильно оценивать риски и учитывать, что антивирус, инспектирующий трафик грубым отключением TLS, во-первых, раскрывает в неизвестном направлении все данные, а не только “зашиту сертификатом”; во-вторых, при этом ещё и переключает доверие TLS: с браузера (или другого клиента) – на свою собственную реализацию.



Комментировать »

В Go 1.24 появилась типовая библиотека, реализующая ML-KEM – crypto/mlkem. На ML-KEM, если нужно задекларировать постквантовую стойкость, нетрудно перейти, например, с ECDH – “эллиптического” протокола Диффи-Хеллмана. Предположим, что у нас ECDH используется для получения общего секрета при обмене данными между микросервисами по HTTP: один из сервисов выступает HTTP-клиентом и сначала запрашивает у сервера сеансовый открытый ключ ECDH (далее – просто DH), а потом в ответном сообщении передаёт свой открытый ключ, вместе с зашифрованными данными. Данные зашифровываются симметричным шифром, а симметричный ключ вычисляется на основе общего секрета DH.

ECDH – протокол симметричный, если смотреть на уровне привычных операций: и на клиенте, и на сервере требуется две одинаковых операции – умножение на скаляр (M). Ниже приведена схема.

ECDH
Здесь A, B – открытые параметры (ключи) DH. Блоком с буквой M обозначена основная операция протокола (умножение на скаляр).

А вот ML-KEM, на таком же уровне, симметрии не демонстрирует – здесь на сервере используются две операции: первая (K) – получение секрета для декапсуляции, этот секрет выводится вместе с открытым ключом (ключ инкапсуляции); вторая (D) – декапсуляция секрета из полученного от клиента шифротекста. На клиенте – работает одна операция (E), вычисление общего секрета с инкапсуляцией его в виде шифротекста. Посмотрим на схему.

ML-KEM
(Здесь EK – открытый ключ, CT – шифротекст.)
Хорошо видно, что схема перестала быть симметричной. Однако, если немного подрегулировать “абстракцию”, то можно убрать преобразования DH на стороне клиента в блок E (инкапсуляция). Вот так:

ECDH

Если A и B справа поменяются местами, то получится в точности схема ML-KEM. Собственно, часто так и делают: алгоритм Диффи-Хеллмана можно строго переписать в терминах KEM, это известно. Следовательно, нетрудно заменить в проекте, реализованном на Go, ECDH на ML-KEM: доработки оказываются минимальными. Посмотрим чуть детальнее.

Во-первых, речь вот про что. В начале сессии клиент отправляет HTTP-запрос GET на адрес API “коннектора”, где получает, кроме прочего, сессионный ключ. Ответ приходит в JSON. Ключ сервера находится в “блобе” SessionKey (Base64). Выглядит это примерно так, как в распечатке ниже. Кроме ключа в ответе есть токен, таймстемп и подпись ECDSA (на все поля), но это здесь “для антуража”, так что в данной записке не рассматривается.

type serverMessage struct {
	Id		uint64	`json:"Id"`
	Timestamp	uint64	`json:"Timestamp"`
	SessionKey	[]byte	`json:"SessionKey"`
	ECDSASig	[]byte	`json:"Sig"`
}

Во-вторых, мы всего лишь хотим получать вместо DH – ключ ML-KEM. Передавать “блоб” ключа можно в том же поле – в SessionKey.

Чтобы сгенерировать на сервере ключ, нужно сделать пару вызовов crypto/mlkem:

	var sMsg serverMessage
	var err error
[...] // В sMsg записывается принятый JSON-объект.
	dk, err := mlkem.GenerateKey768()
	if err != nil {
		fmt.Fprintf(os.Stderr, "ML-KEM error: %s\n", err)
		return
	}
	sMsg.SessionKey = append(sMsg.SessionKey, dk.EncapsulationKey().Bytes()[0:]...)
[...]

dk – это секретный ключ, он временно сохраняется на сервере в привязке к сессии.
Клиент получает общий секрет и шифротекст, записывает шифротекст в запрос, который отправляет по адресу API для обработки сессии. Формат данных клиента, предположим, такой:

type clientMessage struct {
	Id		uint64	`json:"Id"`
	Timestamp	uint64	`json:"Timestamp"`
	MLKEMCT		[]byte	`json:"MLKEMCT"`
	ECDSASig	[]byte	`json:"Sig"`
}

Здесь уже придётся поменять имя поля на MLKEMCT – раньше тут был бы открытый параметр DH клиента (SessionKey).
В коде, обработка на стороне клиента выглядит так:

	ek, err := mlkem.NewEncapsulationKey768(sMsg.MLKEMKey)
	if err != nil {
		fmt.Fprintf(os.Stderr, "ML-KEM error: %s\n", err)
		return
	}
	sharedSecret, ct := ek.Encapsulate()
	var cMsg clientMessage
	cMsg.MLKEMCT = append(cMsg.MLKEMCT, ct[0:]...)
[...]

Когда клиент пришлёт шифротекст, серверу нужно извлечь из него общий секрет, это делается так (используется сохранённый в обработчике “коннектора” dk):

	var cMsg clientMessage
	sharedSecret, err := dk.Decapsulate(cMsg.MLKEMCiphertext)
	if err != nil {
		fmt.Fprintf(os.Stderr, "ML-KEM error: %s\n", err)
		return
	}
[...]

Алгоритмически, от ECDH, реализуемого типовой библиотекой crypto/ecdh, отличается мало. Но замена даёт постквантовую стойкость и ML-KEM работает даже несколько быстрее, чем ECDH.

Может показаться, что в новой схеме сервер генерирует какой-то ключ, а клиент этот ключ использует, а в варианте с ECDH – ключи генерировали обе стороны. Однако, во-первых, секрет здесь генерируется и на стороне клиента; во-вторых, при использовании DH, секрет каждой из сторон раскрывает общий сессионный секрет – то есть, в DH достаточно знать секретный параметр сервера или клиента, чтобы из открытого параметра вычислить общий секрет, использованный для защиты трафика. Так что схема ML-KEM в этой части, на практике, не отличается (хоть и устроена иначе). При желании, можно сделать “гибрид”, как в TLS, но тогда придётся и передавать параметры DH в дополнение к ML-KEM, и хранить дополнительные ключи, пока не установлена сессия. Формально, спецификация ML-KEM не требует “гибридизации” – эта криптосистема заявлена как стойкая, поэтому, если соответствия спецификации достаточно, то – достаточно.



Комментировать »

Небольшой, – зато самодостаточный, – практикум по “хакерскому исследованию” DNSSEC.

В DNSSEC “цепочка доверия” выстраивается при помощи связки делегирующих записей, ключей и, собственно, цифровой подписи. Делегирующая запись – это DS (Delegation Signer), которая содержит отпечаток ключа из зоны уровнем ниже. Ключи размещаются в записях DNSKEY, а подписи – в RRSIG. Иногда спрашивают, можно ли простым методом посмотреть на детали устройства DNSSEC. Конечно, совсем вручную – будет медленно, но можно воспользоваться “низкоуровневым” режимом утилиты dig, чтобы извлечь байты нужных записей, а “сходимость” записей непосредственно проверить при помощи OpenSSL. Всё в консоли, то есть, можно сказать, что в ручном режиме. Это упражнение и разобрано ниже. Оно помогает понять практическую сторону DNSSEC.

Утилита dig – это инструмент для непосредственной работы с DNS из пакета BIND, входит в стандартный набор для данной технологической области. При помощи dig можно отправлять DNS-запросы в адрес различных интернет-узлов, получать вывод в доступной форме, а при необходимости – получать байтовые дампы, чем и воспользуемся. Общий смысл запросов должен быть очевиден, а заковыристые опции и загадочные термины ниже объясняются по мере их появления. Утилита dig не единственный вариант, но здесь используем именно её. Кроме dig и openssl dgst задействованы типовые для современных линуксов (и unix-подобных систем вообще) консольные средства: xxd, cat и др. Я здесь использую dig версии 9.16.50 под Debian 11, но это не является требованием.

План достаточно простой: получить базовые записи, относящиеся к DNSSEC, из DNS; сравнить ключи и отпечатки, что называется, вручную; убедиться, что всё сходится, сверив, как минимум, одну цифровую подпись “вручную”, то есть, при помощи инструментария openssl.

Утилита dig, при самом простом вызове (dig example.com), запрашивает A-запись у локального (системного) резолвера. По умолчанию dig печатает большое количество сведений и о запросе, и об ответе, и об ответившем сервере. Чтобы не перегружать текст, ниже используются различные дополнительные опции (+short, +noall и др.), позволяющие управлять составом вывода dig.

В качестве источника примеров здесь используется DNS-зона example.com, которая не только предназначена для того, чтобы служить зоной-примером, но ещё и удачно подписана DNSSEC (что в современной глобальной DNS – редкость).

Итак, для начала нужно узнать, какие серверы имён отвечают за зону com.

$ dig -t NS com. +noall +authority @a.root-servers.net
com.	172800	IN	NS	l.gtld-servers.net.
com.	172800	IN	NS	j.gtld-servers.net.
com.	172800	IN	NS	h.gtld-servers.net.
com.	172800	IN	NS	d.gtld-servers.net.
com.	172800	IN	NS	b.gtld-servers.net.
com.	172800	IN	NS	f.gtld-servers.net.
com.	172800	IN	NS	k.gtld-servers.net.
com.	172800	IN	NS	m.gtld-servers.net.
com.	172800	IN	NS	i.gtld-servers.net.
com.	172800	IN	NS	g.gtld-servers.net.
com.	172800	IN	NS	a.gtld-servers.net.
com.	172800	IN	NS	c.gtld-servers.net.
com.	172800	IN	NS	e.gtld-servers.net.

За этим несложным запросом и строчками ответа скрывается большое количество логических элементов DNS. Пока что непосредственно до DNSSEC мы не добрались, но описываемые подготовительные шаги прямо относятся и к устройству DNSSEC.
Расшифровка запроса dig:
-t NS – означает, что запрашивать нужно NS-записи (то есть, записи, содержащие перечень NS-серверов);
com. – это имя зоны, для которой производится поиск: это ключ для поиска, поскольку DNS является распределённой базой данных типа “ключ-значение”;
+noall +authority – это параметры, управляющие отображением результата; в dig вывод разбит на блоки различного назначения, а первый слева параметр (+noall) запрещает вывод всех этих блоков; второй параметр – включает вывод блока “авторитативной части” (Authority – см. ниже); другими словами – чтобы не множить опции, отключая отдельные блоки, сначала отключены все, а потом включен единственный блок, который мы тут хотим видеть – Authority.
@a.root-servers.net. – это DNS-имя одного из корневых серверов (про то, как это имя превращается в IP-адрес – см. ниже); сведения о зоне com. мы спрашиваем сначала у корневых серверов, обслуживающих вершину глобальной DNS.

В DNS каждая доменная зона обслуживается специальными серверами – серверами имён, которые уполномочены отвечать на запросы об именах в данной зоне. Такие серверы называют “авторитативными серверами” (или “авторитетными”). Авторитативными – серверы назначают в результате выполнения процесса делегирования: список прописывается в зоне уровнем выше. Вся глобальная DNS по именам выстроена в иерархию, ведущую к единому (пока что) корню. Эта иерархия имеет ключевое значение и для DNSSEC. “Ключевое” – в прямом смысле. В мире DNS корневой домен настолько важен, что он обозначается пустым множеством, чтобы математически подчеркнуть фундаментальность. Корневой домен отделяется в DNS-именах крайней справа точкой, вот так: habr.com. – это называется FQDN (fully-qualified domain name) и, скорее всего, знакомо многим инженерам DevOps. Поэтому в запросе выше “com.” снабжается правой точкой, как и имена авторитативных серверов для зоны com.

В DNSSEC цепочка доверия выстраивается от корневого ключа, соответствующего корню глобальной DNS. Звенья цепочки – соответствуют делегированию. Корневой ключ должен передаваться на сторону резолвера, проверяющего DNSSEC-записи, не через DNS.

Почему использована опция +authority? Потому, что корневые серверы не являются авторитативными для зоны com. Соответственно, корневой сервер, который получил запрос, отвечает “делегирующими записями”, а именно – перечнем серверов имён, которые могут быть авторитативными. Это и есть основа так называемого рекурсивного опроса. DNS-сообщения разбиты на несколько блоков (секций). Записи, соответствующие “делегирующему ответу”, передаются в специальном блоке (Authority). Если бы корневой сервер был авторитативным для com., то он бы ответил перечнем NS в блоке обычного ответа (Answer). Таким образом мы проверили работу делегирования: сервер вышестоящей зоны отвечает списком NS у которых нужно спрашивать записи для зоны по мнению данного конкретного DNS-сервера. И пусть мы спрашивали именно NS-записи – полученный ответ всё равно здесь не является “окончательным” и авторитативным, несмотря на название блока Authority.

Обратите внимание, что мы спрашивали NS потому, что, во-первых, для изучения DNSSEC нам потребуются NS-записи; во-вторых, мы уже заранее знали, как устроено делегирование зоны com. Но спросить можно было и A-запись, и TXT-запись, и другую запись, и сразу для example.com. – корневой сервер всё равно ответил бы делегирующим списком серверов имён (NS list), потому что не является авторитативным, но является “делегирующим” для com. Например, спросим TXT для example.com.:

$ dig -t TXT example.com. +noall +authority @a.root-servers.net
com.	172800	IN	NS	l.gtld-servers.net.
com.	172800	IN	NS	j.gtld-servers.net.
com.	172800	IN	NS	h.gtld-servers.net.
com.	172800	IN	NS	d.gtld-servers.net.
com.	172800	IN	NS	b.gtld-servers.net.
com.	172800	IN	NS	f.gtld-servers.net.
com.	172800	IN	NS	k.gtld-servers.net.
com.	172800	IN	NS	m.gtld-servers.net.
com.	172800	IN	NS	i.gtld-servers.net.
com.	172800	IN	NS	g.gtld-servers.net.
com.	172800	IN	NS	a.gtld-servers.net.
com.	172800	IN	NS	c.gtld-servers.net.
com.	172800	IN	NS	e.gtld-servers.net.

А блок Answer – пустой:

$ dig -t TXT example.com. +noall +answer @a.root-servers.net
$

Для того, чтобы подключиться к корневому серверу a.root-servers.net утилита dig должна была определить его IP-адрес. Так как всякий рекурсивный опрос DNS начинается с корневого домена, а IP-адреса корневых серверов невозможно извлечь при помощи только DNS, то серверы, отвечающие за корневую зону, должны быть заранее известны стороне, выполняющей рекурсивный опрос. Поэтому IP-адреса корневых серверов распространяются без DNS, например, в составе дистрибутива резолвера (исторический файл root hints, например) или дистрибутива операционной системы. Впрочем, этот момент к DNSSEC напрямую не относится – для DNSSEC важен криптографический аналог: корневой ключ DNS.

Тем не менее, так как корень обычно уже настроен, для определения IP-адреса корневого сервера, который мы указали по имени a.root-servers.net., dig может использовать локальный рекурсивный резолвер:

$ dig -t A a.root-servers.net. +short
198.41.0.4

Соответственно, запросы к корневому серверу можно было отправлять по IPv4-адресу 198.41.0.4 (это только один из IP-адресов корневых серверов; конкретные IP-адреса указывают на различные узлы, являющиеся составляющими частями одного логического корневого сервера из тринадцати; в зависимости от сетевой конфигурации в точке подключения к Интернету, отвечать на запросы, отправляемые по одному и тому же IP-адресу корневого севера, могут разные физические узлы).

Итак, мы получили делегирующий ответ со списком NS для зоны com. В DNSSEC делегирующие ответы не подписываются, а связь между зонами, позволяющая строить цепочки доверия, устанавливается по ключам. В корневой зоне для com. присутствует DS-запись, содержащая отпечаток, – значение хеш-функции, – открытого ключа, размещённого в зоне com. Подписывается именно DS-запись (их может быть несколько). Заметьте, что если DS-запись отсутствует, то тем или иным способом обязательно удостоверяется (подписывается) факт отсутствия записи. Для этого служат записи NSEC/NSEC3,которые, впрочем мы здесь не рассматриваем.

Запрашиваем DS-запись:

$ dig -t DS com. +short @a.root-servers.net
19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A

Здесь использованы следующие параметры:
-t DS – DS-запись;
+short – краткий вывод (только DS-запись);
@a.root-servers.net – всё у того же корневого сервера: DS-запись для com должна быть опубликована в корневой зоне.

В ответ получили состав DS-записи. Это числа. Что они обозначают? Смотрим слева направо:

19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A

Первое число (десятичная запись, 19718) – это 16-битный идентификатор ключа, облегчающий его поиск (на практике – есть оговорки: идентификаторы могут легко совпасть у разных ключей и поэтому не должны использоваться в качестве единственного “указателя” на ключ).

Второе число (десятичная запись, 13) обозначает криптосистему ключа; здесь это ECDSA на кривой P-256, что весьма удобно для наших целей, поскольку запись ключей имеет разумную длину в 64 байта.

Третье число (два) это идентификатор хеш-функции, использованной для получения отпечатка: SHA-256.

Наконец, четвёртое число (шестнадцатеричная запись, в нём много цифр, поэтому оно разбито пробелом на две части) – это сам отпечаток, значение хеш-функции от данных записи с ключом DNSSEC.

Для DS-записи в корневой зоне есть подпись. Подписи DNSSEC публикуются в RRSIG-записях. Проверим (значение RRSIG разбито на строки):

$ dig -t DS com. +dnssec +short @a.root-servers.net
19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A
DS 8 1 86400 20250315050000 20250302040000 26470 . hYFItZHTilkQr5S2UgyiqboUu7tQ2z
QyqGJU++jAGdDMdFLNCAMZ575q sbR+A/B35ARDCeJuzC54mBn9LXrGNO8EQHoGpBX4l8n6s0y4m6bHQha2
6pOBVtB5waWns/glKgVbxStEUrlGf110Q+ShWSodgEMVkHKeOJghmC/6 jRiHyVLsiUQjYqXVUln9o9sr1l
hGvWcwrzuxuEeaQh1PRuLpqUO9KL80 pwnOK1MDFKDakmnT8sKhaegMUXKqQN6vKDJefEpFU1fqGk1hP4GV
Uy/i UbvbTpLRoJj4HBBmw8phvd1qFRbtQ2pqdCRMpVSNzdESw0fFncMutkiX NwXT8Q==

Здесь добавлена опция +dnssec, которая устанавливает в DNS-запросе флаг, требующий от авторитативного сервера отправки DNSSEC-записей, соответствующих запросу. Мы получили RRSIG. Это RRSIG от ключа подписи корневой зоны (идентификатор 26470). Корневая зона использует RSA-ключи, поэтому такой большой ответ. Чтобы не возиться с байтами устаревшей RSA – мы не станем проверять именно эту RRSIG-запись, а проверим RSSIG с ECDSA когда перейдём непосредственно к example.com. ниже. Логика работы с RRSIG от уровня зоны никак не зависит, и от используемой криптосистемы подписи тоже не зависит. Поля RRSIG тоже рассматриваются детально ниже.

Здесь же проверим, что сходится сама DS-запись, то есть, убедимся, разобрав всё по байтам, что отпечаток соответствует ключу в зоне com.

Для начала такой проверки нужно получить соответствующий ключ. Ключи публикуются в DNSKEY-записях. Запросим ключи у авторитативного сервера зоны com. (то есть, уже не у корневого сервера):

$ dig -t DNSKEY +noall +answer @a.gtld-servers.net. -q com.
com.	86400	IN	DNSKEY	257 3 13 tx8EZRAd2+K/DJRV0S+hbBzaRPS/G6JVNBitHzqpsGlz8hu
E61Ms9ANe 6NSDLKJtiTBqfTJWDAywEp1FCsEINQ==
com.	86400	IN	DNSKEY	256 3 13 pkypg02poKlakMmicLGweP8+fDKFWyeUuYXlkJ+qhZHwrHZ
q+xRSeEm+ /68t287aYn0tn/IRw7PbHl08cnH66A==

Здесь использована новая опция +answer – она означает, что мы хотим вывести основной блок ответа (Answer).

В ответе мы получили два ключа. Посмотрим на параметры записей, слева направо:
com. – имя зоны;
86400 – TTL, время кеширования записи;
IN – класс записи (не рассматриваем, здесь все будут IN);
DNSKEY – запись DNSKEY, которая содержит ключ.

Далее идёт значение флагов, соответствующих ключу (257 и 256). Это весьма важное для DNSSEC поле. В данном случае 257 означает, что установлен бит SEP (Secure Entry Point – точка доверенного входа). Именно ключ с флагом SEP нам и нужен, потому что на него должна показывать DS-запись. Естественно, флаг SEP не является строгим указанием на то, что ключ с этим флагом действительно является ключом в цепочке доверия: нужно проверить соответствие подписей и отпечатков.

Ключи в зоне с DNSSEC делят на KSK и ZSK. Первый тип, KSK (Key Signing Key – ключ подписи ключа), служит для подписывания записей с ключами DNSKEY и является SEP-ключом. Ключ ZSK (Zone Signing Key) служит для подписывания прочих DNS-записей в зоне. На практике, это достаточно условное деление: в зоне может быть только один ключ (KSK, в таком случае), которым подписано всё остальное.

Формально, разделение ключей на KSK и ZSK объясняется необходимостью снизить количество подписей, выполняемых каждым ключом до его замены: например, ZSK обновляется реже, чем другие записи в зоне, которые им подписываются, при этом сам ZSK может быть заменён без необходимости замены KSK, которая требует и замены DS-записи в зоне уровнем выше. ZSK может быть меньшей разрядности, чем KSK, что иногда может повысить производительность, но уменьшит допустимое время использования ключа. И так далее. Всё это верные, но скорее теоретические рассуждения, относящиеся к “сферической в вакууме”, идеальной системе, потому что на практике DNSSEC в корне всё ещё использует RSA, а сама система была запущена даже без проектирования процесса ротации корневых ключей. Конечно, в зоне может быть несколько ключей KSK, несколько ZSK. Ключи должны быть предварительно опубликованы в процессе ротации, также предусмотрен бит статуса, отмечающий отозванные ключи.

Вернёмся к записи DNSKEY (убран пробел и удалены общие поля):

257 3 13 tx8EZRAd2+K/DJRV0S+hbBzaRPS/G6JVNBitHzqpsGlz8huE61Ms9ANe6NSDLKJtiTBqfTJWDAywEp1FCsEINQ==

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

Параметр 13 – обозначает криптосистему: ECDSA-P256 (обратите внимание, что уже сходится с параметрами DS-записи выше).

Следом идёт значение ключа, записанное здесь в Base64. Запись в Base64 генерирует dig.

Значение хеш-функции для DS-записи вычисляется от данных DNSKEY-записи. Для сравнения результатов преобразований проделаем несколько шагов.

Шаг 1. Получение байтового представления DNSKEY-записи

Значение хеш-функции для DS-записи вычисляется от конкатенации имени, к которому относится ключ, с DNSKEY-записью. Всё в байтовом представлении. Поэтому нам нужны байты DNSKEY-записи, а не текстовое отображение, которое получено утлитой dig выше. К счастью, всё та же утилита dig может вывести и исходные байты: для этого нужно указать параметр +unknownformat (или просто +unknown), здесь вывод байтовых значений разбит на несколько строк:

$ dig -t DNSKEY com. +noall +answer @a.gtld-servers.net +unknownformat
com.	86400	CLASS1	TYPE48	\# 68 0101030DB71F0465101DDBE2BF0C9455D12FA16C1CDA44F4BF1BA255
3418AD1F3AA9B06973F21B84EB532CF4035EE8D4832CA26D89306A7D
32560C0CB0129D450AC10835
com.	86400	CLASS1	TYPE48	\# 68 0100030DA64CA9834DA9A0A95A90C9A270B1B078FF3E7C32855B2794
B985E5909FAA8591F0AC766AFB14527849BEFFAF2DDBCEDA627D2D9F
F211C3B3DB1E5D3C7271FAE8

Нам нужен ключ из первого набора (257 == 0x101), а точнее – байты ресурсной записи с этим ключом, поскольку до значения ключа, если смотреть слева направо, указаны ещё флаги и идентификаторы.

Шаг 2. Добавление значения имени

Мы используем HEX-текстовую запись байтов, чтобы вычислить значение хеш-функции утилитой sha256sum. Но для этого к байтам нужно слева присоединить имя зоны. Именем у нас является com. – обязательно указывается в FQDN-формате. Для кодирования DNS-имён в DNS применяется отдельная схема: имена разбиваются на лейблы (это, грубо говоря, то, что идёт “между точками”), каждый лейбл предваряется байтом, содержащим длину записи, после которой идут байты с ASCII-кодами. То есть, никаких точек нет, но зато есть корневой домен, обозначаемый нулевым байтом. У нас для com. получается три байта со значениями букв: 0x63, 0x6F, 0x6D и 0x00 для корневого домена; перед байтами записи имени должен быть байт со значением 0x03 – это длина. Полученную строку присоединяем слева к записи DNSKEY (результат опять разбит на строки):

03636F6D000101030DB71F0465101DDBE2BF0C9455D12FA16C1CDA44F4BF1BA2
553418AD1F3AA9B06973F21B84EB532CF4035EE8D4832CA26D89306A7D32560C
0CB0129D450AC10835

Хорошо, что это “эллиптический” ключ на кривой P-256, а не RSA в 4096 бит.

Шаг 3. Вычисление значения хеш-функции

Теперь строку можно преобразовать в байты, передать в sha256sum и получить результат. Сделаем это так:

$ echo "03636F6D000101030DB71F0465101DDBE2BF0C9455D12FA16C1CDA44F4BF1BA2\
553418AD1F3AA9B06973F21B84EB532CF4035EE8D4832CA26D89306A7D32560C\
0CB0129D450AC10835" | xxd -r -p | sha256sum
8acbb0cd28f41250a80a491389424d341522d946b0da0c0291f2d3d771d7805a  -

Здесь xxd -r -p преобразует HEX-текст в байты. -r – означает “обратное” преобразование, то есть, из “дампа” в байты, а -p – задаёт тип входной строки: поток HEX-текста.

Полученный результат совпал со значением в DS-записи из корневой зоны (см. выше):

8acbb0cd28f41250a80a491389424d341522d946b0da0c0291f2d3d771d7805a
8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D771D7805A

Дальше – RRSIG

Что ж, раз ключ совпал, можно переходить к следующему упражнению: мы планировали проверить RRSIG для каких-то записей, но уже в зоне example.com. Обратите внимание, что только что обработанный ключ не подходит для example.com, поскольку это был ключ из com. Мы выше определили, на какие авторитативные серверы делегирована зона com. Спросим у них имена NS для example.com:

$ dig -t NS example.com. +noall +authority @a.gtld-servers.net
example.com.	172800	IN	NS	a.iana-servers.net.
example.com.	172800	IN	NS	b.iana-servers.net.

Зона делегирована на a и b в .iana-servers.net. Запросим у сервера b A-записи для example.com (значение RRSIG разбито на несколько строк):

$ dig -t A example.com. +noall +answer +dnssec @b.iana-servers.net.
example.com.	300	IN	A	23.192.228.80
example.com.	300	IN	A	23.192.228.84
example.com.	300	IN	A	23.215.0.136
example.com.	300	IN	A	23.215.0.138
example.com.	300	IN	A	96.7.128.175
example.com.	300	IN	A	96.7.128.198
example.com.	300	IN	RRSIG	A 13 2 300 20250322112746 20250301062039 13517 \
			example.com. c9mvE8rEy8qnrjHkTXI2vszUdxJeex0RmmG1G2DTRABeYENPZvl3JNgz \
			bWylvUIDptgCaQhVoOFq/mWCR7mXqA==

Рассмотрим запись RRSIG, начиная от названия записи:

A 13 2 300 20250322112746 20250301062039 13517 example.com. c9m...

A – означает, что подпись RRSIG относится к A-записям;
13 – тип криптосистемы подписи (ECDSA-P-256);
2 – количество лейблов в имени; это поле необходимо для того, чтобы можно было отличить “сконструированные” имена, для wildcard-записей (то есть, вида *.example.com), от “прямых” имён; например, в example.com – два лейбла (корневой домен не считается);
300 – исходное значение TTL для подписывания: исходное значение нужно потому, что конкретные значения TTL для A-записей могли уменьшиться, так как ответ мог прийти от кеширующего резолвера; для того, чтобы подпись проверить, нужно знать то значение TTL, котрое было указано в DNS-зоне;
20250322112746 – время окончания действия подписи (2025-03-22 11:27:46);
20250301062039 – время начала действия подписи;
13517 – идентификатор ключа.

Далее идёт значение подписи в Base64. Мы будем проверять подпись при помощи утилиты dgst из OpenSSL. Чтобы проверить подпись, нужно выполнить следующее: определить байтовый состав подписываемого сообщения; найти значение подписи и преобразовать в подходящий для openssl dgst формат; извлечь из DNS ключ; привести ключ в формат, подходящий для использования с openssl dgst; проверить, что подпись сходится, при помощи openssl dgst.

Итак, RRSIG охватывает все A-записи. Естественно, подписывается не текстовый вариант, который выведен ниже, а объединение байтового представления самих записей, как они фигурируют в DNS, вместе с RRSIG (но кроме самого значения подписи). Заметьте, что в состав A-записи входит не только адрес, но и сопутствующие поля. (Всё это вместе называется RDATA.) Получим байтовое представление при помощи +unknownformat (запись RRSIG разбита на строки):

$ dig -t A example.com. +noall +answer +dnssec +unknownformat @b.iana-servers.net.
example.com.	300	CLASS1	TYPE1	\# 4 17C0E450
example.com.	300	CLASS1	TYPE1	\# 4 17C0E454
example.com.	300	CLASS1	TYPE1	\# 4 17D70088
example.com.	300	CLASS1	TYPE1	\# 4 17D7008A
example.com.	300	CLASS1	TYPE1	\# 4 600780AF
example.com.	300	CLASS1	TYPE1	\# 4 600780C6
example.com.	300	CLASS1	TYPE46	\# 95 00010D020000012C67DE9EB267C2A737
34CD076578616D706C6503636F6D0073
D9AF13CAC4CBCAA7AE31E44D7236BECC
D477125E7B1D119A61B51B60D344005E
60434F66F97724D8336D6CA5BD4203A6
D802690855A0E16AFE658247B997A8

У нас шесть A-записей. Базовое значение A-записи – это просто IPv4-адрес, записанный в четырёх октетах (байтах): 0x17C0E450 == 23.192.228.80. Чтобы преобразовать одну A-запись в байтовый формат, нужно добавить байты, обозначающие имя, тип, класс записи, а также исходное значение TTL (из RRSIG). В байтах это выглядит так:

076578616D706C65 // "example" - лейбл из DNS-имени example.com.
03636F6D         // "com" - следующий лейбл в example.com.
00               // "." - "корневой домен", обозначающий окончание записи имени.
0001             // DNS-класс записи (здесь это единица, IN).
0001             // Тип записи - единица обозначает A-запись.
0000012C         // Исходное значение TTL: 300 секунд.

Просто приписываем эти байты в качестве префикса слева к каждому значению A-записи, а результат – сортируем по возрастанию, как числа:

076578616D706C6503636F6D00000100010000012C000417C0E450 // Первая A-запись.
076578616D706C6503636F6D00000100010000012C000417C0E454 // Вторая [...]
076578616D706C6503636F6D00000100010000012C000417D70088 // [...]
076578616D706C6503636F6D00000100010000012C000417D7008A
076578616D706C6503636F6D00000100010000012C0004600780AF
076578616D706C6503636F6D00000100010000012C0004600780C6

Сортировка нужна для того, чтобы определить единственный порядок вхождения значений в подписываемое сообщение: из DNS записи могут возвращаться в произвольном порядке. Мы уже получили первый блок байтов для проверки подписи. Его нужно присоединить справа к данным RRSIG в сетевом представлении (кроме подписи). Байты RRSIG мы получили выше, но основная их часть – это значение подписи, которую нужно удалить. Как отрезать подпись? Очень просто – в префиксе, в известном формате, указаны значения полей RRSIG, в порядке слева направо, как они были рассмотрены выше. Поэтому нетрудно отделить байты, предшествующие подписи:

0001              // A-запись: это RRSIG для A-записей.
0D                // 13 - индекс криптосистемы.
02                // 2 - количество лейблов.
0000012C          // исходное значение TTL.
67DE9EB2          // таймстемп окончания действия.
67C2A737          // таймстемп начала действия.
34CD              // идентификатор ключа.
076578616D706C65  // example
03636F6D          // com
00                // .

То есть, чтобы получить подписываемое сообщение – нужно присоединить блок с байтами A-записей к этой части RRSIG. Получится следующий блок, он разбит на несколько строк:

00010D020000012C67DE9EB267C2A73734CD076578616D706C6503636F6D0007
6578616D706C6503636F6D00000100010000012C000417C0E450076578616D70
6C6503636F6D00000100010000012C000417C0E454076578616D706C6503636F
6D00000100010000012C000417D70088076578616D706C6503636F6D00000100
010000012C000417D7008A076578616D706C6503636F6D00000100010000012C
0004600780AF076578616D706C6503636F6D00000100010000012C0004600780
C6

Переходим к извлечению и преобразованию подписи. Все байты, которые идут следом за полями с параметрами RRSIG – байты подписи. В ECDSA подпись предcтавляет собой два целых значения r и s. Так как мы используем ECDSA на кривой P-256 значения будут 256-битными, то есть займут 32 байта (строго 32, поскольку спецификация предписывает в DNSSEC указывать байты полной разрядности). Чтобы подпись успешно прочитал пакет OpenSSL эти значения нужно записать в формате ASN.1/DER, что не так уж сложно сделать руками: нужно задать тип SEQUENCE, внутри которого находятся два элемента с типом INTEGER. ASN.1 здесь – это кодирование с указанием длины блока данных перед самими байтами блока. То есть, тип SEQUENCE – 0x30, длина данных будет (0x20+1+1)*2 == 0x44 (ниже будет понятно почему). Значит, первые два байта: 0x30, 0x44. Внутри SEQUENCE – INTEGER. Тип INTEGER – 0x02, длина данных будет 0x20 (это 32 байта, которые занимает и r, и s). Префикс INTEGER получается 0x02, 0x20. И он должен быть указан перед каждым 32-байтовым элементом: и r, и s. Отсюда и общая длина 0x44. (Тут есть некоторые оговорки, которые конкретно в этих данных не проявились: в ASN.1 тип INTEGER знаковый, знак записывается в старший бит старшего байта, это необходимо учитывать, дописывая нулевой байт слева, если старший бит равен единице; кроме того, длина блоков данных может иметь разный формат записи, а определяется этот формат старшим битом первого байта, следующего за байтом, указывающим тип.)

Дописав байты, получаем такое представление для значения подписи:

3044 // ASN.1/SEQUENCE
0220 // ASN.1/INTEGER
73D9AF13CAC4CBCAA7AE31E44D7236BECCD477125E7B1D119A61B51B60D34400
0220 // ASN.1/INTEGER
5E60434F66F97724D8336D6CA5BD4203A6D802690855A0E16AFE658247B997A8

Подпись в нужном формате одним блоком:

3044022073D9AF13CAC4CBCAA7AE31E44D7236BECCD477125E7B1D119A61B51B
60D3440002205E60434F66F97724D8336D6CA5BD4203A6D802690855A0E16AFE
658247B997A8

Переходим к извлечению ключа (значения опять разбиты на строки, а пробелы – удалены).

$ dig -t DNSKEY example.com +noall +answer +unknownformat @b.iana-servers.net.
example.com.		3600	CLASS1	TYPE48	\# 68 0100030D92F8951FD3F87B6DD
6E04B5FF1DA38E293DA1853ECEAB520C1BAD224FA120E04B494E9F28342AC8C55F28C55C7044FA8
0EC06F610C26FA6E4FF38FBB126443FF
example.com.		3600	CLASS1	TYPE48	\# 68 0101030D9172A4BD6537BC661
F4C91A5DEA05DE2A8625A9E5A46CED8B64089C43D9DFADECA5EAC1A870C3922026DC494F6C8522D
96081ACF27D7A891153A6309DEA4F4B5

Мы проверяем подпись внутри зоны, поэтому будем использовать ключ без флага SEP. Это ключ, соответствующий записи на 0x0100 (то есть, 256). Открытый ключ ECDSA – это точка на кривой. Здесь используется несжатый формат, так что будут указаны две координаты: X и Y. Каждая по 32 байта, поскольку это P-256. Если отделить поля параметров (0x0100030D), то найти байты ключа нетрудно:

92F8951FD3F87B6DD6E04B5FF1DA38E293DA1853ECEAB520C1BAD224FA120E04
B494E9F28342AC8C55F28C55C7044FA80EC06F610C26FA6E4FF38FBB126443FF

Здесь верхняя строка – X-координата, а нижняя – Y-координата. Осталось преобразовать ключ к формату, который можно обработать openssl dgst. OpenSSL принимает ключи в ASN.1, можно завернуть в PEM. Небольшую трудность, относительно кодирования подписи, представляет указание OID (ObjectID) – то есть, идентификаторов, по которым OpenSSL сможет опредлить, что в файле именно открытый ключ на кривой P-256. Тем не менее, всё кодирование тут точно так же, как и для подписи, сводится к ручному построению байтов префикса (это настоящий хакерский подход, между прочим) и приписыванию этого префикса к байтам ключа. Делается это следующим образом.

Префикс:

3059             // ASN.1/SEQUENCE
3013             // ASN.1/SEQUENCE
0607             // ASN.1/OID
2A8648CE3D0201   // 1.2.840.10045.2.1 ecPublicKey
0608             // ASN.1/OID
2A8648CE3D030107 // 1.2.840.10045.3.1.7 P-256
0342             // ASN.1/BIT STRING
0004...

Ключ кодируется типом ASN.1 BIT STRING, при этом X и Y объединяются, и приписывается идентификатор несжатого представления (0x04). Итоговое значение в байтах:

3059301306072A8648CE3D020106082A8648CE3D0301070342000492F895
1FD3F87B6DD6E04B5FF1DA38E293DA1853ECEAB520C1BAD224FA120E04B4
94E9F28342AC8C55F28C55C7044FA80EC06F610C26FA6E4FF38FBB126443
FF

Ключ в PEM (далее – zsk-1.pem):

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkviVH9P4e23W4Etf8do44pPaGFPs6rUgwbrSJPoS
DgS0lOnyg0KsjFXyjFXHBE+oDsBvYQwm+m5P84+7EmRD/w==
-----END PUBLIC KEY-----

Данные, которые записаны HEX-текстом, нетрудно преобразовать в исходные бинарные файлы при помощи утилиты xxd. Например, если HEX-запись подписываемого сообщения сохранена в файле tbs.hex, то получить бинарный файл можно так:

cat tbs.hex | xxd -r -p > tbs.bin

Аналогично получаем файл подписи – sig.bin

Итак, у нас есть нужный для проверки набор данных, все элементы которого получены из DNS в ручном режиме. А именно: tbs.bin – подписанные данные, A-записи и RRSIG; zsk-1.pem – ключ проверки подписи; sig.bin – подпись. Осталось убедиться, что всё сходится:

$ openssl dgst -sha256 -verify zsk-1.pem -signature sig.bin tbs.bin
Verified OK

Проверка завершилась успешно – что и требовалось доказать.



Комментировать »

В DNS-зоне dxdt.ru уже несколько лет присутствует HTTPS-запись. Эта запись позволяет размещать много полей, например, конфигурацию ECH (Encrypted Client Hello). Можно указать и так называемые “хинт-адреса” – IPv4, IPv6, – что я на днях и проделал, ограничившись, впрочем, IPv4-адресом. Указан тот же адрес, что и в A-записи. Ниже приведён скриншот страницы сервиса audit.statdom.ru с параметрами HTTPS для dxdt.ru.

Screenshot, HTTPS record data

Вообще, браузеры сейчас не используют “хинт-адреса” из HTTPS-записей. Возможно, это правильно, потому что спецификация несколько “перекручена”: строго говоря, HTTPS-запись – это разновидность SVCB-записи, а последняя допускает не только разные поля и префиксы, но и поддерживает два различных “режима” рекурсивного опроса; если этого мало, то один из этих режимов (AliasMode) усложняет и так самую путаную часть DNS – обработку CNAME. Так что далеко не факт, что браузеры начнут поддерживать SVCB полностью, однако для конфигураций ECH поддержка уже есть.

Логика, которая скрывается за добавлением “хинт-адресов”, такая: можно из DNS запросить сразу HTTPS-запись для искомого имени хоста; если ответ получен, то в этом ответе уже будет и перечень IP-адресов, к которым можно подключаться (в дополнение к адресам может быть указан перечень протоколов, криптографическая конфигурация для скрытого доступа и т.д.). В теории, “хинт-адреса” в HTTPS-записи экономят запрос на извлечение A-записей (и AAAA-записей), однако спецификация отдаёт приоритет последним – то есть, A- и AAAA-записям, – мотивируя это заботой о работе балансировщиков нагрузки.

Посмотреть HTTPS-записи можно при помощи “dig -t HTTPS”.



Комментировать »