Learning Platform
Глоссарий Troubleshooting
Урок 04.01 · 22 мин
Начальный
HTTPSTLSCertificatesSNIPKI

HTTPS и TLS: что зашифровано, что нет, и как клиент это проверяет

Junior часто думает, что HTTPS — это «магия», которая «делает всё безопасным». На самом деле HTTPS — это просто HTTP, обёрнутый в TLS-туннель. Понять, как это работает на уровне пакетов и сертификатов, важно: это объясняет, почему requests.get(url, verify=False) — небезопасно, почему атакующий в кафешке видит, на какой сайт вы зашли, и почему сертификат внезапно перестал работать в 03:00 утра в выходной.

В этом уроке разберём: что такое TLS, что именно скрывается за шифрованием, как устроен handshake, как клиент проверяет цепочку сертификатов, и как ведут себя curl и Python.


HTTPS = HTTP + TLS

HTTPS — это не «другой протокол». Это та же самая семантика HTTP (метод, путь, headers, body, статус), но передаётся не в открытом TCP-сокете, а внутри TLS-туннеля. TLS (Transport Layer Security) — это криптографический протокол, который сидит между TCP и HTTP.

Стек HTTP vs HTTPS

HTTPS добавляет один слой между TCP и HTTP

HTTP (plain)HTTP-запросы и ответы идут поверх TCP. Все байты видны на проводе. Кто перехватит трафик -- увидит URL, headers, body
TCPТранспортный уровень: гарантированная доставка, упорядоченность, контрольные суммы
IPСетевой уровень: маршрутизация пакетов от источника к получателю
HTTPSHTTP-запросы и ответы идут внутри TLS-туннеля. На проводе видны только зашифрованные байты
TLS 1.3Криптографический слой. Шифрует body HTTP-сообщений, аутентифицирует сервер (через сертификат), обеспечивает целостность (MAC)
TCPТот же самый TCP. TLS просто дополнительный слой
IPТа же самая маршрутизация. IP-адреса видны в любом случае

Когда вы делаете https://api.github.com/users/octocat, клиент:

  1. Резолвит api.github.com в IP-адрес через DNS (обычно не зашифровано, если у вас нет DoH/DoT).
  2. Открывает TCP-соединение на порт 443.
  3. Запускает TLS handshake (об этом ниже).
  4. После handshake — внутри туннеля отправляет обычный HTTP-запрос: GET /users/octocat HTTP/1.1\r\nHost: api.github.com\r\n....
  5. Получает HTTP-ответ внутри того же туннеля.

Для приложения это прозрачно: библиотека requests принимает URL, остальное за неё делает OpenSSL.


Что зашифровано, что нет

Это важный момент. HTTPS не делает вас невидимым в сети.

Что видит атакующий на проводе

Шифрование защищает содержимое запроса, но не сам факт соединения

IP-адресаИсточник и назначение видны всегда. Любой провайдер/администратор сети знает, что вы соединились с 140.82.121.6
ПортыTCP-порты не зашифрованы. 443 = HTTPS, 80 = HTTP, 22 = SSH. Видно тип сервиса
SNIServer Name Indication -- TLS-расширение, в котором клиент сообщает серверу, к какому домену хочет подключиться. В TLS 1.3 SNI всё ещё в clear-text (ECH/Encrypted Client Hello развивается, но не повсеместен). Атакующий видит api.github.com даже без расшифровки трафика
Размер пакетовМетаданные трафика: сколько байт прилетело, в какой момент. По размерам пакетов можно догадаться о действии (логин vs загрузка большого файла vs idle)
ВремяTiming атаки: момент запроса, длительность ответа. Можно понять, какой endpoint дёрнули
URL pathСкрыто. Внутри TLS. /users/octocat не видно на проводе
HeadersСкрыто. Authorization, cookies, User-Agent -- всё внутри TLS
BodyСкрыто. POST-данные, JSON-payload, файлы -- всё зашифровано
Response bodyСкрыто. Тело ответа сервера зашифровано

Практический вывод для data engineer: даже если вы тянете данные через HTTPS, ваш сетевой администратор и провайдер знают, с каким API вы общаетесь (по SNI и IP). Они не знают, что именно вы запросили и какие данные получили. Это всё, что вы получаете от HTTPS на сетевом уровне.

NOTE

SNI в clear-text — это решение совместимости. Без SNI один IP мог обслуживать только один TLS-сертификат, что было неприемлемо для shared hosting. ECH (Encrypted Client Hello) шифрует SNI, но требует поддержки и на клиенте, и на сервере. Cloudflare уже поддерживает ECH; в curl 8.10+ есть --ech флаг. К 2026 году ECH постепенно становится default-ом, но в production-API он встречается редко.


