TLS-рукопожатие по шагам: ClientHello, сертификат, ключи — и на каком шаге оно ломается
14 мин чтения
TLS-рукопожатие по шагам: ClientHello, сертификат, ключи — и на каком шаге оно ломается

TLS-рукопожатие по шагам: ClientHello, сертификат, ключи — и на каком шаге оно ломается

Каждое HTTPS-соединение начинается с обмена сообщениями длиной в один-два оборота сети. Когда он срывается, браузер показывает ERR_SSL_PROTOCOL_ERROR или ERR_CERT_AUTHORITY_INVALID, curl — код 35 или 60, и по коду не видно, где сломалось. Разбираем рукопожатие TLS 1.2 и 1.3 по RFC 5246 и RFC 8446, привязываем ошибки к шагам и смотрим живой обмен через openssl и curl.

TLS-рукопожатие (TLS handshake) — процедура в начале защищённого соединения, в ходе которой клиент и сервер договариваются о версии протокола и наборе шифров, сервер подтверждает подлинность сертификатом, и обе стороны вычисляют общие сеансовые ключи. В TLS 1.2 полное рукопожатие занимает два оборота сети (2 RTT), в TLS 1.3 — один (1 RTT), а при возобновлении сессии в TLS 1.3 первый HTTP-запрос может уйти вместе с первым же сообщением (0-RTT). HTTP-данные идут только после рукопожатия: его сбой означает, что сайт не откроется, а задержка прибавляется к каждому новому соединению.

Зачем нужно рукопожатие: три задачи за один-два RTT

Рукопожатие решает три задачи: согласовать параметры (версию TLS, набор шифров, расширения SNI и ALPN), аутентифицировать сервер (клиент проверяет цепочку сертификатов и подпись его закрытым ключом) и выработать общий секрет для симметричных ключей. Без аутентификации ключи можно выработать с подставным сервером, без обмена ключами нечем шифровать.

Что согласуется, видно по имени набора шифров. В TLS 1.2 имя описывает всё разом: в TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 это обмен ключами (ECDHE), подпись в сертификате (RSA), шифр данных (AES-128-GCM) и хеш (SHA-256); этот набор RFC 9113 требует от любого сервера HTTP/2. В TLS 1.3 имя короче — TLS_AES_128_GCM_SHA256: обмен ключами и подпись вынесены в расширения supported_groups и signature_algorithms.

Как проходит рукопожатие TLS 1.2: четыре полёта и два RTT

Полное рукопожатие TLS 1.2 по RFC 5246 состоит из четырёх «полётов»: клиент шлёт ClientHello, сервер отвечает пачкой ServerHello → Certificate → ServerKeyExchange → ServerHelloDone, клиент — ClientKeyExchange → ChangeCipherSpec → Finished, сервер закрывает обмен своими ChangeCipherSpec → Finished. Первый байт HTTP-запроса уходит только на третьем обороте.

TLS 1.2 — полное рукопожатие (RFC 5246), 2 RTT

Клиент → Сервер ClientHello: версия, client_random, список наборов шифров, SNI, ALPN

▼

Сервер → Клиент ServerHello (версия, шифр, server_random) → Certificate → ServerKeyExchange (эфемерный ключ + подпись) → ServerHelloDone

конец 1-го RTT

▼

Клиент → Сервер проверяет цепочку и подпись → ClientKeyExchange → ChangeCipherSpec → Finished

▼

Сервер → Клиент ChangeCipherSpec → Finished

конец 2-го RTT — дальше HTTP-запрос

У каждого сообщения есть числовой тип по RFC 5246 — именно эти номера curl печатает в скобках при флаге -v:

