Как работает HTTPS: протокол TLS, сертификат и пример на Python

Серверная стойка с индикаторами в полутёмном зале Технологии

HTTPS работает так: браузер и сервер сначала устанавливают безопасное соединение по протоколу TLS, а потом передают внутри него обычный HTTP. На старте они договариваются об общем ключе, и браузер проверяет сертификат сайта; после этого данные передаются в зашифрованном виде, и подменить их незаметно нельзя.

Содержание
  1. Что такое HTTPS и чем он отличается от HTTP
  2. SSL и TLS: почему два названия
  3. Какие данные защищает HTTPS и что видит провайдер
  4. Как устанавливается соединение в TLS 1.3
  5. Чем TLS 1.3 отличается от TLS 1.2
  6. Каким ключом шифруются данные после установки соединения
  7. Сертификат и цепочка доверия
  8. Пример на Python: свой HTTPS-сервер и проверка сертификата
  9. Шаг 1. Самоподписанный сертификат
  10. Шаг 2. HTTPS-сервер на http.server и ssl
  11. Шаг 3. Клиент без своего CA: CERTIFICATE_VERIFY_FAILED
  12. Шаг 4. Клиент с cafile: версия TLS, шифр и поля сертификата
  13. HSTS: как сайт запрещает браузеру ходить по HTTP
  14. Ошибки сертификата в браузере и что они значат
  15. Как перевести сайт на HTTPS
  16. Частые вопросы
  17. Можно ли использовать HTTPS бесплатно?
  18. Видит ли провайдер, какие страницы сайта я открываю?
  19. Может ли фишинговый сайт работать по HTTPS?
  20. Защищает ли HTTPS от всех атак?
  21. Почему HTTPS-соединение устанавливается дольше, чем HTTP?
  22. Можно ли в Python просто отключить проверку сертификата?

Что такое HTTPS и чем он отличается от HTTP

HTTP (Hypertext Transfer Protocol) передаёт запросы и ответы в открытом виде. HTTPS (Hypertext Transfer Protocol Secure) использует тот же формат сообщений, но кладёт их в канал TLS. По стандарту HTTP такой канал считается защищённым, только если сервер доказал, что действует от имени домена из адреса, а весь обмен данными с ним зашифрован и защищён от изменения.

Протокол HTTP Протокол HTTPS
Порт по умолчанию TCP 80 TCP 443
Данные в сети в открытом виде зашифрованы протоколом TLS
Проверка сервера нет сертификат и цифровая подпись
Адрес сайта начинается с http:// https://

Адреса http://site.ru и https://site.ru для браузера считаются двумя разными источниками (origin) с раздельными пространствами имён. Какие протоколы работают на порту 443, разобрано в статье про TCP и UDP.

SSL и TLS: почему два названия

Протокол SSL (Secure Sockets Layer) создала компания Netscape, и TLS (Transport Layer Security) вырос из спецификации SSL 3.0. Различия между ними настолько заметны, что TLS и SSL 3.0 напрямую не совместимы. SSL 3.0 запрещён к использованию, TLS 1.0 и 1.1 официально выведены из употребления. Остались TLS 1.2 (стандарт 2008 года) и TLS 1.3 (2018 год).

Название «SSL-сертификат» осталось от старого протокола. И в TLS 1.2, и в TLS 1.3 сервер предъявляет сертификат формата X.509v3, так что «SSL-сертификат» и «TLS-сертификат» означают одно и то же. А вот если сервер сайта в 2026 году предлагает протокол SSL, это повод его перенастроить.

Какие данные защищает HTTPS и что видит провайдер

Ноутбук с размытым кодом на экране и значком замка в адресной строке

Протокол TLS даёт три свойства: сервер всегда проходит проверку подлинности, передаваемые данные после установки соединения видны только двум сторонам, и злоумышленник не сможет изменить их по дороге незаметно. Защита работает на уровне канала: сервер получает данные пользователей уже в расшифрованном виде.

Владелец сети или провайдер видит Не видит
IP-адрес сервера путь страницы: /account/orders
домен сайта в SNI, если браузер не использует ECH параметры запроса после ?
DNS-запрос, если DNS не зашифрован заголовки, включая cookie и токены
объём и время передачи пакетов тело запроса и ответа: данные форм, пароли, номера карт
сертификат сервера в TLS 1.2 сертификат сервера в TLS 1.3

