Тема 15

Протоколы: аутентификация, TLS 1.3, SSH и мессенджеры

Протоколы: аутентификация, TLS 1.3, SSH и мессенджеры

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

История протоколов аутентификации — это история ошибок, каждая из которых выглядела безобидно. Нидхем и Шрёдер в 1978 году предложили схему с одноразовыми числами и шифрованием; семнадцать лет спустя Гэвин Лоу показал, что в ней можно выдать себя за одного из участников, если аккуратно переставить сообщения 4. В 1990-х так же ломались Kerberos, SSL 2.0 и десятки самодельных схем. Общий вывод: протокол нельзя проверить «на глаз», его нужно либо доказывать формально, либо брать готовый и стандартизованный.

TLS 1.3 — пример второго пути. Это не эволюция TLS 1.2, а переделка с нуля, в которой убрали всё, что успело сломаться: CBC-режимы, сжатие, статический обмен ключами RSA, пересогласование. Осталось ровно то, что можно доказать: AEAD, эфемерный Диффи — Хеллман, HKDF и подпись по транскрипту. Результат — рукопожатие за один round-trip, минимальная поверхность атаки и явно описанное расписание ключей.

Стоит сразу оговориться о границах темы. TLS защищает канал между двумя узлами, но не концы: если на сервере стоит прокси, расшифровывающий трафик (тема 14), а на клиенте — вредоносное расширение, TLS этого не заметит. SSH защищает сеанс, но не команды, которые в нём выполняются. Signal защищает содержимое переписки, но не метаданные о том, кто с кем и когда переписывается. Понимание того, что именно защищает протокол, а что остаётся снаружи, — половина грамотного проектирования.

Тема устроена так: сначала протокол как композиция и классические атаки — повтор, отражение, человек посередине, понижение версии. Затем аутентификация «запрос — ответ», одноразовые пароли HOTP и TOTP и место FIDO2. После — TLS 1.3 по сообщениям, расписание ключей на HKDF и запись AEAD. Далее — таблица атак, из-за которых TLS переделали. Завершают тему SSH, сквозное шифрование в мессенджерах и ответ на вопрос, почему свой протокол изобретать нельзя.

Паспорт темы. Нужно знать: темы 9, 10, 11, 14. Освоите: описывать протокол как конечный автомат; находить атаки повтора, отражения и понижения версии; разбирать рукопожатие TLS 1.3, transcript hash и расписание ключей; объяснять риски 0-RTT; реализовывать TOTP. Проверяет: ЛР 15. Минимум: разделы 1–4. Углубление: разделы 5–6.

Протокол как композиция примитивов

Что должен обеспечивать протокол

Любой протокол аутентификации и обмена ключами решает четыре задачи, и путать их нельзя.

  • Аутентификация сторон. Каждый участник должен убедиться, что говорит с тем, с кем думает.
  • Согласование ключа. Стороны должны получить общий секрет, неизвестный противнику.
  • Свежесть. Каждый сеанс должен быть уникальным, иначе старое сообщение можно предъявить повторно.
  • Целостность транскрипта. Нельзя незаметно выкинуть, вставить или переставить сообщения.

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

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

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

Протокол как конечный автомат

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

СостояниеОжидаетсяДопустимый переходРеакция на нарушение
StartClientHelloвыбор параметров → WaitServerHelloзакрыть соединение
WaitServerHelloServerHelloпроверка выбора → WaitEncryptedошибка unexpected_message
WaitEncryptedEncryptedExtensions, Certificateнакопление транскриптаразрыв
WaitFinishedCertificateVerify, Finishedпроверка подписи и MAC → Establishedразрыв
Establishedданные приложенияобработка записейclose_notify
Closed——игнорировать

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

Вторая — завершение. Нормальное закрытие отличается от разрыва: close_notify в TLS защищает от усечения, при котором противник просто обрывает соединение, и приложение не знает, что данные получены не полностью. Без явного сообщения о завершении протокол допускает truncation attack.

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

Формальная модель полезна не только для документации. Атака отражения, разобранная в следующем разделе, находится именно сопоставлением «кто в каком состоянии может принять это сообщение»: один и тот же текст сообщения допустим в двух состояниях — и протокол ломается.

Классические атаки

Четыре атаки стоит знать наизусть — они повторяются из протокола в протокол.

Повтор (replay). Противник записывает сообщение и предъявляет его позже. Защита — одноразовое число или метка времени, причём проверяющая сторона должна помнить использованные значения (окно повторов).

Отражение (reflection). Противник возвращает участнику его же сообщение в другом контексте. Если протокол не различает роли и направления, сообщение принимается как ответ от другой стороны. Именно так ломается схема из следующего раздела.

Человек посередине (MITM). Противник встаёт между сторонами, поддерживая два отдельных соединения. Защита — аутентификация ключа: подпись транскрипта или предварительно известный ключ.

Понижение версии (downgrade). Противник выкидывает из предложений современные версии, чтобы стороны договорились о старых и слабых. Защита — включать версию и набор алгоритмов в подписанный или MAC-аутентифицированный транскрипт.

Все четыре атаки укладываются в модель Долева — Яо, которой пользуются при формальном анализе: противник полностью контролирует сеть, может читать, менять, удалять и вставлять сообщения, знает все публичные значения и может запускать сколько угодно сеансов от имени любого участника. Единственное, чего он не может, — вскрыть криптографический примитив. Если протокол стоек в такой модели, он устойчив и к менее могущественному противнику.