Сообщение Тип Кто Что внутри
ClientHello 1 Клиент Версия, client_random, наборы шифров, расширения server_name, ALPN, supported_groups, signature_algorithms
ServerHello 2 Сервер Выбранные версия и набор, server_random, session_id
Certificate 11 Сервер Цепочка: серверный сертификат первым, затем промежуточные
ServerKeyExchange 12 Сервер Только для (EC)DHE: эфемерный ключ и подпись над ним. При RSA-обмене не отправляется
ClientKeyExchange 16 Клиент Эфемерный ключ клиента (ECDHE) либо premaster secret под открытым ключом сервера (RSA)
Finished 20 Оба Хеш всех предыдущих сообщений под новым ключом

Ключевой момент — подпись. В ECDHE-варианте сервер подписывает закрытым ключом сертификата эфемерный ключ в ServerKeyExchange: так доказывается владение сертификатом, а сеансовый ключ не зависит от долгоживущего (forward secrecy). В RSA-варианте подписи нет, и утечка ключа сервера раскрывает весь записанный трафик. Общий обзор HTTPS — в статье «Как работает HTTPS»; здесь мы идём глубже.

Что изменилось в TLS 1.3: один RTT, обязательный ECDHE, шифрование с ServerHello

TLS 1.3 (RFC 8446, август 2018) укладывает полное рукопожатие в один RTT: клиент отправляет эфемерные ключи (расширение key_share) сразу в ClientHello, а сервер одним полётом присылает ServerHello, сертификат, подпись и Finished. Всё после ServerHello зашифровано — включая сертификат, который в TLS 1.2 шёл открытым текстом.

TLS 1.3 — полное рукопожатие (RFC 8446), 1 RTT

Клиент → Сервер ClientHello + key_share (готовые ключи X25519, P-256) + supported_versions + SNI + ALPN

▼

Сервер → Клиент ServerHello + key_share, далее уже зашифровано: EncryptedExtensions → Certificate → CertificateVerify (подпись над всем рукопожатием) → Finished

конец 1-го RTT

▼

Клиент → Сервер Finished — и в том же полёте первый HTTP-запрос. Затем сервер присылает NewSessionTicket для будущих возобновлений

Что убрано (раздел 1.2 RFC 8446): статический RSA и Diffie–Hellman как обмен ключами (обмен всегда эфемерный, все наборы дают forward secrecy), сжатие, DSA, повторное рукопожатие. Осталось пять наборов шифров, все AEAD: TLS_AES_128_GCM_SHA256 (обязателен для всех реализаций), TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256 и два CCM-варианта. Наборов с CBC, RC4, 3DES и MD5 в TLS 1.3 не существует.

0-RTT — режим возобновления, при котором клиент отправляет HTTP-запрос вместе с ClientHello, шифруя его ключом из предыдущей сессии (PSK из NewSessionTicket; срок жизни билета — не больше 604 800 секунд, 7 дней). RFC 8446 предупреждает: у 0-RTT-данных нет forward secrecy и защиты от повтора, поэтому так можно принимать только идемпотентные запросы. В nginx режим (ssl_early_data) по умолчанию выключен.

Чем TLS 1.3 отличается от TLS 1.2: сводная таблица

Главные различия — число RTT, обязательная forward secrecy и шифрование сертификата. Сводка по RFC 5246, RFC 8446, caniuse и документации вендоров на сентябрь 2026:

Параметр TLS 1.2 (RFC 5246, 2008) TLS 1.3 (RFC 8446, 2018)
Полное рукопожатие 2 RTT 1 RTT (2 при HelloRetryRequest)
Возобновление 1 RTT (session ID или ticket) 1 RTT по PSK; 0-RTT для первого запроса
Обмен ключами RSA, DH, DHE, ECDHE на выбор Только (EC)DHE или PSK+(EC)DHE
Forward secrecy Только с (EC)DHE-наборами Всегда (кроме 0-RTT-данных)
Наборы шифров Десятки, включая CBC, 3DES, RC4; обязательный — TLS_RSA_WITH_AES_128_CBC_SHA 5, все AEAD; обязательный — TLS_AES_128_GCM_SHA256
Сертификат сервера Открытым текстом Зашифрован
Поддержка клиентов Повсеместно. TLS 1.0/1.1 отключены в Chrome 84 (14 июля 2020) и Firefox 78 (30 июня 2020), запрещены RFC 8996 (март 2021) Chrome 70 (16 октября 2018), Firefox 63, Edge 79, iOS 12.2, Safari 14 (macOS), Android 10 (без 0-RTT), Windows 11 и Server 2022, OpenSSL 1.1.1 (11 сентября 2018)