TLS handshake: упрощённая модель

TLS 1.3 handshake пошагово: от ClientHello до 0-RTT

Когда клиент устанавливает HTTPS-соединение, происходит TLS handshake. В TLS 1.3 он стал проще и быстрее (1-RTT вместо 2-RTT в TLS 1.2). Разберём шаги.

TLS 1.3 handshake (упрощённо)

ClientHello, ServerHello + сертификат, проверка, переход к шифрованному обмену

Client
Server
ClientHelloServerHello + Certificate + FinishedFinished (encrypted)GET /users/octocat HTTP/1.1 (encrypted)HTTP/1.1 200 OK + body (encrypted)

Ключевые моменты:

  • Diffie-Hellman key exchange: клиент и сервер обмениваются публичными ключами, и каждый независимо вычисляет общий secret. Атакующий, наблюдающий за каналом, не может вычислить этот secret даже зная оба публичных ключа (это математически защищено). Этот secret используется как ключ для симметричного шифрования (AES-GCM).
  • Forward secrecy: каждое соединение использует новый эфемерный DH-ключ. Если атакующий завтра украдёт приватный ключ сервера, он не сможет расшифровать вчерашние записанные сессии. В TLS 1.3 forward secrecy обязательна (DH-only cipher suites).
  • CertificateVerify: сервер подписывает данные handshake’а своим приватным ключом. Клиент проверяет подпись публичным ключом из сертификата. Так клиент убеждается, что разговаривает именно с владельцем сертификата, а не с MITM.

В TLS 1.2 handshake был сложнее — 2 RTT, отдельный ChangeCipherSpec. В TLS 1.3 убрали много legacy: только AEAD-cipher’ы (AES-GCM, ChaCha20-Poly1305), только DH, только сильные хеши.


Сертификаты: цепочка доверия

Сертификаты и PKI: кто кому доверяет в интернете

Сертификат — это документ в формате X.509, в котором написано:

  • Кому принадлежит (subject): CN=api.github.com, O=GitHub, Inc.
  • Кто выдал (issuer): CN=DigiCert TLS Hybrid ECC SHA384 2020 CA1.
  • Публичный ключ сервера.
  • Срок действия (notBefore, notAfter).
  • Subject Alternative Names (SAN): список доменов, для которых валиден (api.github.com, *.github.com).
  • Подпись CA — гарантия, что CA подтверждает, что владелец публичного ключа — это действительно владелец домена.

Один сертификат сам по себе ничего не доказывает. Нужна цепочка доверия (chain of trust).

Цепочка сертификатов

Сервер -> промежуточный CA -> корневой CA. Корневой CA лежит в root store клиента