Полезно различать и цели: пассивный противник только слушает (защита — шифрование), активный вмешивается (защита — аутентификация и целостность). Протокол, который «шифрует трафик», но не аутентифицирует стороны, защищает только от первого.

К этим четырём примыкает атака на композицию: протокол может быть безопасен сам по себе и стать небезопасным, если его запустить поверх другого протокола или тем же ключом в другом контексте. Отсюда правило разделения ключей по назначению (тема 10).

Урок Нидхема — Шрёдера

Схема Нидхема — Шрёдера 1978 года использует доверенный центр, который выдаёт участникам долговременные ключи, и одноразовые числа для свежести 5. В упрощённом виде обмен выглядит так.

  1. A → S: A, B, N_A
  2. S → A: {N_A, B, K_AB, {K_AB, A}_K_BS}_K_AS
  3. A → B: {K_AB, A}_K_BS
  4. B → A: {N_B}_K_AB
  5. A → B: {N_B − 1}_K_AB

Выглядит надёжно: одноразовые числа, шифрование, доверенный центр. Лоу в 1995 году показал, что этого недостаточно 4. Атакующий M начинает сеанс с A от имени B, получает от A сообщение для B, и, не зная ключа K_AB, переправляет его настоящему B как начало другого сеанса. B отвечает {N_B}, A расшифровывает и подтверждает, а B остаётся уверен, что говорит с A. Ключ при этом не украден — украдена аутентификация.

Лечится это одной строкой: включить имя B в сообщение 6 (исправление Лоу). То есть связать каждое сообщение с идентичностью того, кому оно предназначено. Именно поэтому в современных протоколах транскрипт целиком хешируется и подписывается, а не «каждое сообщение по отдельности».

Аутентификация «запрос — ответ» и одноразовые пароли

Симметричный запрос-ответ и атака отражением

Простейшая взаимная аутентификация на общем ключе выглядит так: клиент присылает одноразовое число N_C, сервер отвечает своим N_S и подтверждением MAC(K, N_C), клиент проверяет подтверждение и отправляет MAC(K, N_S).

Если подтверждение считается как MAC(K, N) — без указания роли, — протокол уязвим к отражению. Атакующий, не зная ключа, делает так:

сессия 1:  M → S:  N_M
           S → M:  N_S1, MAC(K, N_M)
сессия 2:  M → S:  N_S1            (выдаёт чужое число за своё)
           S → M:  N_S2, MAC(K, N_S1)
сессия 1:  M → S:  MAC(K, N_S1)    (предъявляет как свой ответ)

Сервер принимает: ведь MAC(K, N_S1) действительно корректное подтверждение для N_S1. Атакующий доказал знание ключа, не имея ключа. Это ровно та же ошибка, что у Нидхема — Шрёдера: сообщение не привязано к роли и направлению.

Атака отражением
Атакующий переиспользует ответ сервера на собственный вызов: без привязки к роли один и тот же MAC годится и как ответ клиента

Лечится это добавлением роли в MAC: MAC(K, "S" ‖ N_C) для сервера и MAC(K, "C" ‖ N_S) для клиента. Теперь ответ сервера на N_S1, полученный в другой сессии, имеет вид MAC(K, "S" ‖ N_S1) и не подходит туда, где ожидается MAC(K, "C" ‖ N_S1). Одна строка, и атака исчезает — но найти её без формального анализа или внимательного чтения невозможно.

HOTP

Одноразовые пароли решают другую задачу: аутентификация без интерактивного обмена, когда у пользователя есть только небольшое устройство. HOTP (RFC 4226 2) превращает счётчик в код:

h      = HMAC-SHA1(K, counter как 8 байт big-endian)
offset = младшие 4 бита последнего байта h
code   = (h[offset : offset+4] как uint32) & 0x7fffffff
code   = code mod 10^6, дополнить нулями слева

Счётчик увеличивается на каждой аутентификации, сервер хранит ожидаемое значение и допускает небольшое окно вперёд — на случай, если пользователь нажал кнопку лишний раз. Тестовые векторы из RFC 4226 для ключа 12345678901234567890 дают коды 755224, 287082, 359152 и так далее.

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

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

TOTP

TOTP (RFC 6238 3) заменяет счётчик временем: counter = floor((T − T0) / 30). Код действует 30 секунд, и синхронизация не нужна — нужны более-менее точные часы. Проверка допускает окно в один-два шага назад и вперёд, чтобы компенсировать задержку ввода.

import hashlib
import hmac
import struct

def totp(key: bytes, at: float, step: int = 30, digits: int = 6) -> str:
    counter = int(at) // step
    h = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest()
    offset = h[-1] & 0x0F
    value = struct.unpack(">I", h[offset:offset + 4])[0] & 0x7FFFFFFF
    return str(value % 10 ** digits).zfill(digits)
Окно проверки TOTP
Код действует один шаг; проверка с окном ±1 принимает соседние шаги и компенсирует задержку ввода

TOTP — это второй фактор, а не первый. Шестизначный код имеет всего миллион вариантов; при окне ±1 «живых» кодов три, то есть вероятность случайного попадания примерно 3·10⁻⁶. Если сервер разрешает сто попыток в минуту, ожидаемое время подбора — порядка нескольких суток, а при тысяче попыток в минуту — часов. Отсюда и требования: ограничение попыток, задержка между ними, блокировка после серии неудач. Поэтому рядом обязательны: ограничение числа попыток, блокировка после серии неудач, привязка к устройству и вход по паролю до запроса кода. И ещё одна деталь: секрет TOTP — это долговременный секрет, и его компрометация означает компрометацию фактора, а не одного кода.