SNI (Server Name Indication) придуман для серверов, у которых на одном IP-адресе несколько сайтов со своими сертификатами: по домену сервер выбирает нужный. Браузер пишет домен сайта в первое сообщение соединения открытым текстом. Спрятать его может расширение ECH (Encrypted Client Hello), стандарт которого вышел в 2026 году. С ECH браузер отправляет два ClientHello, один внутри другого: внешний идёт открытым и обычно содержит в SNI публичное имя фронтового сервера, а внутренний, с настоящим доменом, зашифрован ключом этого сервера. Но и с ECH домен можно узнать по незашифрованному DNS-запросу или по IP-адресу сервера. По длине и времени пакетов протокол тоже уязвим: длину передаваемых данных он не прячет, хотя стороны могут дополнять записи.

Как устанавливается соединение в TLS 1.3

Две роботизированные руки пожимают друг другу ладони над сетевыми кабелями

Установка соединения (handshake, по-русски его называют рукопожатием) в версии 1.3 укладывается в один круг «туда и обратно» (1-RTT): клиент отправил первое сообщение, получил ответ сервера и уже может слать HTTP-запрос. Порядок такой:

  1. ClientHello: клиент отправляет случайное число, список поддерживаемых версий протокола, список шифров, домен сервера (SNI) и свою публичную часть ключа ECDHE для одной или нескольких групп (расширение key_share).
  2. ServerHello: сервер выбирает версию и шифр и присылает свою публичную часть ключа в той же группе. Теперь каждая сторона вычисляет общий секрет у себя, и всё, что идёт дальше, шифруется ключами рукопожатия.
  3. EncryptedExtensions: ответы на расширения клиента, которые не нужны для выбора ключей.
  4. Certificate: сертификат сервера и цепочка промежуточных, если они есть.
  5. CertificateVerify: подпись всех предыдущих сообщений закрытым ключом, парным к публичному ключу из сертификата. Клиент обязан проверить эту подпись.
  6. Finished: код аутентификации (MAC) от всего обмена. Он подтверждает, что обе стороны получили одинаковые ключи и никто не изменил сообщения по дороге.
  7. Клиент проверяет сертификат, подпись и Finished сервера, отправляет свой Finished, и после этого идут данные приложения: HTTP-запрос и ответ. Исключение составляет режим 0-RTT: если у сторон уже есть общий ключ (PSK), например от прошлого соединения, клиент может отправить часть данных в первом же сообщении.
Читайте также:  REST API

Если сервер не поддерживает ни одну из групп, для которых клиент прислал ключ, он отвечает HelloRetryRequest, и клиент начинает заново с подходящим key_share. Это лишний круг. Если общих параметров нет совсем, сервер прерывает рукопожатие сигналом handshake_failure или insufficient_security.

Чем TLS 1.3 отличается от TLS 1.2

TLS 1.2 TLS 1.3
Кругов до первого HTTP-запроса два (по схеме полного рукопожатия) один
Обмен ключами в том числе статический RSA: клиент шифрует 48-байтовый секрет публичным ключом из сертификата только (EC)DHE или PSK, статический RSA и DH убраны
Прямая секретность не у всех обменов ключами у всех обменов с публичным ключом
Сертификат сервера до ChangeCipherSpec, то есть до включения шифрования зашифрован, как и всё после ServerHello
Алгоритмы шифрования данных разные, включая устаревшие только AEAD

Прямая секретность (forward secrecy) значит вот что: если закрытый ключ сервера украдут после того, как сеанс завершился, ключ этого сеанса от кражи не раскроется. Как устроена подпись RSA, которую TLS 1.3 по-прежнему поддерживает для сертификатов, есть в статье про RSA шифрование.

Каким ключом шифруются данные после установки соединения

Два компьютера на столе и запечатанный конверт между ними

Асимметричная криптография в HTTPS работает в начале: обмен ECDHE даёт общий секрет, подпись в CertificateVerify подтверждает сервер. Из общего секрета функция HKDF выводит отдельные ключи для рукопожатия и для данных приложения, и дальше для шифрования данных используется симметричный алгоритм.

В протоколе версии 1.3 все алгоритмы шифрования относятся к классу AEAD (authenticated encryption with associated data): одна операция и шифрует запись, и проверяет её целостность. Стандарт требует от реализаций поддерживать набор TLS_AES_128_GCM_SHA256, а TLS_AES_256_GCM_SHA384 и TLS_CHACHA20_POLY1305_SHA256 рекомендованы. Поддержка обмена ключами на кривой secp256r1 обязательна, на X25519 рекомендована. Режим GCM, тег аутентификации и то, почему nonce нельзя повторять, разобраны в статье про шифрование AES.

