Перейти к содержимому
RootPoligon

HTTPS: имя, сертификат, срок

Чтение и практика ~75 мин

вы называете, какую из проверок HTTPS не прошёл запрос — достижимость адреса и порта, доверие к цепочке, имя в сертификате или срок действия; различаете роли имени из URL (DNS, SNI, проверка сертификата, заголовок Host); не принимаете -k за ремонт и выбираете решение, зная цену и откат.

Лаборатория и подготовка — архив один для всех пяти глав. Перед новым сценарием остановите прежний через Ctrl+C.

Что нового

Раньше запрос шёл так: имя → адрес → соединение → HTTP. У HTTPS между соединением и HTTP появляется рукопожатие TLS, и клиент проверяет сервер, прежде чем отправить хоть байт HTTP:

имя ──▶ адрес:порт ──▶ TLS: SNI, сертификат, проверки ──▶ HTTP: Host ──▶ приложение
 (DNS)    (TCP)                 (участок 3)                (участок 4)

Какие роли играет имя из URL. Одно и то же имя notes.lab.test используется четыре раза:

  1. DNS превращает его в адрес. curl --resolve ИМЯ:ПОРТ:АДРЕС подменяет только это.
  2. SNI — поле в начале рукопожатия: клиент сообщает серверу, какое имя ему нужно, чтобы сервер, обслуживающий много имён, выбрал нужный сертификат. По документации curl, имя для SNI берётся только из URL.
  3. Проверка сертификата. Клиент проверяет, что сертификат подписан доверенной CA и что он выпущен на имя из URL (страница curl о сертификатах: «crafted for the server name provided in the URL»). Имя ищется в списке имён сертификата (SAN, subjectAltName), а не в «владельце» (subject).
  4. Заголовок Host — часть HTTP-запроса, уже внутри защищённого канала. curl -H 'Host: …' меняет только его.

Поэтому для HTTPS «-H Host не помогает»: документация curl прямо говорит, что для HTTPS заголовка Host недостаточно; в лаборатории это показано запуском.

Сроки. У сертификата есть период действия (notBefore … notAfter). Клиент сравнивает его с текущим временем своих часов. Страница curl о сертификатах про даты не говорит; что просроченный сертификат отвергается, в этой главе — наблюдение запуском (curl 8.5.0, OpenSSL 3.0.13), и формулировка ошибки зависит от версии.

Про -k / --insecure. Флаг отключает проверку сертификата целиком: и цепочки, и имени, и срока. Для диагностики («жив ли сервер за сертификатом?») его можно использовать один раз и сказать об этом вслух; приёмкой ремонта он быть не может: запрос проходит и до ремонта, и после.

Код выхода curl 60 — «сертификат сервера не прошёл проверку»; причина — в сообщении. Формулировки зависят от версии curl и библиотеки TLS; в главе — из curl 8.5.0.

Инструменты (порядок выбираете сами):

python3 lab/netlab.py fetch --cacert CA URL    # настоящий curl; учебная CA доверена только этому вызову (в системе не прописывается)
python3 lab/netlab.py fetch --verbose …        # что curl успел узнать о сертификате до отказа
python3 lab/netlab.py certinfo URL             # что отдаёт сервер: субъект, имена (SAN), издатель, срок, время проверки; вердикта нет
curl --noproxy '*' --resolve ИМЯ:ПОРТ:АДРЕС --cacert CA [-H 'Host: …'] URL   # то же без fetch, если нужен свой набор ключей curl
openssl s_client -connect АДРЕС:ПОРТ -servername ИМЯ -CAfile CA          # рукопожатие и код проверки цепочки
openssl s_client … -verify_hostname ИМЯ                                  # то же, но имя проверяется (по умолчанию — нет!)
openssl verify -CAfile CA [-verify_hostname ИМЯ] [-attime СЕКУНДЫ] ФАЙЛ  # проверка файла сертификата; -attime — «как если бы» время было другим
python3 lab/netlab.py config / logs --source tls / logs --source app     # владелец службы: конфигурация и журналы

Путь к учебной CA лаборатория печатает при запуске и в config. Системные часы, файл hosts, хранилище доверия и браузер не меняются нигде: «если бы время было другим» делает -attime, а не перевод часов.