Отдельная практическая угроза — «усталость от MFA»: если вместо кода приходит push-уведомление с кнопкой «Подтвердить», пользователя можно засыпать запросами, пока он не нажмёт. Это не криптографическая атака, а социальная, и защита у неё организационная: показывать источник запроса, ограничивать частоту, требовать код вручную. TOTP от такой атаки защищён лучше просто потому, что требует осознанного действия.

Сравнение подходов удобно держать в одной таблице.

ПодходСекретСвежестьФишингГлавная слабость
HOTPобщий ключ + счётчиксчётчикуязвимрассинхронизация
TOTPобщий ключ + времявремяуязвимперехват кода за 30 с
FIDO2/WebAuthnпара ключей на устройствеchallenge от сервераустойчивнужна поддержка браузера

FIDO2 и WebAuthn

FIDO2 решает проблему, которую TOTP решить не может: фишинг. В WebAuthn 9 сервер присылает случайный challenge и идентификатор origin, а аутентификатор подписывает clientDataHash, включающий этот origin. Ключ хранится в устройстве и не покидает его; подпись привязана к конкретному сайту, поэтому код, введённый на поддельной странице, бесполезен — аутентификатор откажется подписывать чужой origin.

Passkeys — это тот же WebAuthn, но с синхронизацией ключа между устройствами пользователя через облако платформы. Для сайта ничего не меняется: он по-прежнему видит подпись и проверяет origin. Меняется модель угроз: ключ теперь хранится не в одном аутентификаторе, а в аккаунте платформы, и компрометация этого аккаунта равна компрометации фактора. Это осознанный компромисс между удобством и стойкостью, и его стоит проговаривать, а не замалчивать.

Аттестация в WebAuthn позволяет серверу узнать модель аутентификатора, но она же и создаёт проблему приватности: набор «каких устройств у меня есть» становится идентификатором. Поэтому большинство сайтов аттестацию не запрашивают.

С точки зрения криптографии это обычная подпись ECDSA или Ed25519 (темы 12–13). Новизна — в модели угроз: аутентификатор считается отдельным устройством с собственной границей доверия, а проверка origin встроена в протокол, а не оставлена на внимательность пользователя.

TLS 1.3: рукопожатие

Цели и отличия от TLS 1.2

TLS 1.3 (RFC 8446 1) задумывался как ответ на две проблемы: протокол оброс необязательными и опасными возможностями, а рукопожатие занимало два round-trip. Документ RFC 7624 о защите от массового наблюдения 12 прямо подтолкнул к тому, чтобы шифровать как можно больше метаданных.

Что убрали:

  • CBC-режимы и RC4. Остались только AEAD: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256.
  • Сжатие. Оно и было источником CRIME.
  • Статический обмен ключами RSA и статический DH. Остался эфемерный DH (или PSK), то есть прямая секретность обязательна.
  • Пересогласование. Вместо него — отдельный механизм обновления ключей (KeyUpdate).
  • Собственные Mac-then-encrypt схемы. Запись всегда encrypt-then-MAC внутри AEAD.

Что добавили: шифрование большей части рукопожатия, подпись всего транскрипта в CertificateVerify, явное расписание ключей на HKDF и режим 0-RTT — с оговорками.

Сравнение по пунктам:

СвойствоTLS 1.2TLS 1.3
Round-trip до данных21 (0-RTT — 0, с риском)
Обмен ключамиRSA, DHE, ECDHE(EC)DHE, PSK
AEADопциональнообязательно
Сжатиеестьнет
Пересогласованиеестьнет (KeyUpdate)
Шифрование сертификатанетда
Подписьпо частям транскриптапо всему транскрипту

1-RTT по сообщениям

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

openssl s_server -accept 8443 -cert pki/server.pem -cert_chain pki/chain.pem \
    -key pki/server.key -tls1_3 -www
openssl s_client -connect 127.0.0.1:8443 -tls1_3 -groups X25519 \
    -CAfile pki/root.pem -msg

Флаг -msg печатает каждое сообщение рукопожатия с длиной:

>>> TLS 1.3, Handshake [length 00dd], ClientHello
<<< TLS 1.3, Handshake [length 007a], ServerHello
<<< TLS 1.3, ChangeCipherSpec [length 0001]
<<< TLS 1.3, Handshake [length 001c], EncryptedExtensions
<<< TLS 1.3, Handshake [length 0233], Certificate
<<< TLS 1.3, Handshake [length 004f], CertificateVerify
<<< TLS 1.3, Handshake [length 0034], Finished
>>> TLS 1.3, ChangeCipherSpec [length 0001]
>>> TLS 1.3, Handshake [length 0034], Finished

Читается это так. ClientHello содержит список шифронаборов, поддерживаемые группы, key_share (открытый ключ эфемерного DH) и случайные данные. ServerHello выбирает набор, группу и присылает свой key_share. С этого момента у сторон есть общий секрет, и всё дальнейшее уже шифруется — обратите внимание на InnerContent перед EncryptedExtensions: это признак зашифрованной записи. EncryptedExtensions несёт параметры, Certificate — цепочку, CertificateVerify — подпись транскрипта, Finished — MAC всего рукопожатия.