Мы проверили живой сайт example.com 24 октября 2026 года: на подключение из Python сервер выбрал версию протокола TLS 1.3 и набор TLS_AES_256_GCM_SHA384. Тот же набор выберет и наш локальный сервер ниже.

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

Цепочка из трёх металлических звеньев крупным планом

Цифровой сертификат связывает домен сайта с публичным ключом и подписан центром сертификации (CA). Доверие к нему строится цепочкой:

  • корневой сертификат лежит в хранилище операционной системы или браузера, он и есть точка доверия;
  • промежуточный в простом случае подписан корневым, им центр подписывает сертификаты сайтов;
  • сертификат сайта подписан промежуточным.

Сервер отправляет свой сертификат первым, за ним промежуточные, и каждый следующий подписывает предыдущий. Корневой можно не присылать: у клиента он и так есть. Для каждого звена клиент проверяет подпись вышестоящим ключом, срок действия, отзыв и совпадение издателя. Затем сверяет домен: по стандарту HTTP его ищут в поле subjectAltName, а поле CN (common name) для этого использовать не должны.

Такая проверка подтверждает только то, что сервер отвечает за этот домен. Бесплатные сертификаты Let’s Encrypt, например, относятся к типу DV (domain validation): центр проверяет владение доменом. HTTPS в адресе поэтому не гарантирует честность владельца сайта, и фишинговый сайт на своём домене тоже может работать по HTTPS.

Пример на Python: свой HTTPS-сервер и проверка сертификата

Крупный план разъёма сетевого кабеля

Проверено на Windows 10: Python 3.14.3 (модуль ssl собран с OpenSSL 3.0.18), утилита openssl 3.5.9 из Git Bash. Все файлы лежат в одной папке, команды запускаются из неё. На других версиях Python и OpenSSL тексты ошибок могут отличаться.

Шаг 1. Самоподписанный сертификат

Создаём ключ на кривой P-256 и самоподписанный сертификат на 30 дней для домена localhost:

mkdir https-demo && cd https-demo
MSYS_NO_PATHCONV=1 openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
  -noenc -keyout localhost.key -out localhost.crt -days 30 \
  -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"

Команда печатает строку ----- и создаёт два файла: localhost.crt и закрытый ключ localhost.key. Флаг -noenc оставляет ключ без пароля. В OpenSSL 3.5.9, на котором мы проверяли, старый флаг -nodes с тем же действием помечен в справке как устаревший. Расширение subjectAltName добавляем потому, что клиент ищет домен именно там.

Читайте также:  OAuth 2.0: что это и как работает протокол авторизации

Приставка MSYS_NO_PATHCONV=1 нужна в Git Bash: без неё он принимает /CN=localhost за путь и подставляет папку установки. Команда тогда падает с ошибкой:

req: subject name is expected to be in the format /type0=value0/type1=value1/type2=... where characters may be escaped by \. This name is not in that format: 'C:/Program Files/Git/CN=localhost'

Шаг 2. HTTPS-сервер на http.server и ssl

Сервер отвечает на GET строкой текста и добавляет заголовок HSTS. Сохраните как server.py и запустите в первом окне терминала:

import ssl
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


class HelloHandler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def do_GET(self):
        body = "Привет по HTTPS\n".encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Strict-Transport-Security", "max-age=300")
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, format, *args):
        print("сервер получил:", self.requestline)


tls_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
tls_context.load_cert_chain("localhost.crt", "localhost.key")

httpd = ThreadingHTTPServer(("127.0.0.1", 8443), HelloHandler)
httpd.socket = tls_context.wrap_socket(httpd.socket, server_side=True)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
print("HTTPS-сервер слушает 127.0.0.1:8443")
input("Enter остановит сервер\n")
HTTPS-сервер слушает 127.0.0.1:8443
Enter остановит сервер

Слушающий сокет мы обернули в TLS, и каждое принятое соединение модуль ssl сам проводит через рукопожатие. Сервер работает в фоновом потоке, а главный поток ждёт Enter: после нажатия программа завершается вместе с потоком. Строку protocol_version = "HTTP/1.1" мы добавили после первого прогона: без неё http.server отвечает в формате HTTP/1.0.

Шаг 3. Клиент без своего CA: CERTIFICATE_VERIFY_FAILED