Leaf certificateСертификат конкретного сервера: api.github.com. Подписан промежуточным CA. Срок действия обычно 90 дней (Let's Encrypt) или 1 год (комм. CA). С 2026 года планируется максимум 47 дней по политике CA/Browser Forum
signed by
Intermediate CADigiCert TLS Hybrid ECC SHA384 2020 CA1 -- промежуточный центр сертификации. Подписан корневым DigiCert Root. Промежуточный нужен, чтобы корневой ключ не использовался напрямую (его компрометация = катастрофа)
signed by
Root CADigiCert Global Root G2 -- корневой сертификат. Self-signed (сам себя подписал). Лежит в root store: системном или браузерном. Mozilla, Apple, Google поддерживают свои списки доверенных корневых CA

Когда клиент получает leaf-сертификат, он:

  1. Читает поле issuer -> ищет в цепочке (которую обычно прислал сам сервер) промежуточный CA.
  2. Проверяет подпись leaf-сертификата публичным ключом промежуточного CA.
  3. Читает issuer промежуточного -> находит корневой CA в локальном root store.
  4. Проверяет подпись промежуточного корневым.
  5. Если всё сходится — цепочка доверия валидна.

Root store — это набор предустановленных корневых сертификатов. У каждой системы свой:

  • macOS / iOS: System Keychain.
  • Windows: Certificate Manager (certmgr.msc).
  • Linux: /etc/ssl/certs/ (как правило, ca-certificates package).
  • Firefox: свой root store, не системный.
  • Python (requests / httpx): пакет certifi (берёт root store из Mozilla NSS).
WARNING

certifi обновляется отдельно от системы. Если вы не обновляли Python-пакеты полгода, у вас старый root store. Если в это время Mozilla отозвала какой-то скомпрометированный CA, ваш requests всё ещё будет ему доверять. Команда pip install -U certifi периодически — хорошая привычка.


Что именно проверяет клиент

Помимо подписи в цепочке, клиент проверяет несколько других вещей. Если хоть одна проверка падает — TLS handshake fails.

Что проверяет клиент при валидации сертификата
ПодписьКаждый сертификат в цепочке подписан следующим. Корневой self-signed и должен быть в root store клиента. Если CA не в store -- UNKNOWN_ISSUER
HostnameДоменное имя (api.github.com) должно совпадать с одним из SAN-записей сертификата. CN (Common Name) больше не используется для матчинга начиная с RFC 6125 и большинством клиентов с 2017 года
СрокТекущее время должно быть между notBefore и notAfter. Сервер с просроченным сертом = CERTIFICATE_EXPIRED. Локальные часы расходятся с реальностью -- клиент видит просрочку даже если серт валидный
RevocationСертификат не должен быть отозван CA. Проверяется через OCSP (Online Certificate Status Protocol) или CRL (Certificate Revocation List). Многие клиенты по умолчанию НЕ проверяют revocation (производительность), но Firefox и Chrome делают через OCSP-stapling
Key usageВ сертификате есть extension Key Usage и Extended Key Usage. Для веб-сервера должны быть Digital Signature и serverAuth. Если серт сделан для email -- он не валиден для веб-сайта
ConstraintsBasic Constraints -- может ли этот сертификат подписывать другие? Для leaf-сертификата CA:FALSE -- он не может быть промежуточным

Если что-то не сходится — клиент по идее должен прервать соединение. Но… не все клиенты это делают одинаково строго.


Как curl и Python проверяют сертификаты

Посмотрим на конкретных инструментах. Сравним поведение и опции.

# curl: по умолчанию проверяет всё
curl https://api.github.com/users/octocat
# Если что-то не так:
# curl: (60) SSL certificate problem: certificate has expired

# Verbose handshake
curl -v https://api.github.com 2>&1 | grep -E "TLS|SSL|certificate"
# *  SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
# *  Server certificate:
# *   subject: CN=*.github.com
# *   start date: Mar  6 00:00:00 2025 GMT
# *   expire date: Mar  7 23:59:59 2026 GMT
# *   issuer: C=US; O=DigiCert Inc; CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1
# *   SSL certificate verify ok.

# Опасный флаг: отключение verify
curl -k https://expired.badssl.com/  # -k = --insecure
# Соединится, но это плохо
# requests: по умолчанию verify=True (через certifi)
import requests
r = requests.get("https://api.github.com/users/octocat")
print(r.status_code)  # 200

# Опасный вариант -- отключение verify
r = requests.get("https://api.github.com", verify=False)
# Сработает + warning: InsecureRequestWarning

# Правильный вариант -- указать свой CA-bundle (для корп-CA)
r = requests.get("https://internal.corp.local", verify="/etc/ssl/corp-ca.pem")

# httpx (0.28+): то же самое, но другое API
import httpx
client = httpx.Client(verify="/etc/ssl/corp-ca.pem")
r = client.get("https://internal.corp.local")

Что происходит «под капотом» в Python:

  • requests использует urllib3, который использует ssl модуль stdlib, который использует OpenSSL (системный или встроенный в Python).
  • Root store берётся из certifi.where() — путь к pem-bundle, который ставится с пакетом certifi.
  • При verify=False urllib3 отключает проверку через ssl.CERT_NONE. Клиент примет ЛЮБОЙ сертификат, в том числе самоподписанный от любого MITM.

Почему verify=False — катастрофа

Это самый частый антипаттерн в junior-коде. Кажется: «у меня тестовая среда, серт самоподписанный, выключу проверку — и поедем». Что в реальности происходит?

MITM-атака на verify=False

Без проверки сертификата клиент верит любому серверу

Your Python script
Attacker (MITM)
Real API server
ClientHello (думает что говорит с api.example.com)ServerHello + FAKE certificateGET /api/data + Authorization: Bearer XXX (encrypted к атакующему)GET /api/data + Authorization: Bearer XXX (forwarded)200 OK + sensitive data200 OK + data (forwarded)

С verify=False вы:

  • Отдаёте Bearer-токены, API-ключи, cookies — атакующему.
  • Получаете данные, которые атакующий может изменить на лету.
  • Не имеете никакой защиты от MITM. HTTPS превращается в неаутентифицированное шифрование.

Правильные альтернативы:

  • Корпоративный CA: получите corp-ca.pem, скормите verify="/path/to/corp-ca.pem".
  • Самоподписанный серт для теста: сгенерируйте серт, положите в локальный bundle, укажите путь.
  • Локалка / dev: используйте mkcert — он создаёт локальный CA и добавляет его в root store.
DANGER

verify=False допустимо ровно в одном случае: вы пишете diagnostics-скрипт для проверки expired/broken сертификатов на собственной инфраструктуре, и понимаете риск. В production-коде, в скриптах ETL, в anything-что-ходит-в-интернет — это бомба замедленного действия.


Когда сертификат внезапно перестаёт работать

Дебаг TLS на практике: openssl s_client, SSL Labs, типичные ошибки

В практике data engineer вы столкнётесь с этими ошибками. Стоит знать заранее, что они значат.

ОшибкаПричинаЧто делать
CERTIFICATE_VERIFY_FAILEDСертификат серверa не валиден (просрочен, не от доверенного CA, неверный hostname)Проверьте curl -v https://... — он покажет конкретную причину
HOSTNAME_MISMATCHСерт выдан на foo.com, а вы стучитесь на bar.comПроверьте URL. Возможно, лоадбалансер обслуживает несколько доменов
CERTIFICATE_EXPIREDnotAfter < nowСвяжитесь с командой API. Серт нужно обновить
UNKNOWN_ISSUERКорневой CA не в root store клиентаДля корп-CA: добавьте в verify=.... Для public — обновите certifi
SELF_SIGNED_CERT_IN_CHAINПромежуточный или leaf — self-signedСкорее всего dev-окружение. Добавьте этот серт в bundle
OCSP_RESPONSE_UNAUTHORIZEDOCSP-сервер CA вернул, что не знает про этот сертРедко. Часто временное у CA. Подождите

Попробуй сам

Поиграйте с настоящими сертификатами и сравните вывод.

# 1. Посмотреть полный handshake
curl -v https://api.github.com 2>&1 | less

# 2. Достать сертификат сервера в pem
echo | openssl s_client -showcerts -servername api.github.com -connect api.github.com:443 2>/dev/null \
  | openssl x509 -inform pem -noout -text | head -40
# Видно: subject, issuer, validity, SANs, fingerprint

# 3. Проверить срок до истечения
echo | openssl s_client -servername api.github.com -connect api.github.com:443 2>/dev/null \
  | openssl x509 -noout -dates
# notBefore=Mar  6 00:00:00 2025 GMT
# notAfter=Mar  7 23:59:59 2026 GMT

# 4. Намеренно сломанные сайты (для тестов)
curl https://expired.badssl.com/        # CERTIFICATE_EXPIRED
curl https://self-signed.badssl.com/    # SELF_SIGNED_CERT_IN_CHAIN
curl https://wrong.host.badssl.com/     # HOSTNAME_MISMATCH

# 5. То же из Python
python3 -c "
import requests, certifi
print('certifi bundle path:', certifi.where())
print('---')
r = requests.get('https://api.github.com')
print('OK status:', r.status_code)
"

Дальше — попробуйте сами:

  • Откройте certifi.where() и посмотрите, сколько корневых CA в bundle (grep -c BEGIN /path/to/cacert.pem). Обычно 130-150.
  • Найдите там DigiCert Global Root G2 — это родитель цепочки для github.com.
  • Запустите свой requests.get(url, verify=False), посмотрите warning. Поймите, почему его пишут.

Проверка знанийKnowledge check
Друг говорит: "HTTPS защищает всё -- провайдер не видит, на каких сайтах я был". Какие части этого утверждения неверны? Что именно видит провайдер для HTTPS-трафика, а что не видит?
ОтветAnswer
Утверждение неверно: провайдер видит ДОВОЛЬНО МНОГО, просто не содержимое. Провайдер видит: (1) IP-адреса источника и назначения -- это маршрутизация, шифровать их нельзя. (2) SNI в clear-text внутри ClientHello -- это домен (api.github.com), к которому подключаются. В TLS 1.3 SNI всё ещё открытый, ECH (Encrypted Client Hello) развивается, но не повсеместен. (3) Размеры пакетов и timing -- метаданные трафика. По ним можно угадать тип активности (скачивание видео vs обычный браузинг). (4) DNS-запросы, если у пользователя нет DoH/DoT -- открытое имя домена в plain-text перед TLS. Не видит: URL path (/users/octocat), HTTP headers (Authorization, Cookie), request body, response body. Практический вывод для DE: HTTPS не делает вас анонимным в сети, он защищает только содержимое. Для скрытия SNI нужен ECH, для DNS -- DoH/DoT, для метаданных -- Tor или VPN. И ещё важный нюанс: корпоративный MITM (firewall с собственным CA в системном root store) видит ВСЁ -- он терминирует TLS, расшифровывает, и заново шифрует к серверу. На корпоративном лэптопе считайте, что HTTPS прозрачен для DLP/SIEM.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Вы открыли https://api.github.com/users/octocat в браузере, провайдер пишет логи DPI. Что провайдер видит из этого запроса?

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

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

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

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