Иногда список оказывается длиннее: если клиент прислал key_share для группы, которую сервер не поддерживает, сервер отвечает HelloRetryRequest с указанием нужной группы, и обмен начинается заново — это стоит дополнительного round-trip. Поэтому клиенты присылают key_share для самой вероятной группы (обычно X25519) и надеются, что угадали.

Ещё одна деталь: ClientHello отправляется открытым текстом, и по нему видны имя сайта (SNI), список шифронаборов и версия. Шифрование SNI (ECH, Encrypted Client Hello) разрабатывается отдельно и в RFC 8446 не входит. То есть TLS 1.3 скрывает содержимое и сертификат, но не факт подключения к конкретному домену.

Заметьте, чего в списке сообщений нет: отдельного ServerKeyExchange и ClientKeyExchange. Они не нужны, потому что key_share встроен в приветствия. Именно это и даёт один round-trip.

Рукопожатие TLS 1.3
ClientHello с key_share и ServerHello с key_share дают общий секрет; дальше всё шифруется, включая сертификат

CertificateVerify

Самое важное сообщение — CertificateVerify. Сервер подписывает не сертификат и не случайное число, а хеш всего транскрипта с префиксом контекста: подпись = Sign(sk, "TLS 1.3, server CertificateVerify" ‖ 0x00 ‖ Transcript-Hash). Префикс нужен, чтобы подпись нельзя было переиспользовать в другом протоколе или в другом направлении.

Транскрипт включает все сообщения рукопожатия в порядке обмена — ClientHello со всеми предложениями, ServerHello с выбором, EncryptedExtensions, Certificate. Поэтому противник не может ни выкинуть предложение, ни подменить группу, ни понизить версию: любое изменение меняет хеш и ломает подпись. Это прямое исправление ошибки Нидхема — Шрёдера, только сделанное не в одном сообщении, а в структуре протокола.

Клиентские сертификаты в TLS 1.3 устроены иначе, чем в 1.2: сервер запрашивает их в CertificateRequest, а клиент присылает Certificate и CertificateVerify уже после Finished сервера — то есть внутри зашифрованного канала. Более того, существует пост-рукопожатийная аутентификация: сервер может запросить клиентский сертификат в любой момент сеанса, а не только при установлении соединения. Это удобно для сценариев вроде «сначала вошли как гость, потом подтвердили личность», но требует аккуратной обработки на клиенте: не все библиотеки её поддерживают.

Клиент проверяет подпись ключом из сертификата, сертификат — по цепочке (тема 14), имя — по subjectAltName. Если хоть одна проверка не прошла, рукопожатие прерывается до отправки данных приложения.

Для RSA-сертификатов в TLS 1.3 обязателен PSS, а не PKCS#1 v1.5: подпись v1.5 детерминирована и допускает атаки на схему дополнения, о которых шла речь в теме 12.

Возобновление сессии, PSK и 0-RTT

Полное рукопожатие требует вычислений на кривой и проверки цепочки. Чтобы не делать это при каждом подключении, TLS 1.3 поддерживает возобновление через PSK: после первого соединения сервер выдаёт NewSessionTicket с производным секретом, и следующее рукопожатие идёт в режиме PSK — короче и дешевле.

Отдельный режим — 0-RTT: клиент отправляет данные приложения сразу в первом полёте, используя ранний ключ из PSK. Экономия целого round-trip, но цена — потеря свойства «каждое сообщение уникально»: ранние данные не защищены от повтора. Противник может записать пакет и отправить его снова; для идемпотентного GET это безобидно, для POST перевода — нет.

Формальный анализ показывает, что 0-RTT безопасен только при дополнительных ограничениях на приложение 11. Поэтому в TLS 1.3 ранние данные отделены от остальных, сервер может их отвергнуть, а приложения обязаны либо не использовать 0-RTT для изменяющих операций, либо защищаться на своём уровне (одноразовые токены, идемпотентные ключи).

Расписание ключей и запись

HKDF-Expand-Label

Все ключи TLS 1.3 выводятся из одного общего секрета через HKDF (RFC 5869 8) с функцией HKDF-Expand-Label, которая добавляет к метке префикс "tls13 " и кодирует длину. Это не украшательство: метка гарантирует, что ключ для одной цели не совпадёт с ключом для другой, даже если входные секреты совпали.

HKDF-Expand-Label(Secret, Label, Context, Length) =
    HKDF-Expand(Secret, HkdfLabel, Length)
    где HkdfLabel = uint16(Length) ‖ uint8(len("tls13 " + Label))
                    ‖ "tls13 " + Label ‖ uint8(len(Context)) ‖ Context

Дерево ключей

Из общего секрета DHE последовательно выводятся три уровня, и на каждом используется HKDF-Extract с солью — это гарантирует, что знание одного уровня не даёт следующих.

Приём с Derive-Secret(..., "derived", "") выглядит странно, но у него есть точный смысл. Нельзя подавать handshake_secret напрямую как соль для следующего Extract: если один и тот же секрет используется и как выход одного шага, и как вход другого, разные ветви дерева могут случайно совпасть. Промежуточное значение с отдельной меткой разводит ветви и делает выводы независимыми в модели случайного оракула.

Расписание ключей TLS 1.3
HKDF-Extract на каждом уровне, затем Expand-Label с метками — так получаются ключи рукопожатия и приложения
early_secret      = HKDF-Extract(0, PSK)            (или 0 без PSK)
derived           = Derive-Secret(early_secret, "derived", "")
handshake_secret  = HKDF-Extract(derived, DHE)
derived2          = Derive-Secret(handshake_secret, "derived", "")
master_secret     = HKDF-Extract(derived2, 0)