Подготовка

Нужна команда openssl (проверьте openssl version). Из папки курса, в первом окне: python3 lab/netlab.py start ch5. Лаборатория создаёт ключи и сертификаты во временной папке и удаляет её при остановке (Ctrl+C); порт HTTPS — 18443, только 127.0.0.1. Если порт занят или openssl нет — лаборатория откажет и ничего не изменит. Остальное — во втором окне.

Ситуация

Суббота, 17 октября, 15:00. «Заметки» переехали с имени notes-old.lab.test на notes.lab.test и теперь работают по HTTPS: https://notes.lab.test:18443/api/notes. Пользователи и скрипты получают ошибку сертификата. Дежурный Пётр:

Порт открыт, сервер отвечает, сертификат свежий — я смотрел дату, срок ещё не вышел. Значит, клиенты придираются. Добавим всем -k, а тем, кто не может, — заголовок Host: notes-old.lab.test: заработает.

Воспроизведите симптом (путь к CA — в первом окне):

python3 lab/netlab.py fetch --cacert /tmp/…/ca.pem https://notes.lab.test:18443/api/notes

Мини-задача

Задача: выяснить, какая проверка HTTPS не пройдена и почему, и вернуть работоспособный проверенный путь пользователя, не потеряв заметки. Готовой цепочки команд нет: инструменты названы выше, порядок выбираете сами.

До любого наблюдения, кроме повтора симптома, запишите:

  1. Не меньше двух гипотез, какая проверка не пройдена (одна — слова Петра); для каждой — что подтвердило бы её и что бы исключило.
  2. Предсказание: что вы увидите в выводе, если верна ваша главная гипотеза.
  3. Одно наблюдение, которое различает гипотезы. Выполните его, запишите вывод и то, какие гипотезы оно исключило.

Затем: причина одним предложением и две строки вывода (сообщение клиента и то, что отдаёт сервер).

Разбор мини-задачи

Откройте, когда записали свои ответы

Гипотезы.

ГипотезаЕсли вернаЧто исключает
Адрес или порт недоступныкод 7 или тайм-аут, нет рукопожатияcurl дошёл до проверки сертификата: код 60, в --verbose — SSL connection using TLSv1.3
Клиент не доверяет CAцепочка не проверяется: «unable to get local issuer certificate»openssl s_client -CAfile CA без -verify_hostname: Verify return code: 0 (ok) — цепочка в порядке
Срок истёкnotAfter в прошлом; «certificate has expired»в certinfo notAfter позже времени проверки; сообщение клиента про имя, не про срок
Сертификат выпущен не на это имяв SAN нет notes.lab.test; клиент называет имяв SAN — notes.lab.test (тогда причина другая)

Различающее наблюдение — сравнить имя из URL с именами в сертификате, который отдаёт сервер. certinfo (или openssl s_client … | openssl x509 -noout -ext subjectAltName) показывает DNS:notes-old.lab.test; в URL — notes.lab.test. Срок в порядке (notAfter позже времени проверки — это видно в самом выводе certinfo); цепочка в порядке (строка Verify return code: 0 (ok) в выводе s_client), хотя s_client имя не проверяет — с -verify_hostname notes.lab.test строка будет Verify return code: 62 (hostname mismatch). Это строка о результате проверки, а не код завершения команды: без -verify_return_error s_client продолжает обмен и после ошибки проверки и завершается с кодом 0 (проверено запуском; с -verify_return_error обрыв рукопожатия и код 1).