Во втором окне запустим errors.py. Он подключается дважды: сначала с настройками по умолчанию, затем с нашим сертификатом, но под чужим доменом:

import socket
import ssl

cases = [
    ("без своего CA", None, "localhost"),
    ("чужое имя", "localhost.crt", "example.local"),
]

for title, cafile, domain in cases:
    context = ssl.create_default_context(cafile=cafile)
    try:
        with socket.create_connection(("127.0.0.1", 8443)) as tcp_link:
            with context.wrap_socket(tcp_link, server_hostname=domain):
                print(title, "-> соединение установлено")
    except ssl.SSLCertVerificationError as error:
        print(title, "->", error.reason)
        print("   ", error.verify_message)
без своего CA -> CERTIFICATE_VERIFY_FAILED
    self-signed certificate
чужое имя -> CERTIFICATE_VERIFY_FAILED
    Hostname mismatch, certificate is not valid for 'example.local'.

create_default_context() без cafile берёт системные корневые сертификаты (в Windows из хранилищ CA и ROOT), включает проверку цепочки и домена. Нашего файла среди них нет, отсюда self-signed certificate. Во втором случае цепочка в порядке, но example.local не совпадает с localhost из сертификата.

Отключать проверку через verify_mode = ssl.CERT_NONE мы бы не стали: в этом режиме клиент принимает почти любой сертификат, в том числе просроченный и выданный неизвестно кем, и соединение остаётся открытым для атаки «человек посередине». Правильнее передать клиенту тот файл, которому он должен доверять. Если всё же отключаете проверку, сначала выставьте check_hostname = False, иначе Python упадёт с ValueError: Cannot set verify_mode to CERT_NONE when check_hostname is enabled.

Шаг 4. Клиент с cafile: версия TLS, шифр и поля сертификата

Сохраните как client.py и запустите во втором окне, пока сервер работает:

import socket
import ssl

context = ssl.create_default_context(cafile="localhost.crt")

with socket.create_connection(("127.0.0.1", 8443)) as tcp_link:
    with context.wrap_socket(tcp_link, server_hostname="localhost") as encrypted_link:
        print("версия:", encrypted_link.version())
        print("шифр:", encrypted_link.cipher())
        fields = encrypted_link.getpeercert()
        print("кому выдан:", fields["subject"])
        print("кем выдан:", fields["issuer"])
        print("действует до:", fields["notAfter"])
        print("имена:", fields["subjectAltName"])

        request = "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
        encrypted_link.sendall(request.encode("ascii"))
        response = b""
        while chunk := encrypted_link.recv(4096):
            response += chunk

print(response.decode("utf-8"))
версия: TLSv1.3
шифр: ('TLS_AES_256_GCM_SHA384', 'TLSv1.3', 256)
кому выдан: ((('commonName', 'localhost'),),)
кем выдан: ((('commonName', 'localhost'),),)
действует до: Nov 23 10:28:49 2026 GMT
имена: (('DNS', 'localhost'),)
HTTP/1.1 200 OK
Server: BaseHTTP/0.6 Python/3.14.3
Date: Sat, 24 Oct 2026 10:29:27 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 24
Strict-Transport-Security: max-age=300

Привет по HTTPS

cipher() возвращает кортеж из имени шифра, версии протокола и числа бит секрета. У самоподписанного поля «кому выдан» и «кем выдан» совпадают: он сам себе издатель. А HTTP-запрос мы записали в защищённый сокет обычным текстом, и сервер ответил тоже текстом. В окне сервера появится строка сервер получил: GET / HTTP/1.1, а дата и срок действия у вас будут свои.

Если cafile передан, create_default_context() загружает корневые из этого файла вместо системных. Для запросов к другим сайтам в интернете заведите отдельный контекст без cafile.

HSTS: как сайт запрещает браузеру ходить по HTTP

Даже если сайт перенаправляет с HTTP на HTTPS, самый первый запрос по http:// уходит открытым, и злоумышленник может его перехватить. Заголовок Strict-Transport-Security закрывает это для следующих визитов: получив его по HTTPS, браузер на max-age секунд запоминает сайт и сам переписывает http:// на https://, а порт 80 на 443. Директива includeSubDomains распространяет правило на поддомены.

Читайте также:  Что такое webhook и как принять вебхук на Python

Для такого сайта браузер не предлагает пройти дальше при ошибке проверки: соединение просто обрывается. Заголовок, пришедший по обычному HTTP, браузер игнорирует, и сервер не должен его туда ставить. Первый визит остаётся уязвимым. Для него стандарт предлагает встроенный в браузер список сайтов с HSTS, по аналогии со встроенными корневыми сертификатами.

