Как на самом деле работают HTTPS и TLS: рукопожатие и сертификаты
Вы добавляете SSL-сертификат, направляете домен на сервер, и появляется замочек. Большинство разработчиков на этом останавливаются. А потом в один день API-клиент падает с UNABLE_TO_VERIFY_LEAF_SIGNATURE, мобильное приложение отклоняет ваш сертификат на старых Android, или балансировщик нагрузки терминирует TLS, и ваше приложение видит всё как обычный HTTP. И внезапно вам нужно знать, что на самом деле происходит между TCP-соединением и первым HTTP-запросом.
Эта статья разбирает, как на самом деле работают HTTPS и TLS: что делает рукопожатие, как проверяются сертификаты, что изменилось в TLS 1.3 и как диагностировать ошибки, с которыми вы реально столкнётесь.
Что на самом деле добавляет HTTPS
HTTPS — это HTTP, передаваемый через TLS-соединение. TLS обеспечивает три гарантии:
- Конфиденциальность — трафик шифруется симметричными ключами, согласованными во время рукопожатия.
- Целостность — каждая запись несёт тег аутентификации, поэтому подмена обнаруживается.
- Аутентификация — сервер доказывает, что владеет закрытым ключом для сертификата, соответствующего запрошенному вами имени хоста.
Обратите внимание, чего в этом списке нет: клиент по умолчанию не аутентифицируется. TLS аутентифицирует сервер; пользователя аутентифицирует ваше приложение.
Рукопожатие TLS 1.2 шаг за шагом
TLS 1.2 всё ещё распространён на старой инфраструктуре и во многих корпоративных прокси, поэтому его стоит понимать. Полное рукопожатие выглядит так:
- ClientHello — клиент отправляет поддерживаемые версии TLS, случайное значение, список шифров и (если применимо) имя хоста SNI.
- ServerHello — сервер выбирает версию и шифр, возвращает своё случайное значение и отправляет свою цепочку сертификатов.
- Обмен ключами — при ECDHE сервер отправляет эфемерный открытый ключ; клиент отправляет свой. Обе стороны выводят один и тот же общий секрет. Сервер подписывает стенограмму рукопожатия закрытым ключом своего сертификата.
- Finished — обе стороны отправляют MAC по стенограмме рукопожатия, подтверждая, что ничего не было подменено.
- Данные приложения — HTTP-запросы и ответы передаются в зашифрованном виде.
Ключевая деталь — эфемерный обмен ключами. При ECDHE новая пара ключей генерируется для каждого соединения. Даже если злоумышленник запишет годы трафика и позже украдёт закрытый ключ сервера, он не сможет расшифровать прошлые сессии. Это свойство называется прямой секретностью (forward secrecy), и это главная причина, по которой статический обмен ключами RSA был объявлен устаревшим.
Как TLS 1.3 изменил правила игры
TLS 1.3 упрощает рукопожатие до одного кругового обхода и полностью убирает устаревшие алгоритмы.
| Аспект | TLS 1.2 | TLS 1.3 |
|---|---|---|
| Круговые обходы (полное рукопожатие) | 2 | 1 |
| Обмен ключами | RSA или ECDHE | Только ECDHE (прямая секретность) |
| Шифры | Отдельно шифр, MAC, обмен ключами | Только AEAD-наборы, например TLS_AES_128_GCM_SHA256 |
| Статический RSA, CBC, RC4 | Разрешены | Удалены |
| Возобновление сессии | Session IDs / tickets | PSK с опцией 0-RTT |
В TLS 1.3 клиент угадывает группу обмена ключами и отправляет свою долю в первом сообщении, так что сервер может завершить согласование ключей сразу. Сертификаты по-прежнему отправляются, но теперь они зашифрованы, а значит пассивные наблюдатели больше не могут увидеть, какое имя хоста обслуживается — его выдаёт только SNI в ClientHello, если не используется Encrypted Client Hello.
Возобновление 0-RTT позволяет возвращающемуся клиенту отправить данные приложения в первом пакете. Это быстро, но данные 0-RTT можно воспроизвести (replay), поэтому никогда не используйте его для неидемпотентных запросов, например платежей.
Сертификаты и проверка цепочки
Сертификат связывает открытый ключ с именем хоста. Проверка — это проход по цепочке:
- Сервер отправляет свой конечный сертификат плюс промежуточные сертификаты.
- Клиент ищет доверенный корневой сертификат в своём хранилище, затем проверяет каждую подпись вверх по цепочке.
- Он проверяет имя хоста по полю Subject Alternative Name (SAN) — Common Name современные клиенты игнорируют.
- Он проверяет сроки действия и статус отзыва (CRL или OCSP, часто со stapling).
Самая частая причина ошибок unable to verify — отсутствующий промежуточный сертификат. Браузеры иногда маскируют это, кэшируя промежуточные сертификаты, но инструменты командной строки и мобильные приложения падают с ошибкой. Всегда отдавайте полную цепочку: сначала конечный, затем промежуточные — и никогда корневой.
Практическая отладка с OpenSSL
Когда что-то ломается, сначала посмотрите, что на самом деле отправляет сервер, прежде чем винить клиента.
# Показать цепочку сертификатов, которую представляет сервер
openssl s_client -connect example.com:443 -servername example.com -showcerts
# Согласовать только TLS 1.3 и вывести выбранный шифр
openssl s_client -connect example.com:443 -tls1_3
# Проверить срок действия сертификата одной командой
echo | openssl s_client -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates -subject -ext subjectAltNameЕсли s_client сообщает verify error:num=21:unable to verify the first certificate, цепочка неполная. Если он сообщает о несовпадении имени хоста, проверьте список SAN — wildcard-сертификаты покрывают только один уровень, поэтому *.example.com соответствует api.example.com, но не a.b.example.com.
Типичные ошибки конфигурации
- Смешанный контент — HTTPS-страница загружает скрипты или изображения по
http://. Браузеры блокируют активный смешанный контент и могут автоматически обновлять пассивный. - TLS терминируется на балансировщике нагрузки — бэкенд видит обычный HTTP. Передавайте
X-Forwarded-Protoи доверяйте ему только от известных прокси, иначе ваше приложение может генерировать неправильные URL перенаправлений. - Включены слабые версии протокола — TLS 1.0 и 1.1 следует отключить. Стремитесь к TLS 1.2 и 1.3.
- Нет HSTS — без заголовка
Strict-Transport-Securityпервый запрос всё ещё можно понизить. Начните с короткогоmax-age, затем увеличьте его, когда убедитесь в стабильности. - Самоподписанные сертификаты в продакшене — нормально для внутреннего тестирования, но они приучают пользователей и клиентов игнорировать предупреждения.
Разумная конфигурация сервера
Для Nginx современная базовая конфигурация выглядит так:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;Позвольте серверу выбирать шифры в TLS 1.3 — предпочтения клиента здесь нормальны, потому что все наборы сильные. Держите session tickets выключенными, если не ротируете ключи тикетов, поскольку утёкший ключ тикета может скомпрометировать возобновление.
FAQ
Шифрует ли HTTPS имя хоста?
По умолчанию нет. Расширение SNI в ClientHello отправляется открытым текстом, чтобы сервер знал, какой сертификат представить. Encrypted Client Hello (ECH) скрывает его, но распространение пока ограничено. Путь и строка запроса всегда шифруются.
Почему мой сертификат работает в браузере, но не в curl или мобильном приложении?
Почти всегда это отсутствующий промежуточный сертификат. Браузеры часто сами загружают или кэшируют промежуточные сертификаты, а более строгие клиенты требуют, чтобы сервер отправлял полную цепочку. Проверьте с помощью openssl s_client -showcerts.
TLS 1.3 всегда быстрее TLS 1.2?
Для новых соединений — да, один круговой обход вместо двух. Для возобновлённых соединений разрыв сокращается, а 0-RTT может сделать TLS 1.3 ещё быстрее ценой риска воспроизведения для неидемпотентных запросов.
Если вы проводите аудит веб-сервера после изменения TLS, анализ логов часто выявляет реальное влияние: сломанные перенаправления, неожиданные всплески 4xx или клиенты, всё ещё обращающиеся по HTTP. Вы можете загрузить access-лог в Nginx log analyzer, чтобы увидеть распределение кодов состояния и найти клиентов, которые так и не перешли на HTTPS.