Сервер только с TLS 1.0/1.1 не откроет ни один актуальный браузер; в Chromium даже убрали код -218 ERR_SSL_OBSOLETE_VERSION — по комментарию в net_error_list.h, старые версии теперь дают ERR_SSL_VERSION_OR_CIPHER_MISMATCH.

Что внутри сертификата и цепочки: leaf → intermediate → root, SNI и ALPN

В сообщении Certificate сервер присылает цепочку: серверный сертификат (leaf) первым, за ним промежуточные (intermediate), каждый подписан ключом следующего. Корневой (root) обычно не отправляется — он лежит в хранилище доверия клиента, и к нему клиент строит путь: подпись leaf проверяется ключом intermediate, подпись intermediate — ключом root. Нет промежуточного звена — путь не строится, и OpenSSL сообщает «unable to get local issuer certificate» (коды 20/21) при валидном сертификате сайта. Документация nginx требует класть промежуточные сертификаты в тот же файл после основного.

В leaf проверяются subjectAltName (совпадение с запрошенным именем), период notBefore/notAfter (по часам клиента), keyUsage, подпись и статус отзыва. Максимальный срок действия публичных сертификатов на сентябрь 2026 года — 398 дней; по решению CA/Browser Forum от апреля 2025 года он сокращается до 47 дней к марту 2029-го. Как читать сертификат руками — в статье «Как проверить SSL-сертификат сайта».

Два расширения ClientHello решают, какой сертификат и протокол выберет сервер. SNI (server_name, RFC 6066) — имя хоста открытым текстом, по которому сервер с несколькими сайтами на одном IP выбирает сертификат; без SNI он отдаст сертификат по умолчанию. ALPN (RFC 7301) — список прикладных протоколов («h2», «http/1.1»), из которого сервер выбирает один в ServerHello, так HTTP/2 согласуется без лишнего RTT; нет общего протокола — алерт no_application_protocol (120).

На каком шаге ломается рукопожатие: ошибка → шаг → причина → что чинить

Каждая ошибка TLS привязана к шагу: до ServerHello ломаются версии и шифры, на Certificate — цепочка, срок и имя, на Finished — целостность обмена. Коды Chromium — из net_error_list.h, алертов — из RFC 8446 и RFC 6066:

Что видно Шаг Причина Что чинить
ERR_SSL_VERSION_OR_CIPHER_MISMATCH (-113); алерт handshake_failure (40) или protocol_version (70); curl 35 ClientHello → ServerHello Нет общей версии или набора: сервер только TLS 1.0/1.1 или только RSA/3DES; сертификат ECDSA при RSA-наборах у клиента ssl_protocols TLSv1.2 TLSv1.3; наборы с ECDHE и AEAD
ERR_SSL_PROTOCOL_ERROR (-107); OpenSSL «wrong version number»; curl 35 Любой: пришло не то, что ожидалось На 443 отвечает не TLS (HTTP открытым текстом, чужой сервис) либо прокси или DPI вмешался в поток Проверить порт через openssl s_client; исключить middlebox
ERR_CERT_AUTHORITY_INVALID (-202); OpenSSL verify 18/19/20/21; curl 60 Certificate — построение цепочки Самоподписанный сертификат, неизвестный корень, не отправлен промежуточный сертификат Добавить intermediate после leaf; выпустить сертификат у публичного CA
ERR_CERT_DATE_INVALID (-201); алерт certificate_expired (45); OpenSSL verify 10; curl 60 Certificate — срок Сертификат истёк или ещё не действует; сбиты часы клиента («Our clock is wrong» в комментарии Chromium) Перевыпустить, настроить автопродление; на клиенте — время
ERR_CERT_COMMON_NAME_INVALID (-200); OpenSSL verify 62; curl 60 Certificate — имя В SAN нет запрошенного имени: сайт по IP, забытый поддомен, сертификат по умолчанию из-за отсутствия SNI Перевыпустить с нужными SAN; проверить, что клиент шлёт SNI
ERR_SSL_UNRECOGNIZED_NAME_ALERT (-159); алерт unrecognized_name (112) ClientHello → ServerHello (SNI) Сервер отвергает неизвестные имена в SNI (RFC 6066 допускает fatal-алерт вместо сертификата по умолчанию) Добавить server_name в конфигурацию; проверить default_server
ERR_CONNECTION_CLOSED (-100) или ERR_CONNECTION_RESET (-101) во время TLS; curl 35 «Connection reset by peer» Любой шаг до Finished, без алерта Сервер или узел по пути молча закрыл (FIN) или сбросил (RST) сокет: DPI по SNI, антивирус, упавший воркер curl --trace-time из разных сетей; tcpdump на сервере

