Learning Platform
Глоссарий Troubleshooting
Урок 12.02 · 25 мин
Средний
TLSHandshakeECDHEKey exchange0-RTT

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»

Клиент отправляет первое сообщение. Что в нём:

ClientHello -- содержимое
versionlegacy_version поле -- ставится 0x0303 (TLS 1.2) для совместимости со старыми middleboxes; реальная версия в extension supported_versions
random (32 bytes)Случайные 32 байта от клиента. Используются для генерации ключей. Обязательно cryptographically random
cipher_suitesСписок шифров, которые клиент поддерживает. В TLS 1.3 их всего пять: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, и два EXPORT для совместимости
extensionsКуча extensions: SNI (доменное имя), ALPN (HTTP-версии), supported_versions (TLS 1.3), supported_groups (для key exchange), signature_algorithms, key_share
key_shareСамое важное в TLS 1.3: клиент сразу шлёт свой публичный ECDHE-ключ для выбранной кривой (обычно X25519). Сервер использует его для key exchange
server_name (SNI)Имя домена, к которому подключаемся (github.com). Нужно, потому что на одном IP может быть много доменов. В TLS 1.3 plaintext (ECH делает encrypted)

Главная инновация 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:

ECDHE key exchange
Curve paramsОбщая криптокривая, например X25519. Известна всем -- в спецификации TLS. Тысячи стандартизированных точек на этой кривой
КлиентГенерирует random private a (~256 бит), вычисляет публичный A = a x G (G -- base point кривой). A шлёт в ClientHello. a НИКОГДА не покидает клиент
СерверАналогично -- private b, публичный B = b x G. B шлёт в ServerHello
КлиентВычисляет shared = a x B = a x b x G
СерверВычисляет shared = b x A = b x a x G -- то же число!
Sniffer видит A и BАтакующий, перехвативший trafic, знает кривую, A, B. Может ли вычислить a*b*G? Это elliptic curve discrete log problem -- считается практически невозможной для X25519

Магия: оба пришли к одному и тому же 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 проверки целостности).

Клиент:

  1. Расшифровывает (у него уже есть shared secret).
  2. Валидирует сертификат: подпись по цепочке до root CA, имя домена, срок действия.
  3. Проверяет CertificateVerify: подпись подтверждает, что сервер обладает private key’ем, соответствующим public key в сертификате (значит, это реально владелец сертификата).
  4. Проверяет 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:

Cipher suites TLS 1.3
TLS_AES_128_GCM_SHA256AES 128-bit в GCM режиме (AEAD), хэш SHA-256. Самый распространённый, balance speed/security
TLS_AES_256_GCM_SHA384AES 256-bit (более paranoid), GCM, SHA-384. Чуть медленнее, чем 128, но без AES-NI разница больше
TLS_CHACHA20_POLY1305_SHA256ChaCha20 + Poly1305 -- альтернативный шифр, особенно быстрый на устройствах без AES-NI (старые ARM, embedded). Предпочитают мобильные
TLS_AES_128_CCM_SHA256AES в CCM режиме -- альтернатива GCM. Используется в основном в IoT (CCM проще в hardware)
TLS_AES_128_CCM_8_SHA256Сокращённый CCM (8-byte MAC вместо 16). Для constrained устройств. На веб не используется

Все 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:

Как это работает:

  1. На предыдущем handshake сервер выдал клиенту session ticket — зашифрованный токен с информацией о сессии (PSK — pre-shared key, параметры).
  2. Клиент сохранил ticket.
  3. На новом подключении: ClientHello включает этот ticket в extension pre_shared_key и сразу шифрует HTTP-запрос ключами, derived от PSK.
  4. Сервер расшифровывает 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 такого механизма не было.

WARNING

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

Ключевые улучшения:

TLS 1.3 vs TLS 1.2 -- что изменилось
1-RTT vs 2-RTTTLS 1.2 -- 2 RTT для нового соединения. TLS 1.3 -- 1 RTT. Существенный latency wins
0-RTT resumptionВ TLS 1.2 resumption всё ещё 1 RTT. В TLS 1.3 -- 0 RTT для safe ops
Forward secrecyВ TLS 1.2 ECDHE опционально (была RSA key exchange). В TLS 1.3 forward secrecy ВСЕГДА -- RSA убрана
Удалены небезопасныеВ TLS 1.2 много deprecated algorithms (RC4, SHA-1, MD5, static RSA). В TLS 1.3 убраны -- проще, безопаснее
Encrypted extensionsВ TLS 1.2 extensions (включая ALPN) plain text. В TLS 1.3 после ServerHello шифровано. Меньше info для middleboxes
Cipher suite simplification300+ comb. в TLS 1.2 -> 5 в TLS 1.3. Меньше misconfiguration

Что осталось без изменений: 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 до server
  • TLSv1.3 — версия
  • TLS_AES_128_GCM_SHA256 — выбранный cipher
  • Verification: 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-трафик

Что вы должны вынести

  1. TLS 1.3 handshake — 1 RTT. Намного быстрее TLS 1.2 (2 RTT).
  2. ClientHello уже содержит ECDHE public key — это и есть ключевое упрощение.
  3. ECDHE даёт perfect forward secrecy: украденный позже private key сервера не расшифрует архивные сессии.
  4. После shared secret все сообщения шифруются. Сертификат тоже идёт в зашифрованном виде.
  5. Cipher suites в TLS 1.3 — 5 штук, все AEAD. Никакой confusion.
  6. 0-RTT resumption даёт мгновенный second visit, но только для safe методов (replay attack risk).
  7. SNI всё ещё plain (видно, на какой домен идёт). ECH постепенно решает это.

TLS в контексте HTTPS API: HSTS, сертификаты, cipher negotiation mTLS в Kafka: взаимная аутентификация broker и client
Проверка знанийKnowledge check
Команда жалуется на медленное API. Профилирование показывает: TLS handshake занимает 80ms из 100ms общей latency. RTT до сервера 25ms. Используется TLS 1.2. Что бы сделали для улучшения, и какой эффект ожидаете?
ОтветAnswer
TLS 1.2 требует 2 RTT для handshake (ClientHello + ServerHello + Certificate, потом ClientKeyExchange + ChangeCipherSpec + Finished, потом ApplicationData). На 25ms RTT это 50ms минимум + processing. Получаемые 80ms звучат правдоподобно. Варианты ускорения, от простого к сложному: (1) Включить TLS 1.3 на сервере и клиенте. Это упрощает handshake до 1 RTT (~25ms + processing). Ожидаемое улучшение: 80ms -> 35-40ms. Single biggest win, minimal effort -- просто настроить nginx/openssl. (2) Если клиент часто подключается к тому же серверу, включить TLS session resumption. В TLS 1.2 это 1-RTT resume (через session ticket). В TLS 1.3 -- 0-RTT для safe ops. Эффект: повторные подключения вообще без handshake. (3) HTTP keep-alive -- TLS handshake происходит один раз на TCP connection, дальше много HTTP-запросов через тот же encrypted tunnel. Если клиент закрывает connection после каждого запроса -- handshake каждый раз заново. Включить keep-alive (по умолчанию в HTTP/1.1 и обязательно в HTTP/2). (4) HTTP/2 multiplexing -- 100 параллельных запросов через один handshake. (5) Connection pooling на клиенте -- requests.Session(), aiohttp.ClientSession(). Reuse одного TCP+TLS connection для многих запросов. (6) Если действительно много нового connections -- посмотреть на QUIC/HTTP/3 (0-RTT для resume, faster initial handshake intern). (7) Hardware acceleration: убедиться, что сервер использует AES-NI (для AES) -- современные процессоры делают это автоматически. Если на ARM без AES-NI -- ChaCha20-Poly1305 быстрее. Чаще всего сочетание (1) + (2) + (3) даёт 70-80% улучшение. Это типичный TLS performance tuning.

Проверьте понимание

Результат: 0 из 0
Аналитический
Вопрос 1 из 6. В TLS 1.3 ClientHello содержит key_share с ECDHE-публичным ключом клиента ЕЩЁ ДО того, как клиент знает, какой cipher выберет сервер. Зачем такое 'спекулятивное' поведение?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4