Включайте HSTS, когда HTTPS уже работает на всём сайте и на поддоменах, если ставите includeSubDomains: пока max-age не истёк, браузер не пустит пользователя на сайт, если с HTTPS что-то не так. Мы бы начинали с небольшого значения, как max-age=300 в нашем сервере, и потом поднимали до года (max-age=31536000). Как добавить заголовок в http.server и nginx, показано в статье про HTTP заголовки.

Ошибки сертификата в браузере и что они значат

При ошибке проверки Chrome показывает страницу с предупреждением о незащищённом подключении и кодом ошибки, в который входит одно из имён ниже. В правой колонке сообщение, которое в похожем случае вернул наш Python-клиент.

Код в Chrome Что случилось Возможные причины Python 3.14 (OpenSSL 3.0)
ERR_CERT_AUTHORITY_INVALID подпись центра, которому браузер не доверяет самоподписанный сертификат; подмена сертификата; законный сертификат от центра, которого нет в хранилище self-signed certificate
ERR_CERT_COMMON_NAME_INVALID домен в сертификате не совпадает с адресом сервер отдаёт чужой сертификат; сеть Wi-Fi перенаправляет на свою страницу входа; перехват трафика Hostname mismatch, certificate is not valid for '...'
ERR_CERT_DATE_INVALID сертификат просрочен или ещё не начал действовать ошибка настройки сервера; неверные часы на компьютере; старый сертификат в руках атакующего certificate has expired

Отдельно стоит неполная цепочка: сервер отдаёт только свой файл без промежуточного. Мы собрали тестовую цепочку «корневой, промежуточный, сертификат сайта» и дали клиенту только корневой. Когда сервер присылал только свой, Python отвечал unable to get local issuer certificate, а с файлом, где за ним идёт промежуточный, соединение устанавливалось. На сервере поэтому подключают файл полной цепочки: сертификат сайта, за ним промежуточные.

Как перевести сайт на HTTPS

  1. Получите сертификат. Let’s Encrypt выдаёт DV-сертификаты бесплатно на 90 дней. Настройте автоматическое продление.
  2. Подключите на веб-сервере полную цепочку и закрытый ключ, проверьте работу сайта по https://.
  3. Настройте постоянный редирект 301 со всех адресов http:// на те же адреса с https://.
  4. Замените в коде и базе данных адреса вида http://ваш-домен на https://ваш-домен, чтобы страницы сайта не загружали картинки и скрипты по HTTP.
  5. Когда всё работает, добавьте заголовок Strict-Transport-Security.

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

Можно ли использовать HTTPS бесплатно?

Да. Let’s Encrypt не берёт плату за сертификаты. Они действуют 90 дней и подтверждают владение доменом, сертификатов с проверкой организации (OV, EV) этот центр не выдаёт.

Видит ли провайдер, какие страницы сайта я открываю?

Путь страницы, параметры, заголовки и содержимое провайдер не видит: эти данные идут внутри TLS. Видны IP-адрес сервера, объём и время трафика, а домен сайта виден в SNI, если не используется ECH, и в DNS-запросе, если DNS не зашифрован.

Может ли фишинговый сайт работать по HTTPS?

Может. Похожий домен с собственным сертификатом браузер откроет без ошибки, поэтому смотрите на сам адрес.

Защищает ли HTTPS от всех атак?

Нет. HTTPS защищает данные при передаче между браузером и сервером: шифрование и проверка целостности. От фишинга на похожем домене, уязвимостей в коде сайта и анализа объёма трафика он не защищает, а данные на самом сервере хранятся уже без защиты протокола.

Почему HTTPS-соединение устанавливается дольше, чем HTTP?

Перед первым запросом нужно рукопожатие TLS. В TLS 1.3 это один круг до сервера и обратно, в TLS 1.2 по схеме полного рукопожатия два. Если сервер не принял группу ключа клиента, добавляется ещё круг на HelloRetryRequest.

Можно ли в Python просто отключить проверку сертификата?

Можно: сначала check_hostname = False, затем verify_mode = ssl.CERT_NONE. Только такой клиент уже не отличит настоящий сервер от подставного: данные шифруются, но неизвестно для кого. Для своего или тестового сервера передайте его сертификат или сертификат своего CA через cafile, как в шаге 4.

Оцените статью
bestprogrammer.ru
Добавить комментарий