Python-клиент MAX API: как доверять сертификату Минцифры только в одном приложении
Обновиться до 2025.11.7 или поставить плагин безопасности
В начале октября автор статьи столкнулся с ошибкой ClientConnectorCertificateError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate при подключении к API мессенджера MAX. Проблема возникла из-за того, что сертификат сервера MAX подписан цепочкой Минцифры, которая отсутствует в стандартных наборах корневых сертификатов операционных систем и библиотек. Сервер MAX предоставляет два сертификата: *.max.ru (действителен до 06.06.2027) и промежуточный Russian Trusted Sub CA (действителен до 19.07.2029). Корневой сертификат Russian Trusted Root CA Минцифры сервер не отправляет, полагаясь на его наличие в локальном хранилище клиента. Отсутствие этого сертификата в стандартных наборах Debian, certifi и Windows приводит к ошибке проверки.
Облачные установки закрыты, локальные обновляются вручную
Использование verify=False для обхода проверки сертификата недопустимо, так как токен аутентификации бота MAX передаётся в заголовке Authorization с каждым запросом. Перехват такого запроса злоумышленником позволит получить доступ к токену. Добавление корневого сертификата Минцифры в общесистемное хранилище или хранилище библиотеки certifi также нежелательно. Промежуточный сертификат Russian Trusted Sub CA не имеет расширения Name Constraints, что позволяет ему подтверждать сертификаты для любых доменов. Размещение корня в общем хранилище предоставит этой цепочке возможность подтверждать любой домен для всех приложений, использующих это хранилище. В проекте автора статьи клиенты Telegram (aiogram) и Anthropic (httpx), использующие certifi, остались без изменений. Для ограничения действия сертификата на уровне всей системы существуют другие подходы, описанные в статьях о недоверии к сертификатам Минцифры и архиве на Telegram Business API. Автор статьи отказался от установки корневого сертификата Минцифры в систему или браузер ради одного API. Задача заключалась в том, чтобы сертификат признавал только один клиент, обращающийся к одному хосту, и никто другой.
LoadMaster распределяет трафик между серверами приложений
Решение проблемы заключается в создании отдельного TLS-контекста для клиента MAX. Контекст создаётся с помощью ssl.create_default_context(), который сохраняет системные корневые сертификаты и обязательную проверку. Затем в контекст добавляется файл russian_trusted_sub_ca.pem с помощью ctx.load_verify_locations(cafile=str(_CA_FILE)). Флаг ssl.VERIFY_X509_PARTIAL_CHAIN используется потому, что OpenSSL по умолчанию строит цепочку до самоподписанного корня, которого нет в наличии. Этот флаг позволяет цепочке остановиться на любом сертификате из доверенного хранилища, даже если он не самоподписанный. В данном случае якорем становится промежуточный сертификат Russian Trusted Sub CA. Если файл сертификата отсутствует в образе приложения, клиент продолжит выдавать ошибку. Риски включают возможность того, что любой сертификат из хранилища может стать якорем, так как флаг действует на весь контекст. Также, проверка выше якоря не производится, и клиент не узнает об отзыве промежуточного сертификата, если корень его отозвал. Отслеживание отзыва сертификата необходимо выполнять вручную. Контекст не должен утекать в другие клиенты, так как промежуточный сертификат подписывает сертификаты на любые домены. Для библиотеки httpx используется httpx.Client(verify=ctx), для urllib — urllib.request.urlopen(req, timeout=60, context=ctx). Для библиотеки requests применяется адаптер ContextAdapter, так как verify не принимает контекст. Автор статьи проверил решение на Python 3.13.7 с aiohttp 3.13.3, httpx 0.28.1, requests 2.32.5 и urllib. Проверка отпечатка сертификата при старте приложения позволит своевременно выявлять подмену файла в репозитории. В Python 3.13 create_default_context() уже включает VERIFY_X509_PARTIAL_CHAIN, что может привести к ошибкам в более старых версиях Python, используемых в контейнерах.
Источник