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

Разбираем, что именно происходит за тем самым замочком в адресной строке.

TLS 1.3 — протокол, который за одно рукопожатие согласует общий секрет (ECDHE), подтверждает подлинность сервера сертификатом и включает шифрование, обеспечивая forward secrecy по умолчанию.

Каждый HTTPS-запрос начинается с рукопожатия TLS. Для защитника это не магия, а конкретная последовательность сообщений, у которой есть свойства безопасности — и слабые места, если её настроить неправильно. Понимание рукопожатия объясняет, почему важна актуальная версия протокола, что даёт forward secrecy и почему «ускоритель» 0-RTT нужно включать с осторожностью.

Зачем это знать защитнику

TLS защищает почти весь современный трафик: сайты, API, почтовые протоколы, мобильные приложения. Если вы настраиваете сервер, проектируете API или расследуете инцидент, вам нужно отличать безопасную конфигурацию от уязвимой. Старые версии (SSL 3.0, TLS 1.0/1.1) содержат известные дыры (POODLE, BEAST) и должны быть отключены. TLS 1.3, стандартизованный в 2018 году, убрал целые классы проблем, выкинув устаревшие шифры и упростив переговоры.

Как проходит рукопожатие

Рукопожатие TLS 1.3 укладывается в один круговой обмен (1-RTT). Клиент не ждёт, пока сервер выберет параметры, — он сразу предлагает свои.

ClientHello

Клиент отправляет: список поддерживаемых шифронаборов, список именованных групп (кривых) для обмена ключами и — ключевое новшество — уже готовую key share: свою публичную часть ECDHE для предполагаемой кривой. По сути клиент говорит «давай считать общий секрет вот по этой кривой, вот моя половина».

ClientHello
  supported_versions: TLS 1.3
  cipher_suites: TLS_AES_128_GCM_SHA256, TLS_CHACHA20_POLY1305_SHA256
  supported_groups: x25519, secp256r1
  key_share: x25519 -> (публичная точка клиента)

ServerHello и далее

Сервер выбирает шифронабор, присылает свою key share по той же группе — и с этого момента обе стороны могут вычислить общий секрет ECDHE. Всё, что сервер шлёт после ServerHello, уже зашифровано: сертификат, подпись рукопожатия (CertificateVerify) и Finished. Это важное отличие: в TLS 1.3 сертификат сервера не виден в открытом виде, в отличие от 1.2.

ServerHello { key_share: x25519 -> (публичная точка сервера) }
--- дальше всё зашифровано ключами рукопожатия ---
{Certificate}            цепочка сертификатов сервера
{CertificateVerify}      подпись приватным ключом сервера
{Finished}               MAC по всему рукопожатию

Обмен ключами: ECDHE

Буквы ECDHE — это Elliptic Curve Diffie-Hellman Ephemeral. «Ephemeral» (эфемерный) значит, что пара ключей для обмена генерируется на каждое соединение заново и выбрасывается после. Обе стороны независимо вычисляют один и тот же секрет, не передавая его по сети. Покажем идею на крошечных числах (настоящий TLS использует кривую x25519, но математика та же):

p = 23   # модуль (в реальности огромное простое)
g = 5    # генератор
a = 6    # эфемерный приватный ключ клиента
b = 15   # эфемерный приватный ключ сервера
A = pow(g, a, p)   # клиент шлёт A
B = pow(g, b, p)   # сервер шлёт B
s_client = pow(B, a, p)
s_server = pow(A, b, p)
print('A:', A)
print('B:', B)
print('секрет клиента:', s_client)
print('секрет сервера:', s_server)
print('совпало:', s_client == s_server)

Вывод:

A: 8
B: 19
секрет клиента: 2
секрет сервера: 2
совпало: True

Перехватчик видит A и B, но без приватных a или b восстановить секрет вычислительно невозможно. На этом и держится конфиденциальность.

Аутентификация сертификатом