client_hs_traffic = Derive-Secret(handshake_secret, "c hs traffic",  Hash(CH..SH))
server_hs_traffic = Derive-Secret(handshake_secret, "s hs traffic",  Hash(CH..SH))
client_ap_traffic = Derive-Secret(master_secret,    "c ap traffic",  Hash(CH..server Finished))
server_ap_traffic = Derive-Secret(master_secret,    "s ap traffic",  Hash(CH..server Finished))

Ключи рукопожатия действуют только до Finished, затем стираются и заменяются ключами приложения. Это и есть прямая секретность на уровне протокола: даже если долговременный ключ сервера украдут завтра, записанный вчера трафик не расшифровать.

Запись AEAD

Каждая запись TLS 1.3 шифруется AEAD (тема 9) с ключом своего направления и nonce, который строится из порядкового номера:

nonce = IV XOR (sequence_number как 12 байт)

Порядковый номер увеличивается на каждой записи и никогда не повторяется в пределах ключа — это ровно то требование, без которого GCM и ChaCha20-Poly1305 ломаются (тема 9). У AES-GCM есть и практический предел: после примерно 2^24,5 записей на одном ключе стойкость падает из-за коллизий в аутентификации. Поэтому TLS 1.3 разрешает и рекомендует KeyUpdate — сообщение, после которого обе стороны выводят новые ключи из master_secret с новой меткой. Это дешёвая операция: никаких вычислений на кривой, только HKDF. Дополнительные данные (AAD) — заголовок записи, поэтому тип содержимого и версия защищены от подмены. Внутри шифротекста лежит настоящий тип содержимого: снаружи видно только «application_data», и по трафику нельзя отличить рукопожатие от данных.

История атак, из-за которых TLS переделали

Каждая строка таблицы — отдельная тема курса, и вместе они объясняют, почему TLS 1.3 выглядит так аскетично.

АтакаГодЧто ломалаЧем закрытаТема курса
BEAST2011предсказуемый IV в CBCAEAD вместо CBC7
CRIME2012сжатие перед шифрованиемсжатие убрано7
Lucky 132013тайминг проверки MAC в CBCAEAD, постоянное время9
POODLE2014дополнение в CBC, откат на SSL 3.0CBC и SSL убраны7
FREAK2015экспортные шифры RSA-512экспортные наборы убраны12
Logjam2015экспортный DH-512 и общие параметрыDHE ≥ 2048, свои группы11
Heartbleed2014чтение памяти в OpenSSLреализация, не протокол—

Две последние стоят комментария. FREAK и Logjam — атаки на понижение стойкости: противник заставлял стороны договориться об экспортных 512-битных параметрах, которые спецслужбы могли факторизовать. Logjam вдобавок показал, что общие параметры DH у миллионов серверов позволяют готовить предвычисления один раз для всех 10. Вывод — не только запретить слабые параметры, но и генерировать свои.

Ещё одна атака, которая привела к удалению целого механизма, — атака на пересогласование (CVE-2009-3555, 2009). В TLS 1.2 клиент мог инициировать новое рукопожатие внутри существующего соединения, и сервер не различал, где кончается старое и начинается новое. Противник вставлял свои данные в начало сеанса, а затем пересогласовывал соединение с жертвой так, что часть «чужих» данных попадала в аутентифицированный поток. Лечилось это расширением renegotiation_info с проверкой предыдущего Finished, а в TLS 1.3 механизм просто убрали: вместо пересогласования есть KeyUpdate, а вместо смены параметров — новое соединение.

Отдельная строка истории — отказ от старых версий. TLS 1.0 и 1.1 официально устарели (RFC 8996), и современные библиотеки их просто не включают. Проверить конфигурацию своего сервера можно вручную: попытка соединения с -tls1_2 или -tls1_1 покажет, что именно принимает сервер.

Для этого достаточно двух команд: openssl s_client -connect 127.0.0.1:8443 -tls1_2 должен завершиться отказом, а openssl s_client -connect 127.0.0.1:8443 -tls1_3 -groups X25519 -brief — показать выбранную версию, группу и шифронабор одной строкой.

Heartbleed — напоминание, что протокол может быть безупречен, а реализация — нет. Уязвимость была в проверке длины в расширении heartbeat, и она позволила читать до 64 КБ памяти процесса, включая закрытые ключи. Никакая формальная модель протокола такое не поймает.

SSH и мессенджеры

SSH: обмен ключами и аутентификация

SSH (RFC 4253 7) решает ту же задачу, что TLS, но для удалённого доступа. Устройство похоже: сначала обмен ключами Диффи — Хеллмана (современный вариант — curve25519-sha256), затем аутентификация сервера его долговременным ключом, затем аутентификация пользователя.

Разница в модели доверия. В TLS сервер подтверждает имя сертификатом от УЦ. В SSH сервер подтверждает себя ключом, а клиент решает, доверять ли ему: по умолчанию — trust on first use, то есть запоминает отпечаток при первом подключении (тема 14). Дальше — либо known_hosts, либо SSH-сертификаты, где инфраструктурный ключ подписывает ключи хостов и пользователей.

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

Signal: X3DH и Double Ratchet

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

X3DH (extended triple Diffie — Hellman) — начальное согласование ключа. У каждого участника есть долговременный ключ идентичности, среднесрочный подписанный предварительный ключ и набор одноразовых. Инициатор комбинирует три-четыре DH-обмена, получая секрет, который знает только он и получатель. Сервер хранит только открытые части и не может вычислить ключ.