Сертификатные ошибки (-200…-207) возникают, когда сервер уже прислал всё, что должен, — проблема в данных. Ошибки -107 и -113 означают, что до сертификата не дошло. Симптомы самого частого кода — в статье про ERR_SSL_VERSION_OR_CIPHER_MISMATCH.

Как проверить TLS-рукопожатие вручную: openssl, curl, nmap

Три инструмента закрывают диагностику: openssl s_client, curl -v и nmap ssl-enum-ciphers. Порядок действий:

  1. Подключитесь через openssl s_client с -servername (SNI) и посмотрите Certificate chain, строку «New, TLSv1.3, Cipher is …» и «Verify return code»: 0 — порядок, 10 — истёк, 18/19 — самоподписанный, 20/21 — нет промежуточного сертификата.
  2. Повторите с -tls1_2, затем с -tls1_3. Если один из вариантов даёт «alert handshake failure» или «alert protocol version», проблема в ssl_protocols или наборах шифров, а не в сертификате.
  3. Запустите curl -v и найдите, после какого сообщения обмен прервался: после Client hello (1) — версия, шифры или DPI; после Certificate (11) — цепочка или имя; после Finished (20) — прикладной уровень.
  4. Просканируйте сервер nmap --script ssl-enum-ciphers -p 443: скрипт перебирает версии и наборы и ставит оценки от A до F. Всё ниже TLS 1.2 и оценки C и ниже — кандидаты на отключение.
$ openssl s_client -connect example.com:443 -servername example.com \
    -tls1_3 -alpn h2 </dev/null
# пример вывода (сокращён): цепочка leaf -> intermediate -> root
Certificate chain
 0 s:CN = example.com
   i:C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
   v:NotBefore: Jul 29 22:10:08 2026 GMT; NotAfter: Oct 27 22:17:21 2026
 1 s:C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
   i:C = US, O = SSL Corporation, CN = SSL.com TLS Transit ECC CA R2
 2 s:C = US, O = SSL Corporation, CN = SSL.com TLS Transit ECC CA R2
   i:C = US, O = SSL Corporation, CN = SSL.com TLS ECC Root CA 2022
---
Server Temp Key: X25519, 253 bits
SSL handshake has read 4030 bytes and written 400 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
ALPN protocol: h2
Verify return code: 0 (ok)

«Server Temp Key: X25519» — группа эфемерного обмена (тот самый key_share), «4030 bytes» — размер полёта сервера, в основном цепочка. На истёкшем сертификате здесь будет «Verify return code: 10 (certificate has expired)», без промежуточного — «21 (unable to verify the first certificate)», при отсутствии общего набора шифров — «alert handshake failure … SSL alert number 40» и «no peer certificate available».