Обмен ключами защищает от пассивного подслушивания, но не от человека посередине: атакующий мог бы подставить свою key share обеим сторонам. Поэтому сервер подписывает рукопожатие приватным ключом, соответствующим сертификату. Клиент проверяет подпись и саму цепочку сертификатов до доверенного корневого центра (CA). Если подпись верна, значит на другом конце действительно владелец приватного ключа для этого домена — а не посредник.

Forward secrecy

Поскольку ключи ECDHE эфемерны и нигде не сохраняются, компрометация долговременного приватного ключа сервера в будущем не раскрывает прошлые сессии. Злоумышленник, записавший зашифрованный трафик сегодня и укравший ключ сервера через год, не сможет расшифровать ту запись. Это свойство называется forward secrecy (совершенная прямая секретность), и в TLS 1.3 оно включено всегда — режимы без него просто убрали из стандарта.

0-RTT и его риски

TLS 1.3 умеет возобновлять сессию ещё быстрее: при повторном подключении клиент может отправить данные приложения уже в первом пакете, не дожидаясь полного рукопожатия. Это режим 0-RTT (zero round-trip time), он экономит задержку. Но у него есть фундаментальная проблема: 0-RTT данные не защищены от повтора (replay). Атакующий, перехвативший ранний пакет, может переслать его сервру ещё раз. Для идемпотентного GET это не страшно, но для запроса, меняющего состояние («перевести деньги», «удалить запись»), повтор недопустим. Поэтому в 0-RTT отправляют только безопасные операции, а сервер должен защищаться от повторов.

Как это работает под капотом

Из общего секрета ECDHE стороны не используют его напрямую, а прогоняют через цепочку HKDF (функцию формирования ключей), получая разные ключи для разных фаз: ключи рукопожатия, ключи трафика приложения, ключ для возобновления. Каждое сообщение шифруется AEAD-шифром (AES-GCM или ChaCha20-Poly1305), который сразу даёт и конфиденциальность, и контроль целостности. Если хоть один байт изменён, проверка тега AEAD провалится и соединение разорвётся.

Как защититься

  • Отключите старьё. Разрешайте только TLS 1.2 и 1.3, выключите SSLv3, TLS 1.0/1.1 и слабые шифры (RC4, 3DES, экспортные наборы).
  • Включите forward secrecy. В 1.3 это по умолчанию; в 1.2 явно предпочитайте наборы ECDHE.
  • Следите за сертификатами. Автоматизируйте обновление (например, через ACME), не допускайте просрочки и используйте сильные ключи.
  • 0-RTT — осознанно. Включайте, только если приложение защищено от повторов и в ранние данные не попадают изменяющие операции.
  • Добавьте HSTS. Заголовок Strict-Transport-Security заставляет браузер ходить только по HTTPS, закрывая окно для понижения до HTTP.
  • Проверяйте конфигурацию сканерами вроде testssl.sh на своём стенде — они покажут слабые шифры и проблемы цепочки.

Итоги

  • TLS 1.3 согласует общий секрет через эфемерный ECDHE за один круговой обмен и шифрует почти всё рукопожатие, включая сертификат.
  • Конфиденциальность даёт обмен ключами, защиту от посредника — подпись сертификатом, защиту прошлых сессий — forward secrecy.
  • 0-RTT ускоряет повторное соединение, но открывает риск повтора — годится только для безопасных операций.
  • Главная инженерная задача — отключить устаревшие версии и шифры и держать сертификаты в порядке.
Проверьте себя
1. Что означает «E» (Ephemeral) в ECDHE и какое свойство она обеспечивает?
AКлючи обмена генерируются заново на каждое соединение, что даёт forward secrecy
BШифрование выполняется эллиптической кривой вместо RSA
CСертификат передаётся в зашифрованном виде
DИспользуется только один общий ключ на весь сервер
2. Почему режим 0-RTT в TLS 1.3 опасно использовать для операций, меняющих состояние?
AРанние данные передаются без шифрования
BРанние данные не защищены от повтора (replay) — пакет можно переслать заново
C0-RTT не поддерживает аутентификацию сервера
D0-RTT работает только по TLS 1.2