Этап 5 — TLS 1.3: handshake до последнего байта

«Замочек в браузере» — это полстраницы криптографии в одном маленьком handshake. Сейчас разберём TLS 1.3 на молекулы: ClientHello, ServerHello, ECDHE, цепочка сертификатов, OCSP, 0-RTT, session resumption, ECH.

Рабочая ситуация

«Суббота, 03:12. Алерты: у половины сервисов клиенты получают certificate expired. Деплоев не было». Прежде чем читать: как сертификат мог «сломаться сам», если код никто не трогал? Сколько мест в цепочке запроса (CDN → WAF → балансировщик → pod) вообще предъявляют сертификаты — и за каждым ли из них кто-то следит? Держи вопрос в голове: в конце урока ты соберёшь чек-лист, чтобы такой субботы у тебя не было.

Что узнаешь — после урока ты сможешь
Вспомни из прошлых уроков

В этапе 4 TCP-канал открылся за 1 RTT — теперь каждое сообщение TLS едет в этих сегментах, и каждый лишний round-trip виден пользователю. Из этапа 2 вспомни записи CAA (какому CA можно выпускать сертификат) и HTTPS/SVCB с полем ech — обе выстрелят в этом уроке. Базовая интуиция «зачем шифровать и что такое сертификат» — «Сети с нуля», урок 7.

1. Зачем TLS 1.3, а не 1.2

TLS 1.3 — стандарт с 2018. По сравнению с 1.2 — это революция:

СвойствоTLS 1.2TLS 1.3
Handshake RTT2 RTT1 RTT (0-RTT при resumption)
ШифрыДесятки опций, многие плохие5 modern AEAD шифров — всё
Forward secrecyОпциональноОбязательно (только ECDHE)
RSA key exchangeПоддерживаетсяУдалён
SHA-1, MD5Кое-где естьЗапрещены
Шифрование сертификатаВ клиреЗашифрован после Server Hello

В 2026: TLS 1.3 обязательно, 1.2 — только legacy-клиенты, 1.0/1.1 — давно отключены везде.

2. Handshake — поле за полем

TLS 1.3 handshake состоит из 4 групп сообщений. Разберём каждое в деталях.

💻 Клиент 🖥️ Сервер ClientHello • random • cipher_suites • supported_versions • key_share (ECDHE x25519) + SNI, ALPN, sig_algs, signature schemes, ECH (опц.) ServerHello • random • selected cipher • selected version • key_share (ECDHE) Обе стороны вычисляют общий ключ (ECDHE) 🔒 EncryptedExt • Certificate (сервера) • CertVerify (подпись) • Finished (всё зашифровано!) 🔒 Finished → Соединение установлено, шифрованный канал готов

ClientHello — самое большое сообщение

Это «вступление» клиента. Хотя выглядит как один пакет, внутри — много extension'ов:

ClientHello {
  legacy_version: 0x0303,                 # TLS 1.2 (для совместимости с middleboxes)
  random: 32 байта случайных,
  legacy_session_id: 32 байта (или 0),
  cipher_suites: [
    TLS_AES_128_GCM_SHA256,
    TLS_AES_256_GCM_SHA384,
    TLS_CHACHA20_POLY1305_SHA256,
    ...                                    # список того, что клиент поддерживает
  ],
  legacy_compression_methods: [0x00],     # no compression

  extensions: [
    supported_versions: [TLS 1.3, TLS 1.2],
    supported_groups: [x25519, secp256r1, secp384r1, ...],
    signature_algorithms: [ecdsa_secp256r1_sha256, rsa_pss_rsae_sha256, ...],
    key_share: [
      {group: x25519, key: 32 байта публичного ECDHE},
      ...
    ],
    server_name: [host="app.example.com"],   # ★ SNI — критично
    application_layer_protocol_negotiation: ["h2", "http/1.1"],   # ★ ALPN
    psk_key_exchange_modes: [psk_dhe_ke],    # для resumption
    pre_shared_key: ...,                     # если это resumption
    early_data: ...,                         # если 0-RTT
    encrypted_client_hello: ...,             # ECH (опц.)
  ]
}

Каждое поле важно. Разберём ключевые.

3. SNI — Server Name Indication