Double Ratchet — развитие ключа по ходу переписки. Симметричная «трещотка» выводит новый ключ на каждое сообщение (так работает и цепочка в X3DH); DH-«трещотка» добавляет новый обмен при каждом ответе. В результате:

  • Прямая секретность: компрометация текущего ключа не раскрывает прошлые сообщения.
  • Посткомпрометационная стойкость: после компрометации и нескольких обменов сеанс снова становится недоступен противнику.

У Signal есть и механизм проверки личности собеседника — «номера безопасности» (safety numbers): пара отпечатков ключей, которые стороны могут сверить голосом или по QR-коду. Это аналог проверки отпечатка SSH-ключа, только для переписки. Он не обязателен технически, но без него MITM на уровне сервера остаётся возможным, пусть и требующим активного вмешательства.

Формальный анализ протокола подтверждает эти свойства при аккуратных предположениях о примитивах 6. Именно из-за них Signal стал основой для WhatsApp, Wire и Matrix — не потому, что там «какой-то шифр», а потому, что модель угроз описана и проверена.

Olm и Megolm: сквозное шифрование в федерации Matrix

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

Для решения этих задач фонд Matrix.org разработал два взаимосвязанных криптографических протокола:

  1. Olm: прямая адаптация протокола Double Ratchet и X3DH для попарных диалогов (1-на-1). Криптографический стек включает обмен ключами Диффи — Хеллмана на Curve25519, электронные подписи на Ed25519 для аутентификации устройств и симметричное шифрование AES-256 в режиме CBC с контролем целостности HMAC-SHA-256 (в формате Encrypt-then-MAC). Каждый клиент генерирует долговременный ключ устройства (Device Key), набор одноразовых предварительных ключей (One-Time Keys) и публикует их на своём домашнем сервере.
  2. Megolm (Group Ratchet): протокол шифрования для многопользовательских групповых комнат.
    • Проблема масштабирования: если использовать классический попарный Double Ratchet в комнате из N участников, то при отправке одного сообщения отправитель обязан зашифровать его N-1 раз отдельными ключами и отправить N-1 копий шифртекста, что создаёт вычислительную и сетевую сложность O(N^2) для комнаты.
    • Архитектура исходящих сессий (Outbound Group Session): каждый участник комнаты заводит собственный однонаправленный исходящий храповик Megolm. При отправке сообщения отправитель шифрует его ровно один раз симметричным шифром AES-256-CTR + HMAC-SHA-256 и рассылает единый шифртекст всем участникам комнаты через сервер.
    • Распространение ключей сессии: начальное состояние храповика (Outbound Session Key) шифруется и доставляется каждому участнику комнаты индивидуально через надёжные попарные каналы Olm.
    • Компромисс безопасности Megolm: продвижение симметричного храповика на каждое сообщение обеспечивает совершенную прямую секретность (Forward Secrecy) — обладая ключом на шаге k, невозможно вычислить ключи предыдущих шагов 0 \dots k-1. Однако Megolm не обеспечивает посткомпрометационную стойкость (Post-Compromise Security) внутри одной сессии: если текущее состояние храповика скомпрометировано, атакующий сможет расшифровывать все будущие сообщения этой сессии до тех пор, пока клиент принудительно не сбросит сессию (что происходит периодически — например, раз в 100 сообщений или раз в неделю, а также при выходе любого участника из комнаты).

IETF MLS (RFC 9420): стандарт группового сквозного шифрования

Недостатки ad-hoc протоколов группового шифрования (линейная или квадратичная сложность, ослабленная посткомпрометационная защита) побудили инженеров из IETF (при участии Mozilla, Cisco, Wire, Matrix, Google и ведущих криптографических лабораторий) разработать единый открытый индустриальный стандарт — Messaging Layer Security (MLS), опубликованный в июле 2023 года как RFC 9420 14.

В основе MLS лежит алгоритм TreeKEM (древовидное согласование ключей):

  • Дерево храповика (Ratchet Tree): участники группы размещаются в листьях лево-сбалансированного бинарного дерева. Каждый лист соответствует участнику и хранит его ключевую пару. Каждый внутренний узел дерева также ассоциирован с ключевой парой, закрытый ключ которой знают только те участники, которые находятся в поддереве данного узла. Корень дерева (англ. Root Key) задаёт общий групповой секрет, из которого через HKDF выводятся ключи шифрования сообщений эпохи.
  • Логарифмическая сложность O(\log N):
    • При обновлении своего ключа (для обеспечения посткомпрометационной стойкости) участник генерирует новые ключевые пары только для узлов на пути от своего листа к корню дерева (высота дерева h = \lceil \log_2 N \rceil).
    • Новые открытые ключи шифруются открытыми ключами соседних поддеревьев (ко-путь, copath).
    • В результате объем управляющего сообщения обновления составляет всего O(\log N) вместо O(N) в наивных протоколах. В группе из 50 000 участников обновление ключа требует пересылки всего ~16 зашифрованных блоков.
  • Гарантии безопасности: MLS доказуемо гарантирует и совершенную прямую секретность (Forward Secrecy), и посткомпрометационную стойкость (Post-Compromise Security) для масштабных групп: компрометация одного участника полностью нивелируется при первом же обновлении ключа любым участником группы. Протокол включает строгую аутентификацию членства, криптографическую защиту от добавления неавторизованных участников сервером и защиту от расщепления истории переписки.
