TLS 1.3 handshake пошагово — от ClientHello до 0-RTT
В прошлом уроке мы поняли, что TLS даёт три гарантии: confidentiality, integrity, authentication. Сейчас разберём, как это технически достигается через handshake — начальный обмен между клиентом и сервером, в котором они договариваются о шифрах, проверяют сертификаты и генерируют общие ключи.
TLS 1.2 имеет сложный handshake с многими режимами и вариантами. TLS 1.3 (RFC 8446) — значительная переработка: handshake упрощён до одного round-trip (1-RTT), убраны небезопасные алгоритмы, perfect forward secrecy включена по умолчанию. В этом уроке — именно TLS 1.3 как современный стандарт.
После прочтения вы будете понимать, что именно происходит в этих строках вывода curl:
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
Обзор TLS 1.3 handshake — одной картинкой
Всего один RTT — клиент послал hello, сервер ответил полным набором сообщений, клиент послал свой Finished + сразу первый application data (HTTP request). По сравнению с TLS 1.2 (2 RTT) это значимо.
Теперь по шагам, что в каждом сообщении.
Шаг 1: ClientHello — «давайте установим TLS»
Клиент отправляет первое сообщение. Что в нём:
Главная инновация TLS 1.3 — key_share в ClientHello. В TLS 1.2 ключи обменивались в отдельном round-trip; в TLS 1.3 клиент сразу предлагает свой публичный ключ для согласованной криптокривой. Это и есть способ сократить handshake до 1 RTT.
Реальный размер ClientHello — 200-500 байт. Один TCP-пакет.
Шаг 2: ServerHello — ответ сервера
Сервер выбирает: какую версию TLS использовать, какой cipher suite, какую кривую для key exchange. Затем шлёт ServerHello:
ServerHello:
version: 0x0303 (legacy field; real version в extension supported_versions=0x0304)
random: <32 random bytes>
cipher_suite: TLS_AES_256_GCM_SHA384
extensions:
supported_versions: TLS 1.3
key_share: <server's ECDHE public key>
На этом моменте оба знают:
- Публичный ECDHE-ключ клиента (из ClientHello)
- Публичный ECDHE-ключ сервера (из ServerHello)
- random клиента + random сервера (для key derivation)
Из своего private + публичного партнёра по ECDHE математически вычисляют shared secret. Невозможно вычислить snifferу, который видел оба публичных ключа — ECDHE с современными кривыми (X25519, P-256) криптографически устойчиво.
ECDHE: математика обмена ключами
Кратко, как работает Elliptic Curve Diffie-Hellman Ephemeral:
Магия: оба пришли к одному и тому же a*b*G, передавая по сети только A и B. Атакующий, видя A и B, не может вычислить a*b*G без знания a или b (которые приватны).
Ephemeral (E в ECDHE) означает: для каждого нового TLS-сессии генерируется новая пара ключей. Если private key сервера украден позже — архивные сессии расшифровать нельзя (perfect forward secrecy). Старые ECDHE-ключи были временными, их давно нет.
Шаг 3: Сервер шлёт сертификат (шифровано)
После shared secret оба могут шифровать. Сервер шлёт следующие сообщения УЖЕ ШИФРОВАННО:
- EncryptedExtensions — дополнительные параметры (ALPN-выбор, server_name confirmation).
- Certificate — X.509-сертификат сервера + цепочка intermediate CA.
- CertificateVerify — цифровая подпись handshake-сообщений private key’ем, соответствующим public key в сертификате.
- Finished — HMAC всех handshake-сообщений (MAC проверки целостности).
Клиент:
- Расшифровывает (у него уже есть shared secret).
- Валидирует сертификат: подпись по цепочке до root CA, имя домена, срок действия.
- Проверяет CertificateVerify: подпись подтверждает, что сервер обладает private key’ем, соответствующим public key в сертификате (значит, это реально владелец сертификата).
- Проверяет Finished — что handshake не tampered.
Шаг 4: Клиент отвечает Finished + первые данные
Клиент шлёт:
- Finished — свой HMAC handshake-сообщений.
- Application Data — сразу первый HTTP-запрос внутри TLS-обёртки.
Заметьте: Application Data идёт В ТОМ ЖЕ RTT, что и Finished. Сервер получает их одним пакетом, проверяет Finished, обрабатывает запрос, отвечает.
Итог: 1 RTT с момента начала connection до полезных данных.
Cipher suites в TLS 1.3 — упрощение
В TLS 1.2 было 300+ cipher suites, многие небезопасные. Это создавало путаницу и misconfiguration. В TLS 1.3 их всего 5:
Все 5 — AEAD (Authenticated Encryption with Associated Data): шифрование и MAC в одном алгоритме. Это критично — AEAD устраняет целый класс уязвимостей TLS 1.2 (padding oracle, BEAST), где шифрование и MAC были отдельными.
В CipherSuite в TLS 1.3 кодируется только:
- AEAD алгоритм (AES-GCM, ChaCha20-Poly1305, …).
- Хэш функция для key derivation (SHA-256, SHA-384).
Кривая для key exchange и сигнатурный алгоритм — отдельно, в extensions. Это даёт более гибкие комбинации.
0-RTT resumption — ускоренный повторный handshake
Допустим, вы только что подключались к серверу. Через минуту хотите снова. TLS 1.3 даёт возможность отправить application data в первом же пакете — 0 round-trips:
Как это работает:
- На предыдущем handshake сервер выдал клиенту session ticket — зашифрованный токен с информацией о сессии (PSK — pre-shared key, параметры).
- Клиент сохранил ticket.
- На новом подключении: ClientHello включает этот ticket в extension
pre_shared_keyи сразу шифрует HTTP-запрос ключами, derived от PSK. - Сервер расшифровывает ticket, видит PSK, расшифровывает early_data, обрабатывает запрос.
Выгода: ноль round-trip между connect и first byte. Радикально быстрее для повторных visit.
Цена: 0-RTT данные не защищены от replay attacks. Атакующий, перехвативший пакет с early_data, может позже воспроизвести его. Сервер с тем же ticket выполнит запрос снова. Поэтому 0-RTT можно использовать только для safe + idempotent методов (GET, HEAD). Стандарт ЯВНО запрещает POST/PUT/DELETE через 0-RTT.
В TLS 1.2 такого механизма не было.
0-RTT — мощный, но опасный инструмент. Если вы сами пишете TLS-сервер (например, на Go или Rust) и включаете 0-RTT — ваш код должен обрабатывать replay protection (anti-replay window). Большинство production-серверов (nginx, envoy) этой делают это, но проверяйте конфиг. На уровне приложения для критических операций ВСЕГДА используйте idempotency keys, не полагаясь на TLS.
Что меняется в TLS 1.3 vs TLS 1.2
Ключевые улучшения:
Что осталось без изменений: certificates (всё ещё X.509, всё ещё PKI). Но проверка их теперь шифрованная.
SNI и ECH — отдельная история про privacy
В ClientHello клиент шлёт SNI (Server Name Indication) — имя домена, к которому хочет подключиться. Это нужно: на одном IP может быть много сайтов, сервер должен знать, какой сертификат отдавать.
Проблема: SNI идёт в plain text — любой пассивный наблюдатель (ISP, государственный sniffer) видит, на какие домены вы заходите. Содержимое зашифровано, но факт подключения к bank.com или oppositionparty.org — виден.
ECH (Encrypted Client Hello) — новое расширение, шифрует SNI и другие чувствительные extensions. Использует публичный ключ frontend-сервера (например, Cloudflare) для шифрования inner ClientHello. Backend не видит имя домена в plain.
В 2026 году ECH постепенно внедряется (Cloudflare и Chrome его поддерживают), но не повсеместно. Это значимое улучшение privacy.
Реальный пример: openssl s_client
openssl s_client — классический инструмент для дебага TLS:
openssl s_client -connect github.com:443 -servername github.com -tls1_3 < /dev/null 2>&1 | head -50
Что увидите (упрощённо):
CONNECTED(00000005)
depth=2 CN=DigiCert Global Root CA
verify return:1
depth=1 CN=DigiCert TLS RSA SHA256 2020 CA1
verify return:1
depth=0 CN=github.com
verify return:1
---
Server certificate
subject=CN=github.com
issuer=CN=DigiCert TLS RSA SHA256 2020 CA1
---
SSL handshake has read 5732 bytes and written 472 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
Разбор:
depth=2 -> depth=0— цепочка сертификатов от root до serverTLSv1.3— версияTLS_AES_128_GCM_SHA256— выбранный cipherVerification: OK— цепочка валиднаServer public key is 2048 bit— размер RSA-ключа в сертификате
Это эталонный «здоровый» TLS-обмен.
Попробуй сам
# 1. Посмотреть TLS handshake детально (требует современный curl)
curl -v https://github.com 2>&1 | grep -E 'TLS|SSL|Cipher' | head -20
# 2. Принудить TLS 1.3 (не fallback на 1.2)
curl --tls13 -v https://github.com 2>&1 | grep -i 'tls'
# 3. Посмотреть какой cipher использовался
curl -v https://github.com 2>&1 | grep -i 'cipher\|tls'
# 4. openssl s_client с детальной информацией
openssl s_client -connect cloudflare.com:443 -servername cloudflare.com 2>/dev/null < /dev/null | grep -E 'subject|issuer|Cipher|Protocol|verify'
# 5. Список поддерживаемых сервером cipher suites через nmap
# nmap --script ssl-enum-ciphers -p 443 github.com # требует nmap
# Output покажет, какие шифры/протоколы поддерживает сервер
# 6. Альтернатива через testssl.sh -- профессиональный TLS scanner
# https://testssl.sh/ -- shell script
# Запуск: ./testssl.sh github.com -- генерирует полный отчёт
# 7. Wireshark -- захватить TLS-handshake и посмотреть фреймы
# Filter: tls.handshake
# Decode TLS messages: видно ClientHello, ServerHello, etc.
# 8. SSLKEYLOGFILE для расшифровки
# export SSLKEYLOGFILE=/tmp/keys.log
# curl https://github.com # ключи сессии записаны в файл
# Wireshark с Preferences -> TLS -> Pre-Master-Secret Log Filename
# Теперь видно расшифрованный TLS-трафик
Что вы должны вынести
- TLS 1.3 handshake — 1 RTT. Намного быстрее TLS 1.2 (2 RTT).
- ClientHello уже содержит ECDHE public key — это и есть ключевое упрощение.
- ECDHE даёт perfect forward secrecy: украденный позже private key сервера не расшифрует архивные сессии.
- После shared secret все сообщения шифруются. Сертификат тоже идёт в зашифрованном виде.
- Cipher suites в TLS 1.3 — 5 штук, все AEAD. Никакой confusion.
- 0-RTT resumption даёт мгновенный second visit, но только для safe методов (replay attack risk).
- SNI всё ещё plain (видно, на какой домен идёт). ECH постепенно решает это.
TLS в контексте HTTPS API: HSTS, сертификаты, cipher negotiation mTLS в Kafka: взаимная аутентификация broker и client