На одном IP может быть хостингов десятки сайтов. Когда клиент устанавливает TLS, сервер должен знать, для какого хоста отдать сертификат. SNI — это extension в ClientHello, где клиент указывает server_name.

Проблема: SNI идёт в открытом виде (это до того, как ключ согласован). Провайдер видит, к какому сайту ты обращаешься, даже не видя содержимого.

Решение: ECH (Encrypted Client Hello) — RFC 9180. Клиент берёт публичный ключ из HTTPS-DNS-записи, шифрует SNI плюс другие фингерпринт-extensions, и шлёт как «outer ClientHello» с фейковым именем. Сервер расшифровывает.

ECH развёрнут в Cloudflare, Firefox/Chrome поддерживают. Это серьёзный шаг для приватности.

4. ALPN — Application Layer Protocol Negotiation

В TLS 1.2 после handshake клиент мог только начать слать HTTP/1.1. Чтобы согласовать HTTP/2 или HTTP/3, нужен extension ALPN.

Клиент шлёт список — «я могу h2, http/1.1». Сервер в ServerHello выбирает — «давай h2». Дальше пакеты — это HTTP/2 frames.

Без ALPN — нет HTTP/2. Это тоже основа быстрого веба сейчас.

5. ECDHE — обмен ключами по математике

Это сердце TLS. Обе стороны должны договориться о симметричном ключе, не передавая его в клире. Diffie-Hellman решает эту задачу с 1976 года.

Идея (упрощённо)

  1. Клиент берёт случайное число a (приватное), вычисляет A = ga mod p. Отправляет A.
  2. Сервер берёт случайное b, вычисляет B = gb mod p. Отправляет B.
  3. Клиент вычисляет S = Ba mod p. Сервер вычисляет S = Ab mod p. Это одно и то же число — общий секрет.
  4. Атакующий видит g, p, A, B — но вычислить a или b из A или B вычислительно сложно (discrete log problem).

ECDHE — то же самое, но на эллиптических кривых (Curve25519, P-256). Безопаснее при том же размере ключа, быстрее.

«E» в конце — Ephemeral, то есть ключи генерируются заново при каждом handshake. Это обеспечивает Forward Secrecy: даже если завтра украдут приватный ключ сервера, прошлые сессии расшифровать нельзя — там были другие эфемерные ключи.

Аналогия

Диффи-Хеллман — смешивание красок

Классическое объяснение. У всех на виду стоит банка общей жёлтой краски (параметры g, p). Ты втайне добавляешь в свою порцию каплю своего секретного цвета (число a) и ставишь смесь на стол — все её видят, но «размешать обратно» и выделить твой секретный цвет нельзя. Сервер делает то же со своим секретом b. Теперь каждый берёт чужую смесь и добавляет свой секрет: у обоих получается один и тот же итоговый цвет (общий ключ S) — а подслушивающий видел только промежуточные смеси и итог собрать не может. «Ephemeral» — краски замешиваются заново на каждое соединение: украли сегодняшний секрет — вчерашние цвета не восстановить.

6. Сертификат и цепочка