Древовидное согласование ключей TreeKEM в MLS
Структура дерева храповика Ratchet Tree в RFC 9420: операция Update шифрует новый ключ корня для соседних поддеревьев за логарифмическое число шагов O(log N)

Мессенджеры без идентификаторов пользователей: SimpleX Chat и защита от метаданных

Все классические протоколы сквозного шифрования — Signal, WhatsApp, Telegram, Matrix — обладают критической системной уязвимостью: они защищают содержимое сообщений, но полностью раскрывают метаданные. Сервер видит:

  • глобальный идентификатор пользователя: номер телефона в Signal и WhatsApp, логин @alice:matrix.org в Matrix;
  • социальный граф контактов: с кем, когда, с какой частотой и каким размером сообщений обменивается каждый пользователь.

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

Для радикальной ликвидации метаданных был разработан мессенджер SimpleX Chat, использующий архитектуру Zero User ID (без идентификаторов пользователей) и протокол SMP (Simple Messaging Protocol) 15.

Принципы архитектуры SimpleX:

  1. Полное отсутствие учетных записей и глобальных профилей: в SimpleX нет номеров телефонов, логинов, адресов электронной почты и даже постоянных открытых ключей пользователей.
  2. Однонаправленные изолированные очереди (Simplex Queues):
    • Для каждого контакта создаются две независимые однонаправленные очереди сообщений: одна для передачи от Алисы к Бобу, вторая — от Боба к Алисе.
    • Очереди могут физически размещаться на разных независимых SMP-серверах.
    • Сервер, на котором создана очередь, знает только случайный одноразовый токен очереди. Сервер А не знает, кто пишет в очередь, кто её вычитывает, где находится обратная очередь и существует ли она вообще. У серверов отсутствует техническая возможность связать две очереди в диалог или построить граф связей.
  3. Двухуровневое шифрование:
    • Полезная нагрузка шифруется сквозным шифрованием по протоколу Double Ratchet (X3DH + Double Ratchet на Curve25519 / ChaCha20-Poly1305).
    • Транспортный уровень SMP дополнительно оборачивает пакеты в одноразовые криптографические заголовки, так что даже при физическом перехвате трафика между клиентом и SMP-сервером невозможно связать входящие и исходящие датаграммы.
  4. Ротация очередей: в процессе общения очереди автоматически пересоздаются на новых случайных серверах с новыми идентификаторами, разрушая любые долгосрочные корреляции.
Изолированные однонаправленные очереди SimpleX Chat
Сравнение архитектур: раскрытие социального графа в традиционных E2EE мессенджерах и полная изоляция очередей в SimpleX Chat

Сравнение с другими архитектурами приватных мессенджеров:

  • Session 16: отказывается от номеров телефонов и использует децентрализованную сеть нод Service Nodes на базе блокчейна Oxen. Трафик между клиентами маршрутизируется по принципу луковой маршрутизации (протокол Lokinet), скрывая IP-адреса. Однако долговременный публичный ключ пользователя (Session ID) остаётся постоянным публичным идентификатором, что позволяет отслеживать активность конкретного ключа во времени.
  • Briar 17: полностью бессерверный пиринговый (P2P) мессенджер. В нормальных условиях работает через скрытые сервисы Tor (.onion v3), связывая клиентов напрямую без центрального сервера. В условиях блокировки Интернета или аварии инфраструктуры переключается на прямое шифрованное соединение по локальной сети Wi-Fi или Bluetooth Mesh, физически передавая зашифрованные пакеты от устройства к устройству через эффект «эпидемической маршрутизации» (store-and-forward sneakernet).

Почему нельзя изобретать свой протокол

Соберём аргументы, которые обычно игнорируют.

Во-первых, протокол — это не набор примитивов, а их композиция, и ошибки возникают на стыках. Нидхем — Шрёдер использовали стойкие примитивы и всё равно ошиблись; ошибка нашлась через семнадцать лет.

Во-вторых, атаки на протоколы не требуют взлома математики. Отражение, повтор, понижение версии, атака на композицию — всё это работает при целых ключах и корректных шифрах.

В-третьих, проверить протокол «на глаз» невозможно. Нужны либо формальные методы (модельная проверка, доказательства в модели), либо многолетний аудит и публичные атаки — то есть стандарт.

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

Отдельно стоит фреймворк Noise: он не протокол, а конструктор, из которого собирают протоколы с доказуемыми свойствами (им пользуются WireGuard, WhatsApp и десятки других проектов). Идея та же, что у TLS 1.3: фиксированный набор примитивов, явное расписание ключей, подпись или MAC транскрипта и никаких «дополнительных возможностей». Если нужен свой протокол для нестандартной задачи, правильный путь — не писать его с нуля, а собрать на Noise.

В SSH есть и свои практические грабли: агент форвардинг (ssh -A) позволяет использовать ваши ключи на удалённой машине, и если она скомпрометирована, ключами воспользуются. Ограничения в authorized_keys (command=, permitopen=, no-agent-forwarding) — дешёвая защита, которую часто игнорируют.

Практическое правило простое: если нужен защищённый канал — берите TLS 1.3, SSH или проверенную библиотеку. Если нужен протокол с новыми свойствами — берите готовый фреймворк (Noise, Signal) и не меняйте криптографическое ядро. Изобретать имеет смысл только тогда, когда вы готовы доказывать свойства формально и публиковать анализ.

Итоги темы

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