Причина. Сервер отдаёт сертификат, выпущенный на notes-old.lab.test; клиент требует сертификат на notes.lab.test и отказывается (код 60). Адрес, порт и цепочка в порядке: приложение живо — по имени notes-old.lab.test тот же сервер отвечает 200 (fetch --cacert … https://notes-old.lab.test:18443/api/notes). Журнал приложения при отказе пуст: до HTTP не дошло, Host никто не прочитал.

Слова Петра. «Сертификат свежий» — верно и неважно: срок — одна проверка, имя — другая. -H 'Host: notes-old.lab.test' не помогает: проверяется имя из URL, а не заголовок (код 60 остаётся). -k даёт 200 — потому что отключена проверка, из-за которой всё и обнаружилось.

Развилка: 15:30, что делать

Сертификат на сервере ведёте вы; клиенты — разные команды. Решения (порядок случайный):

  • А. Добавить -k во все скрипты и приложения.
  • Б. Передать клиентам заголовок Host: notes-old.lab.test (-H).
  • В. Выпустить и поставить сертификат на имя notes.lab.test: python3 lab/netlab.py set cert_name=notes.lab.test.
  • Г. Временно перевести клиентов на старый адрес https://notes-old.lab.test:18443/… — сертификат ему подходит. Это восстановление доступности, но не завершение переезда: у переезда другой признак готовности (см. «Приёмка»).
  • Д. Добавить учебную CA в хранилище доверия компьютера.
  • Е. Перезапустить сервер (остановить start и запустить заново).
  • Ж. Передать разработчикам: «HTTPS не работает».

Перед ремонтом можно один раз посмотреть список заметок с -k — только как диагностику, сказав об этом вслух — и запомнить число заметок: после ремонта оно не должно измениться.

Для выбранного решения: что увидит пользователь, что сломается у тех, кто ходил по прежнему пути, как вернуть назад.

Последствия
  • А. Вредное: проверка выключена совсем (цепочка, имя, срок); подмена сервера станет незаметной. «Работает» и до, и после — ничего не доказывает. Откат — убрать флаг в каждом клиенте, о которых мы, скорее всего, забудем.
  • Б. Бесполезное: для HTTPS проверяется имя из URL; код 60 остаётся (проверено запуском).
  • В. Допустимо, устраняет причину: 200 с включённой проверкой, заметки на месте. Цена: сертификат на одно имя — notes-old.lab.test теперь не проходит проверку, кто ещё ходит по старому имени, получит код 60. Откат: set cert_name=notes-old.lab.test. Можно рассмотреть сертификат с обоими именами; в лаборатории он не смоделирован.
  • Г. Допустимая временная мера, восстанавливающая доступность: по notes-old.lab.test запрос проходит с включённой проверкой (200, три заметки). Но переезд не завершён: по новому имени notes.lab.test по-прежнему код 60, и приёмка по новому адресу (ниже) не выполнена. Цена: правка всех клиентов, потом их возврат; старое имя по сюжету уходит. Условие отката — сертификат на новое имя поставлен (В) и новый адрес проверен.
  • Д. Вредное и бесполезное: цепочка не была причиной (Verify return code: 0), а доверие к своей CA на всём компьютере — решение для всех сайтов этой CA; в лаборатории намеренно не меняется.
  • Е. Бесполезное: сертификат тот же; заметки в лабе возвращаются к трём.
  • Ж. Бесполезное: причина названа и исправима вами.

Приёмка

  • Гипотезы и предсказание записаны до наблюдений; названо различающее наблюдение и что оно исключило.
  • Причина названа с двумя строками вывода: имя из URL и имя в сертификате.
  • Роль имени из URL названа верно: что меняет --resolve, что -H Host, что не меняет ни то ни другое (SNI и проверку).
  • Выбранное решение называет цену и откат.
  • Завершение переезда (окончательная приёмка). Новый адрес пользователя с включённой проверкой — fetch --cacert … https://notes.lab.test:18443/api/notes — 200, X-App-Version: 3.0, JSON, три заметки на месте; запись через этот канал — 201 (тогда четыре).
  • Если выбрана только временная мера Г: запишите отдельно, что доступность восстановлена (то же по notes-old.lab.test: 200 с проверкой, три заметки), а переезд не завершён — окончательная приёмка по новому адресу остаётся открытым пунктом с условием его закрытия. Старый адрес не заменяет новый.
  • Не принято за ремонт: -k, -H Host, обращение по адресу вместо имени, доверие CA для всего компьютера.

Подсказки

Подсказка 1 — направление

У HTTPS клиент проверяет сервер ещё до отправки запроса. Какие проверки он делает — и о какой из них говорит сообщение curl? Что проверил Пётр, а что — нет?

Подсказка 2 — нужное наблюдение

Посмотрите, какой сертификат отдаёт сервер: на какие имена он выпущен и на какой срок (certinfo или openssl s_client + openssl x509). Сравните это с именем в URL и со временем проверки. Если сомневаетесь, проверяет ли s_client имя, — запустите его с -verify_hostname и без.

Подсказка 3 — разбор

Адрес и порт в порядке, цепочка в порядке (Verify return code: 0), срок в порядке (notAfter позже времени проверки), но сертификат выпущен на notes-old.lab.test, а клиент ходит на notes.lab.test — отсюда код 60 и «no alternative certificate subject name matches». Допустимы В (сертификат на нужное имя; цена — старое имя) и Г как временная мера: Г восстанавливает доступность, но переезд завершает только новый адрес с проверкой. А отключает проверку, Б не меняет проверяемого имени, Д чинит то, что не сломано, Е не меняет сертификат.

Второй вариант

Воскресенье, 18 октября, 09:00. Сертификат на нужное имя notes.lab.test поставлен. Но у пользователей снова код 60, и сообщение другое. Пётр: «Вчера это лечилось выпуском сертификата на notes.lab.test — сделайте то же самое». Перезапустите лабораторию со сценарием ch5b (python3 lab/netlab.py start ch5b) и действуйте так же, как в основной части.

Условия приёмки те же, плюс три:

  1. Объясните, почему ремонт из основной части здесь не помогает — и покажите запуском (что изменится, если «сделать то же самое»).
  2. Сравните срок действия сертификата со временем проверки, не меняя часов компьютера: назовите оба момента и покажите, как узнать, что было бы при другом времени (-attime).
  3. Объясните, почему -k здесь «работает» и почему приёмкой быть не может.

Решения (порядок случайный; ключи — для действий после диагностики):

  • А. Повторить прежний ремонт: set cert_name=notes.lab.test.
  • Б. Перевести часы компьютера назад, «чтобы сертификат был действителен».
  • В. Заменить сертификат на действующий: set cert_valid=fresh.
  • Г. Добавить -k в клиенты на время.
  • Д. Перенести клиентов на https://notes-old.lab.test:18443/….
  • Е. Срочно поставить действующий сертификат (В) и завести напоминание о сроке следующего.
Подсказка 1 — направление

Имя в сертификате вчера уже исправили. Что ещё есть у сертификата, кроме имён? Посмотрите на сообщение клиента — о чём оно?

Подсказка 2 — нужное наблюдение

Запишите период действия сертификата (notBefore … notAfter) и время проверки по часам компьютера и сравните. Проверьте себя, не трогая часы: openssl verify -attime с моментом внутри периода и после него.

Подсказка 3 — разбор

Имя верное (SAN — notes.lab.test), цепочка верна, но notAfter в прошлом: срок истёк, клиент отказывает (код 60, «certificate has expired»). Повтор А ничего не меняет: сертификат уже на нужное имя. Допустимо В (действующий сертификат; цена — замена и перезагрузка, откат — вернуть прежний, что бессмысленно) и Е (то же плюс предотвращение повторения). Б — вредное: часы одни на всю систему, и истёк срок не только у этого сертификата; Г отключает проверку и скрывает причину; Д не поможет, пока сертификат просрочен для любого имени.

Источники

  • curl, «SSL certificates»: проверка подписи доверенной CA и имени сервера из URL. Про проверку сроков действия страница не говорит — в главе это наблюдение запуском (curl 8.5.0, OpenSSL 3.0.13).
  • Everything curl, «Name resolving»: Host: недостаточно для HTTPS-сервера; имя для SNI берётся только из URL; --resolve подменяет адрес для пары «имя–порт», проверяемое имя остаётся именем из URL.
  • OpenSSL 3.0, «Verification options»: проверка периода действия сертификата и -attime («как если бы» время было другим) — документированы. Там же и в справке openssl-verify — -verify_hostname.
  • OpenSSL 3.0, openssl-s_client: без -verify_return_error диагностический s_client продолжает обмен после ошибки проверки; строка Verify return code — результат проверки, не код завершения команды.
  • Коды в строке Verify return code — 10 (срок истёк), 62 (имя не совпало), 21 (издатель не найден, цепочка не проверена) — и формулировки ошибок curl в главе — наблюдения запуском (curl 8.5.0, OpenSSL 3.0.13), не обещания документации; в других версиях текст может отличаться.

Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.