$ curl -v --tls-max 1.2 https://example.com/ -o /dev/null
# пример вывода: TLS 1.2, в скобках - тип сообщения по RFC 5246
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN),  TLS handshake, Server hello (2):
* TLSv1.2 (IN),  TLS handshake, Certificate (11):
* TLSv1.2 (IN),  TLS handshake, Server key exchange (12):
* TLSv1.2 (IN),  TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN),  TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305 / X25519

$ curl -v --tlsv1.3 https://badssl.com/ -o /dev/null
# пример вывода: сервер без TLS 1.3 - обрыв сразу после Client hello
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN),  TLS alert, handshake failure (552):
curl: (35) OpenSSL/3.0.13: error:0A000410:SSL routines::
    sslv3 alert handshake failure

Без --tls-max тот же сервер ответит по TLS 1.3: после Server hello (2) одним полётом пойдут Encrypted Extensions (8), Certificate (11), CERT verify (15) и Finished (20), а после рукопожатия — Newsession Ticket (4). Коды выхода curl делят ошибки на две группы: 35 — «проблема где-то в SSL/TLS-рукопожатии» (версии, шифры, обрыв), 60 — «сертификат сервера признан негодным» (цепочка, срок, имя). «Server finished (14)» — это ServerHelloDone, не путать с Finished (20).

$ nmap -sV --script ssl-enum-ciphers -p 443 example.com
# пример вывода из документации скрипта (сокращён)
| ssl-enum-ciphers:
|   TLSv1.0:
|     ciphers:
|       TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048) - C
|     warnings:
|       64-bit block cipher 3DES vulnerable to SWEET32 attack
|   TLSv1.2:
|     ciphers:
|       TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
|_  least strength: C

Отчёт — готовый список на отключение: секция TLSv1.0 целиком, 3DES с SWEET32, наборы TLS_RSA_* без forward secrecy. Оценки, по документации nmap, следуют шкале Qualys SSL Labs.

Сколько стоит рукопожатие в миллисекундах и как его ускорить

Цена рукопожатия измеряется в RTT: полный TLS 1.2 добавляет к каждому новому соединению два оборота сети, TLS 1.3 — один, и это поверх одного RTT на TCP. При условном RTT 50 мс первый байт ответа по TLS 1.2 приходит не раньше 200 мс плюс время сервера, по TLS 1.3 — 150 мс, при 0-RTT — 100 мс.

Фаза RTT При RTT 50 мс Переменная curl -w
DNS 0 из кеша, иначе 1+ 0–50 мс time_namelookup
TCP 1 50 мс time_connect − time_namelookup
TLS 1.2, полное 2 100 мс time_appconnect − time_connect
TLS 1.3, полное или возобновление 1 (0 для данных в 0-RTT) 50 мс time_appconnect − time_connect
Запрос → первый байт (TTFB) 1 + сервер 50 мс + сервер time_starttransfer − time_appconnect

Разложить соединение на фазы умеет сам curl: переменные -w отсчитываются от старта, длительность фазы — разность соседних значений:

$ cat > fmt.txt <<'EOF'
dns=%{time_namelookup}
tcp=%{time_connect}
tls=%{time_appconnect}
ttfb=%{time_starttransfer}
total=%{time_total}
EOF
$ curl -s -o /dev/null -w @fmt.txt https://example.com/
# пример вывода (секунды, накопительно от старта)
dns=0.012
tcp=0.061      # TCP занял 0.049 с - один RTT ~49 мс
tls=0.114      # TLS занял 0.053 с - один RTT, значит TLS 1.3
ttfb=0.171     # сервер ответил через 0.057 с после рукопожатия
total=0.173

# с --tls-max 1.2 фаза TLS удвоится: два RTT вместо одного