TOTP и HOTP дают второй фактор из общего секрета, но не защищают от фишинга; FIDO2/WebAuthn защищает, потому что подпись привязана к origin. TOTP требует ограничения попыток и понимания, что секрет — долговременный.

TLS 1.3 убрал всё, что успело сломаться: CBC, сжатие, статический RSA, пересогласование. Осталось AEAD, эфемерный DH, HKDF и подпись транскрипта. Рукопожатие занимает один round-trip, расписание ключей явное, а CertificateVerify защищает весь транскрипт от подмены. 0-RTT экономит ещё один round-trip, но теряет защиту от повтора — и это ограничение нужно учитывать на уровне приложения.

SSH использует ту же криптографию с другой моделью доверия (TOFU или сертификаты). Signal добавляет прямую секретность и посткомпрометационную стойкость через X3DH и Double Ratchet. Общее правило для всех: не изобретать протокол, а брать стандарт и проверенную реализацию.

Следующая тема — про то, что делать, когда ломается не протокол, а сама математика: постквантовые схемы, гибридные обмены и практика безопасного применения криптографии.

Литература

  1. Rescorla E. The Transport Layer Security (TLS) Protocol Version 1.3. — RFC 8446, IETF, 2018. https://www.rfc-editor.org/rfc/rfc8446
  2. M'Raihi D., Bellare M., Hoornaert F., Naccache D., Ranen O. HOTP: An HMAC-Based One-Time Password Algorithm. — RFC 4226, IETF, 2005. https://www.rfc-editor.org/rfc/rfc4226
  3. M'Raihi D., Machani S., Pei M., Rydell J. TOTP: Time-Based One-Time Password Algorithm. — RFC 6238, IETF, 2011. https://www.rfc-editor.org/rfc/rfc6238
  4. Lowe G. An Attack on the Needham-Schroeder Public-Key Authentication Protocol. — Information Processing Letters, 1995, Т. 56, № 3, С. 131–133. DOI: 10.1016/0020-0190(95)00144-2
  5. Needham R. M., Schroeder M. D. Using Encryption for Authentication in Large Networks of Computers. — Communications of the ACM, 1978, Т. 21, № 12, С. 993–999. DOI: 10.1145/359657.359659
  6. Cohn-Gordon K., Cremers C., Dowling B., Garratt R., Stebila D. A Formal Security Analysis of the Signal Messaging Protocol. — 2017 IEEE European Symposium on Security and Privacy (EuroS&P), 2017, С. 451–466. DOI: 10.1109/EuroSP.2017.27
  7. Ylonen T., Lonvick C. The Secure Shell (SSH) Transport Layer Protocol. — RFC 4253, IETF, 2006. https://www.rfc-editor.org/rfc/rfc4253
  8. Krawczyk H., Eronen P. HMAC-based Extract-and-Expand Key Derivation Function (HKDF). — RFC 5869, IETF, 2010. https://www.rfc-editor.org/rfc/rfc5869
  9. W3C. Web Authentication: An API for accessing Public Key Credentials. Level 2. — W3C Recommendation, 2021. https://www.w3.org/TR/webauthn-2/
  10. Adrian D., Bhargavan K., Durumeric Z., Gaudry P., Green M., Halderman J. A., Heninger N., Hlaváč J., Iza P., Lange T., van Lunteren J., Lyu M., Mahdian M., Masson B., Marlinspike M., Moss A., Moser D., Mkacher R., Mushtaq S., Oswald D., Pornin T., Roepke T., Sasse A., Schaller R., Sidorenko A., Smith M., Wustrow E., Zanella-Béguelin S., Zimmermann P. Imperfect Forward Secrecy: How Diffie-Hellman Fails in Practice. — Proceedings of the 22nd ACM SIGSAC Conference on Computer and Communications Security (CCS), 2015, С. 5–17. DOI: 10.1145/2810103.2813707
  11. Fischlin M., Günther F. Replay Attacks on Zero Round-Trip Time: The Case of the TLS 1.3 Handshake Candidates. — 2017 IEEE European Symposium on Security and Privacy (EuroS&P), 2017, С. 60–75. DOI: 10.1109/EuroSP.2017.18
  12. Barnes R., Schneier B., Jennings C., Hardie T., Trammell B., Huitema C., Borkmann D. Confidentiality in the Face of Pervasive Surveillance: A Threat Model and Problem Statement. — RFC 7624, IETF, 2015. https://www.rfc-editor.org/rfc/rfc7624
  13. Matrix.org Foundation. Olm: A Cryptographic Ratchet; Megolm: A Group Ratchet. — Matrix Specification, 2024. https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md
  14. Barnes R., Beurdouche B., Robert R., Millican D., Omara E., Cohn-Gordon K. The Messaging Layer Security (MLS) Protocol. — RFC 9420, IETF, 2023. https://www.rfc-editor.org/rfc/rfc9420
  15. SimpleX Chat Team. SimpleX Messaging Protocol (SMP) Specification: A decentralized privacy-first messaging protocol without user identifiers. — 2025. https://github.com/simplex-chat/simplex-chat/blob/stable/docs/protocol/simplex-messaging-protocol.md
  16. OPTF (Open Privacy Tech Foundation). The Session Protocol: Onion routing and decentralized messaging. — 2024. https://sessionapp.com/whitepaper.pdf
  17. Briar Project. Briar: Secure P2P Messaging for Activists and Journalists. — 2024. https://briarproject.org/how-it-works/