Перейти к содержанию
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 из 4