Сервер шлёт цепочку сертификатов (Certificate сообщение, зашифровано в TLS 1.3). Клиент проверяет:

  1. Имя. Subject Alternative Names (SAN) сертификата должно содержать app.example.com (или wildcard *.example.com). Старое поле CN — игнорируется.
  2. Срок. notBefore ≤ now ≤ notAfter. Современные сертификаты — на 90 дней (Let's Encrypt).
  3. Цепочка. Сертификат подписан приватным ключом intermediate CA. Intermediate подписан root CA, который вшит в trust store браузера/ОС.
  4. Подпись. Криптографическая проверка каждого звена.
  5. Не отозван. CRL или OCSP — об этом ниже.
  6. CT log. Certificate Transparency — сертификат должен быть в публичных логах (с 2018 это требование Chrome).

Что значит «доверенный root»

В твоём браузере / ОС вшит trust store — список ~150-200 root-сертификатов. Если цепочка от твоего сертификата приходит к одному из них — браузер доверяет. Если нет — большая красная страница.

Каждый root CA проходит аудит, делает Key Signing Ceremony, и должен выпускать сертификаты только подтверждённым владельцам доменов.

SAN, wildcards и multi-domain

# Wildcard покрывает один уровень
*.example.com  →  www.example.com, api.example.com  ✓
              →  app.staging.example.com  ✗

# Multi-SAN — много имён в одном сертификате
SAN: example.com, www.example.com, api.example.com, *.staging.example.com

7. OCSP — отзыв сертификатов

Что, если сертификат скомпрометирован и его нужно отозвать до истечения срока? Есть два механизма:

📜 CRL — Certificate Revocation List

CA публикует список отозванных серийников. Браузер периодически скачивает.

Проблема: CRL разрастается до мегабайт. Никто реально не качает.

⚡ OCSP — Online Certificate Status Protocol

Браузер делает HTTP-запрос к OCSP-серверу: «серийник X — отозван?». Получает ответ.

Проблема: Дополнительный RTT при каждом TLS handshake. И утечка приватности — OCSP-сервер видит, какие сайты ты открываешь.

OCSP stapling

Решение — OCSP stapling: сервер сам периодически запрашивает свой OCSP-статус и «прикладывает» его к сертификату в handshake. Клиент получает свежий статус без отдельного запроса.

Это включается в Nginx / Caddy одной строкой:

ssl_stapling on;
ssl_stapling_verify on;

OCSP Must-Staple

В сертификате может быть расширение OCSP Must-Staple — это говорит браузеру: «без OCSP-staple не доверяй мне». Защита от ситуации «OCSP сервер атакован, и через 7 дней при тишине считается ok».

8. Session resumption — экономия RTT

Сделал handshake, открыл сайт, закрыл. Через минуту вернулся. Можно ли не делать handshake заново?

TLS 1.3 поддерживает два режима:

Session Tickets

Сервер в конце handshake шлёт session ticket — зашифрованный своим ключом «снимок» состояния. Клиент сохраняет. При следующем подключении вкладывает ticket в ClientHello. Сервер расшифровывает и понимает: «о, это та же сессия». Не нужно делать новый ECDHE.

PSK (Pre-Shared Key)

Аналогично, но «ключ» хранит и сервер, и клиент. Используется в IoT и контролируемых сценариях.

Resumption экономит RTT — handshake становится «0.5-RTT» вместо 1-RTT.

9. 0-RTT — отправить данные сразу с ClientHello

Самая агрессивная оптимизация. При resumption клиент может вложить данные в первое же сообщение — раньше, чем handshake завершён.

ClientHello {
  pre_shared_key: <ticket>,
  early_data: enabled,
  ...
}
+ Application Data (HTTP-запрос!)  ← данные едут СРАЗУ

Сервер расшифровывает и обрабатывает запрос. Ответ возвращается за 1 RTT — фактически 0 RTT для данных от клиента.

⚠️ Replay-атака

0-RTT данные не имеют защиты от replay. Атакующий может перехватить и переслать тот же 0-RTT-запрос дважды. Это плохо для не-идемпотентных операций.

Поэтому правило: 0-RTT только для идемпотентных запросов (GET, OPTIONS). Никогда не разрешай POST/PUT/DELETE через 0-RTT.

10. Шифры в TLS 1.3 — все пять

В TLS 1.2 было 300+ cipher suites, многие небезопасные. В TLS 1.3 — пять, все безопасные AEAD-шифры:

CipherШифрHashКогда
TLS_AES_128_GCM_SHA256AES-128-GCMSHA-256Дефолт. AES в хардваре — везде быстрый.
TLS_AES_256_GCM_SHA384AES-256-GCMSHA-384Если параноишь — больше ключ.
TLS_CHACHA20_POLY1305_SHA256ChaCha20-Poly1305SHA-256Лучше для мобильных без AES-NI.
TLS_AES_128_CCM_SHA256AES-128-CCMSHA-256Редко.
TLS_AES_128_CCM_8_SHA256AES-128-CCM с 64-bit auth tagSHA-256IoT, узкий канал.

AEAD = Authenticated Encryption with Associated Data. Шифрование + integrity check в одном алгоритме. Никакого MAC-then-encrypt или encrypt-then-MAC — это уязвимости 1.2.

11. Post-quantum — что будет завтра

Квантовые компьютеры (когда они появятся) смогут сломать ECDHE и RSA. Сегодня атакующие могут хранить зашифрованные сессии и расшифровать через 10 лет, когда квантовые компьютеры станут реальностью. Это называется «harvest now, decrypt later».

Решение — post-quantum cryptography. В 2024 NIST стандартизировал ML-KEM (Kyber). Cloudflare, Google, Apple уже разворачивают X25519MLKEM768 — гибрид классической X25519 и Kyber. Это компромисс: если Kyber сломают — X25519 защитит. Если X25519 сломают (квантовым) — Kyber защитит.

Запомни: гибриды развёрнуты в Chrome, Cloudflare с 2024. Скоро ты увидишь это и на серверах.

12. mTLS — взаимная аутентификация

Обычный TLS аутентифицирует только сервер. Клиент анонимен (или авторизуется через Bearer-токен поверх TLS).

mTLS — обе стороны предъявляют сертификаты. Сервер в CertificateRequest просит клиента: «дай свой сертификат». Клиент шлёт свой Certificate + CertVerify (подписывает hash handshake-сообщений своим приватным ключом).

Где используется:

13. TLS terminator и middleware

В реальности TLS терминируется не на твоём приложении — а раньше:

То есть один запрос может пройти через 3-4 TLS-туннеля. На каждом — отдельный handshake, сертификаты, OCSP.

Это нормально. Но за этим нужно следить:

14. Мини-лаба: попробуй прямо сейчас

openssl есть почти в любой системе (в Windows — в составе Git Bash). Разбери «замочек» любого сайта:

# Полный handshake вживую
openssl s_client -connect example.com:443 -servername example.com -tls1_3

# Какие TLS-версии поддерживает сервер
nmap --script ssl-enum-ciphers -p 443 example.com

# Цепочка сертификатов и сроки
echo | openssl s_client -connect example.com:443 -showcerts 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

# Только дата истечения, ничего лишнего
echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -enddate

# OCSP — проверить статус
openssl ocsp -issuer chain.pem -cert cert.pem \
  -url http://ocsp.example.com -resp_text

# Проверить весь сайт онлайн
# https://www.ssllabs.com/ssltest/  → A+ обязательно

# Сделать локальный самоподписанный для тестов
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \
  -days 365 -nodes -subj "/CN=localhost"
Ожидаемый результат

В выводе s_client найди строки из этого урока: TLSv1.3 и cipher (обычно TLS_AES_256_GCM_SHA384 или TLS_AES_128_GCM_SHA256), блок Certificate chain — там видно leaf → intermediate (root сервер не шлёт: он уже в твоём trust store), поле Verify return code: 0 (ok). Команда с -noout -dates вернёт notBefore/notAfter — у Let's Encrypt разница будет 90 дней. Проверь и провальный случай: openssl s_client -connect expired.badssl.com:443 — увидишь certificate has expired, то самое сообщение из субботнего инцидента.

15. Что с этим делает DevOps

16. Задачи самопроверки — реши, потом раскрой ответ

Задача 1. Ночью истёк сертификат — но только на одном из слоёв: CDN показывает валидный, а мобильное приложение, ходящее мимо CDN прямо на балансировщик, получает ошибку. Как такое возможно и что внедрить, чтобы это не повторялось?

TLS терминируется несколько раз: у CDN свой сертификат (авто-обновляемый), у балансировщика — свой, и вот он-то и истёк. Браузерные пользователи ходят через CDN и ничего не видят, приложение — напрямую. Внедрять: инвентаризацию ВСЕХ точек терминации, автообновление на каждой (ACM/cert-manager/certbot) и внешний мониторинг срока именно тех endpoint'ов, куда реально ходят клиенты, с алертом за 14 дней.

Задача 2. Безопасность требует: «даже если приватный ключ сервера утечёт, записанный ранее трафик не должен расшифровываться». Какое свойство TLS это обеспечивает и почему в TLS 1.3 оно есть всегда?

Forward secrecy. Сессионные ключи выводятся из эфемерного ECDHE-обмена, который живёт один handshake, а не из долгоживущего ключа сертификата. В TLS 1.2 существовал RSA key exchange без этого свойства (украл ключ — расшифровал архив трафика), в TLS 1.3 он удалён: ECDHE обязателен, так что forward secrecy — не опция, а конструктивное свойство протокола.

Задача 3. Включили 0-RTT ради скорости, и пентест показал: запрос «перевести 100 $» можно записать и воспроизвести повторно. Почему это касается именно 0-RTT и как жить дальше?

Early data едет ДО завершения handshake, и у сервера нет способа отличить повтор: атакующий может переслать записанный 0-RTT-пакет ещё раз (replay). Поэтому 0-RTT разрешают только для идемпотентных запросов (GET без побочных эффектов); мутации должны ехать после полного рукопожатия — серверы и CDN умеют отвечать 425 Too Early, а приложение может требовать анти-replay токены.

Задача 4. На сервере один IP и десять HTTPS-сайтов. Благодаря какому полю ClientHello сервер выбирает правильный сертификат — и какую проблему приватности это поле создаёт?

SNI: клиент открытым текстом пишет имя хоста в ClientHello, и сервер (или балансировщик) по нему выбирает сертификат и бэкенд. Обратная сторона: провайдер и любой наблюдатель видят, НА КАКОЙ сайт ты идёшь, даже не расшифровывая трафик — по SNI работают блокировки. Решение — ECH (Encrypted Client Hello): настоящий SNI шифруется ключом из DNS-записи HTTPS/SVCB.

Задача 5. Wildcard-сертификат *.example.com прекрасно работает для api.example.com, но canary.api.example.com получает ошибку имени. Почему и какие варианты?

Wildcard покрывает ровно один уровень: *.example.com — это api, www, cdn, но не canary.api. Варианты: выпустить сертификат с нужным SAN (например, *.api.example.com или явное имя), пересмотреть схему имён, или использовать автоматический выпуск per-host (cert-manager + Let's Encrypt), где каждый хост получает своё имя в SAN.

17. Словарик урока

рукопожатие · TLS handshakeсогласование версий, ключей и проверок до первого байта приложения; в TLS 1.3 — 1 RTT
указание имени · SNI (server name indication)имя хоста открытым текстом в ClientHello — по нему выбирается сертификат
шифрованный hello · ECHшифрование настоящего SNI ключом из DNS-записи HTTPS/SVCB
выбор протокола · ALPNклиент и сервер договариваются о h2/h3/http/1.1 прямо в handshake
эфемерный обмен · ECDHEодноразовый Диффи-Хеллман на эллиптических кривых; даёт forward secrecy
прямая секретность · forward secrecyутечка ключа сервера не раскрывает прошлые сессии
цепочка сертификатов · certificate chainleaf → intermediate → root из trust store; проверяется каждое звено
SAN · subject alternative namesсписок имён, на которые действителен сертификат; CN устарел
отзыв · OCSP / CRL / staplingпроверка «не отозван ли»; stapling — сервер сам прикладывает свежий OCSP-ответ
прозрачность · certificate transparency (CT)публичные журналы всех выпущенных сертификатов; обязательны для доверия Chrome
возобновление · session resumptionповторное соединение по ticket'у без полной проверки; открывает дорогу 0-RTT
ранние данные · 0-RTT / early dataзапрос вместе с ClientHello; быстро, но уязвимо к replay
взаимный TLS · mTLSобе стороны предъявляют сертификаты; основа Zero Trust и service mesh
Мост к следующему уроку

Канал зашифрован, ALPN сказал серверу «давай h2». Но что это меняет? Внутри защищённой трубы начинается совсем другой HTTP: бинарные кадры вместо текста, десятки параллельных запросов в одном соединении, сжатие заголовков — а HTTP/3 и вовсе выбрасывает TCP. Как выглядит современный HTTP изнутри — следующий этап.

Чек-лист «понимаю это»

TLS 1.3 — это 1-RTT handshake с обязательным ECDHE (forward secrecy). ClientHello несёт всё в одном: SNI, ALPN, key_share. Сертификат проверяется по цепочке до доверенного root + CT + OCSP. Resumption даёт 0-RTT (но только для идемпотентных запросов!). Post-quantum гибриды уже в Chrome — стандарт завтрашнего дня. mTLS — твоё оружие для Zero Trust и Service Mesh.