Если фаза TLS заметно больше одного-двух RTT, причины обычно четыре: тяжёлая цепочка (RSA-4096 с несколькими промежуточными звеньями может не уместиться в начальное окно TCP; ECDSA и короткие цепочки решают это), поход клиента за OCSP-ответом посреди рукопожатия (лечится OCSP stapling; роль OCSP снижается — Let's Encrypt отключил свои ответчики 6 августа 2025 года и перешёл на CRL), отсутствие возобновления сессий и HelloRetryRequest из-за несовпадения кривых у клиента и сервера.

# nginx: минимальный современный блок TLS
server {
    listen 443 ssl;
    http2 on;
    # leaf первым, затем промежуточные
    ssl_certificate     /etc/ssl/example.com/fullchain.pem;
    ssl_certificate_key /etc/ssl/example.com/privkey.pem;

    # умолчание с nginx 1.23.4; задаём явно
    ssl_protocols TLSv1.2 TLSv1.3;
    # TLS 1.2: только ECDHE + AEAD (умолчание - HIGH:!aNULL:!MD5);
    # наборы TLS 1.3 этой директивой не управляются
    ssl_ciphers ECDHE+AESGCM:ECDHE+CHACHA20;

    # возобновление: ~4000 сессий на 1 МБ (умолчание таймаута 5m)
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets on;

    ssl_stapling        on;          # OCSP stapling, умолчание off
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/ssl/example.com/chain.pem;

    # ssl_early_data on;   # 0-RTT: по умолчанию off
}

Самый большой выигрыш даёт отсутствие рукопожатий: HTTP/2 держит одно соединение для всех запросов к хосту, keep-alive в HTTP/1.1 переиспользует соединение, возобновление сессий срезает RTT тем, кто подключается заново.

Как за рукопожатием следит Пруфен

Разовая проверка через openssl показывает состояние на одну минуту с одной машины; за рукопожатием нужно следить постоянно и из разных сетей. HTTP-проверка в Пруфене раскладывает время ответа на фазы — DNS, TCP, TLS, сервер, загрузка — и показывает TLS отдельной цифрой по каждой из 20 точек мониторинга в России, Беларуси, Казахстане и Испании с интервалом от 2 минут: рост фазы TLS из одного региона при здоровом сервере — признак вмешательства в трафик по пути. SSL-проверка следит за сроком сертификата и цепочкой и предупреждает в Email, Telegram или Макс; если ресурс так и не поднялся, подключается голосовой звонок.

Частые вопросы

Почему ошибка называется «SSL handshake failed», если SSL давно нет? По инерции: SSL 2.0 и 3.0 были предшественниками TLS в 1995–1996 годах, TLS 1.0 вышел в 1999-м, и библиотеки сохранили старые имена — отсюда «SSL routines» и «sslv3 alert» в OpenSSL даже для TLS 1.3. Под «SSL handshake failed» понимают любой срыв рукопожатия, чаще всего — алерт handshake_failure (40) из-за отсутствия общего набора шифров или версии.

Сколько RTT занимает TLS-рукопожатие вместе с TCP? До первого байта HTTP-ответа: 1 RTT на TCP плюс 2 RTT на TLS 1.2 или 1 RTT на TLS 1.3, плюс 1 RTT на сам запрос — итого 4 или 3 RTT. При возобновлении TLS 1.3 с 0-RTT остаётся 2 RTT. В HTTP/3 поверх QUIC рукопожатие TLS 1.3 встроено в установку соединения, отдельного TCP-оборота нет.

Почему браузер считает сертификат недействительным, а openssl s_client пишет «Verify return code: 0»? Чаще всего из-за имени хоста: без -verify_hostname s_client проверяет только цепочку и срок. Вторая причина — разные хранилища доверия: openssl берёт системные корни, браузер — свои. Третья — дополнительные проверки браузера: Certificate Transparency, длина срока действия, известные перехватывающие сертификаты (в Chromium — коды -207, -217).

Была ли статья полезна?

Начните мониторинг прямо сейчас

Бесплатный тариф — до 5 проверок. 150 ₽ на баланс при регистрации.

Создать аккаунт