HTTPS работает так: браузер и сервер сначала устанавливают безопасное соединение по протоколу TLS, а потом передают внутри него обычный HTTP. На старте они договариваются об общем ключе, и браузер проверяет сертификат сайта; после этого данные передаются в зашифрованном виде, и подменить их незаметно нельзя.
- Что такое HTTPS и чем он отличается от HTTP
- SSL и TLS: почему два названия
- Какие данные защищает HTTPS и что видит провайдер
- Как устанавливается соединение в TLS 1.3
- Чем TLS 1.3 отличается от TLS 1.2
- Каким ключом шифруются данные после установки соединения
- Сертификат и цепочка доверия
- Пример на Python: свой HTTPS-сервер и проверка сертификата
- Шаг 1. Самоподписанный сертификат
- Шаг 2. HTTPS-сервер на http.server и ssl
- Шаг 3. Клиент без своего CA: CERTIFICATE_VERIFY_FAILED
- Шаг 4. Клиент с cafile: версия TLS, шифр и поля сертификата
- HSTS: как сайт запрещает браузеру ходить по HTTP
- Ошибки сертификата в браузере и что они значат
- Как перевести сайт на HTTPS
- Частые вопросы
- Можно ли использовать HTTPS бесплатно?
- Видит ли провайдер, какие страницы сайта я открываю?
- Может ли фишинговый сайт работать по HTTPS?
- Защищает ли HTTPS от всех атак?
- Почему HTTPS-соединение устанавливается дольше, чем HTTP?
- Можно ли в 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-запрос. Порядок такой:
- ClientHello: клиент отправляет случайное число, список поддерживаемых версий протокола, список шифров, домен сервера (SNI) и свою публичную часть ключа ECDHE для одной или нескольких групп (расширение
key_share). - ServerHello: сервер выбирает версию и шифр и присылает свою публичную часть ключа в той же группе. Теперь каждая сторона вычисляет общий секрет у себя, и всё, что идёт дальше, шифруется ключами рукопожатия.
- EncryptedExtensions: ответы на расширения клиента, которые не нужны для выбора ключей.
- Certificate: сертификат сервера и цепочка промежуточных, если они есть.
- CertificateVerify: подпись всех предыдущих сообщений закрытым ключом, парным к публичному ключу из сертификата. Клиент обязан проверить эту подпись.
- Finished: код аутентификации (MAC) от всего обмена. Он подтверждает, что обе стороны получили одинаковые ключи и никто не изменил сообщения по дороге.
- Клиент проверяет сертификат, подпись и Finished сервера, отправляет свой Finished, и после этого идут данные приложения: HTTP-запрос и ответ. Исключение составляет режим 0-RTT: если у сторон уже есть общий ключ (PSK), например от прошлого соединения, клиент может отправить часть данных в первом же сообщении.
Если сервер не поддерживает ни одну из групп, для которых клиент прислал ключ, он отвечает 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 добавляем потому, что клиент ищет домен именно там.
Приставка 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 распространяет правило на поддомены.
Для такого сайта браузер не предлагает пройти дальше при ошибке проверки: соединение просто обрывается. Заголовок, пришедший по обычному 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
- Получите сертификат. Let’s Encrypt выдаёт DV-сертификаты бесплатно на 90 дней. Настройте автоматическое продление.
- Подключите на веб-сервере полную цепочку и закрытый ключ, проверьте работу сайта по
https://. - Настройте постоянный редирект 301 со всех адресов
http://на те же адреса сhttps://. - Замените в коде и базе данных адреса вида
http://ваш-доменнаhttps://ваш-домен, чтобы страницы сайта не загружали картинки и скрипты по HTTP. - Когда всё работает, добавьте заголовок
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.








