20 октября 2026 года для пользователей Python опубликовали предупреждение: у восьми промежуточных удостоверяющих центров eMudhra / emSign оказались слабо сгенерированные ключи. Браузеры и системные хранилища начали отзывать доверие почти сразу. Но в экосистеме Python этого недостаточно. Официальный pip доверяет встроенной копии бандла certifi, и, как отдельно подчеркнули в официальном обсуждении, системное хранилище этот встроенный бандл не отключает.
Ниже проверяю реальные окружения на одном Linux-стенде: пакетный pip из Debian 13, интерпретаторы Python 3.15.1 и 3.15.2, пакетный certifi 2026.10.23 и pip 26.3 от 2 ноября. Смотрю, куда указывает certifi.where(), какие корни emSign остались в бандле, и есть ли в интерпретаторе блоклист модуля ssl из CVE-2026-115557. Весь вывод — с этой машины: Python 3.13.5 / 3.15.1 / 3.15.2, pip 25.1.1 / 26.2.2 / 26.3, uv 0.13.7.
Что именно сломалось
Цепочка TLS устроена так: сервер показывает сертификат, его подписывает промежуточный УЦ, того подписывает корневой. Клиент принимает соединение, только если цепочка заканчивается корнем из его списка доверия.
Если закрытый ключ промежуточного УЦ можно восстановить по открытому сертификату, обладатель этого ключа может выпускать сертификаты на чужие имена. В официальном обсуждении инцидента разработчик стандартной библиотеки Python отметил, что у всех восьми затронутых промежуточных ключей значение d оказалось меньше 256.
Для браузера обычно хватает обновления системного хранилища или самого браузера. У Python свой путь. Библиотеки вроде requests по умолчанию берут бандл из пакета certifi. pip этот бандл ещё и встраивает в себя. Обновили ca-certificates в системе — а pip продолжит ходить со своей копией, пока не обновлён сам pip.
Исправления выходили слоями:
20 октября вышел
pip26.2.2: во встроенномcertifiстоит версия 2026.10.19. В объявлении для пользователей Python именно эту версиюcertifiназвали минимально рекомендованной (релиз 2026.10.18 снял затронутые корни, следующий вернул совместимые).21 октября вышли Python 3.15.1, 3.14.9 и 3.13.17 с этим
pipвнутри.26 октября вышли Python 3.15.2, 3.14.10, 3.13.18, 3.12.16 и 3.11.18. В модуль
sslдобавили блоклист скомпрометированных промежуточных ключей (CVE-2026-115557): он срабатывает при проверке цепочки, даже если в бандле ещё лежит соответствующий корень. В этих же выпусках закрыли обход фильтра вtarfile(CVE-2026-114583).2 ноября вышел
pip26.3: встроенныйcertifiобновлён до 2026.10.23.
Для uv в том же объявлении рекомендовали версию 0.13.6 или новее. Python 3.10 и старше к тому моменту уже не получали исправлений: последним выпуском 3.10 была 3.10.22 от 1 октября 2026 года.
Учебный скрипт
Один файл, стандартная библиотека плюс то, что уже стоит: pip и, если есть, пакет certifi. Сохраните как check_trust.py. Скрипт не ходит в сеть и не подделывает сертификаты. Версию интерпретатора сверяет со списком выпусков от 26 октября, бандл читает как текст, а блоклист ssl ищет по символу _cert_has_distrusted_spki в бинарнике интерпретатора или в расширении _ssl. Этот символ есть в Python 3.15.2 и отсутствует в 3.15.1 — я сравнивал оба файла на стенде.
#!/usr/bin/env python3 """Проверка окружения после инцидента eMudhra / emSign (октябрь 2026).""" from __future__ import annotations import pathlib import sys # Версии интерпретатора, в которых модуль ssl отклоняет # скомпрометированные промежуточные ключи (CVE-2026-115557). SSL_FIXED = { (3, 15, 2), (3, 14, 10), (3, 13, 18), (3, 12, 16), (3, 11, 18), } EMSIGN_MARKERS = ("emSign", "eMudhra", "emudhra") def python_version() -> tuple[int, int, int]: v = sys.version_info return (v.major, v.minor, v.micro) def interpreter_has_spki_denylist() -> tuple[bool, list[str]]: """Ищем символ _cert_has_distrusted_spki в бинарнике интерпретатора или в расширении _ssl. Так можно отличить 3.15.2 от 3.15.1 без подделки сертификатов. """ marker = b"_cert_has_distrusted_spki" checked: list[str] = [] candidates = [pathlib.Path(sys.executable).resolve()] try: import _ssl so = getattr(_ssl, "__file__", None) if so: candidates.append(pathlib.Path(so).resolve()) except ImportError: pass seen: set[pathlib.Path] = set() for path in candidates: if path in seen or not path.is_file(): continue seen.add(path) checked.append(str(path)) if marker in path.read_bytes(): return True, checked return False, checked def emsign_labels(pem_text: str) -> list[str]: labels: list[str] = [] for line in pem_text.splitlines(): if "Label:" in line and any(m in line for m in EMSIGN_MARKERS): label = line.split("Label:", 1)[1].strip().strip('"') labels.append(label) return labels def read_bundle(path: pathlib.Path) -> str: return path.read_text(errors="replace") def main() -> int: ver = python_version() ver_s = ".".join(map(str, ver)) print(f"Python: {ver_s} ({sys.executable})") print(f"ssl denylist expected by version: {'yes' if ver in SSL_FIXED else 'no'}") has_marker, checked = interpreter_has_spki_denylist() print(f"ssl denylist symbol in binary: {'yes' if has_marker else 'no'}") for item in checked: print(f" scanned: {item}") try: import pip from importlib.metadata import version as pkg_version pip_ver = pkg_version("pip") pip_loc = pathlib.Path(pip.__file__).resolve().parent print(f"pip: {pip_ver} ({pip_loc})") except Exception as exc: print(f"pip: unavailable ({exc})") try: import pip._vendor.certifi as vcert where = pathlib.Path(vcert.where()) v_ver = getattr(vcert, "__version__", "?") print(f"pip vendored certifi version attr: {v_ver}") print(f"pip certifi.where(): {where}") if where.is_file(): used = emsign_labels(read_bundle(where)) print("emSign roots in the bundle actually used:") if used: for name in used: print(f" - {name}") else: print(" (none)") vendored_pem = pathlib.Path(vcert.__file__).with_name("cacert.pem") if vendored_pem.exists() and vendored_pem.resolve() != where.resolve(): leftover = emsign_labels(read_bundle(vendored_pem)) print("UNUSED vendored cacert.pem still contains:") if leftover: for name in leftover: print(f" - {name}") else: print(" (none)") except Exception as exc: print(f"pip vendored certifi: unavailable ({exc})") try: import certifi c_ver = getattr(certifi, "__version__", "?") c_where = pathlib.Path(certifi.where()) print(f"standalone certifi: {c_ver} ({c_where})") if c_where.is_file(): used = emsign_labels(read_bundle(c_where)) print("emSign roots in standalone certifi:") if used: for name in used: print(f" - {name}") else: print(" (none)") except ImportError: print("standalone certifi: not installed") return 0 if __name__ == "__main__": raise SystemExit(main())
Запуск:
python check_trust.py
Прогоните его каждым интерпретатором, который реально используете: системным, из менеджера версий, из виртуального окружения.
Что показал стенд
Системный Python 3.13.5 и пакетный pip 25.1.1 из Debian 13:
Python: 3.13.5 (/usr/bin/python3) ssl denylist expected by version: no ssl denylist symbol in binary: no scanned: /usr/bin/python3.13 scanned: /usr/lib/python3.13/lib-dynload/_ssl.cpython-313-x86_64-linux-gnu.so pip: 25.1.1 (/usr/lib/python3/dist-packages/pip) pip vendored certifi version attr: 2025.01.31 pip certifi.where(): /etc/ssl/certs/ca-certificates.crt emSign roots in the bundle actually used: (none) UNUSED vendored cacert.pem still contains: - emSign Root CA - G1 - emSign ECC Root CA - G3 - emSign Root CA - C1 - emSign ECC Root CA - C3 standalone certifi: 2026.10.23 (/usr/local/lib/python3.13/dist-packages/certifi/cacert.pem) emSign roots in standalone certifi: - emSign Root CA - G1
Здесь две важные детали. Первая: where() смотрит в системное хранилище, и корней emSign в нём уже нет. Этот пакетный pip Debian патчит. Вторая: рядом всё ещё лежит неиспользуемый cacert.pem встроенного certifi 2025.01.31 со всеми четырьмя корнями. Если судить только по номеру pip 25.1.1, можно решить, что окружение уязвимо. Если судить только по системному хранилищу, можно пропустить официальный pip в соседнем venv, который этим патчем не пользуется.
Отдельный пакет certifi 2026.10.23 на этой же машине уже оставляет только emSign Root CA - G1. Это не отменяет проверку самого pip: приложения, которые импортируют certifi напрямую, и инструменты, которые возят бандл внутри себя, — разные копии.
Python 3.15.1 — как раз выпуск от 21 октября, в нём уже pip 26.2.2:
Python: 3.15.1 (/home/box/.local/share/uv/python/cpython-3.15.1-linux-x86_64-gnu/bin/python3.15) ssl denylist expected by version: no ssl denylist symbol in binary: no scanned: /home/box/.local/share/uv/python/cpython-3.15.1-linux-x86_64-gnu/bin/python3.15 pip: 26.2.2 (/home/box/.local/share/uv/python/cpython-3.15.1-linux-x86_64-gnu/lib/python3.15/site-packages/pip) pip vendored certifi version attr: 2026.10.19 pip certifi.where(): /home/box/.local/share/uv/python/cpython-3.15.1-linux-x86_64-gnu/lib/python3.15/site-packages/pip/_vendor/certifi/cacert.pem emSign roots in the bundle actually used: - emSign Root CA - G1 - emSign Root CA - C1 - emSign ECC Root CA - C3 standalone certifi: not installed
pip обновлён до минимально рекомендованной версии, но блоклиста в ssl ещё нет. Во встроенном бандле 2026.10.19 остались три корня emSign: сняли emSign ECC Root CA - G3, остальные на месте.
Python 3.15.2 — выпуск от 26 октября. Встроенный pip тот же, 26.2.2, бандл тот же, 2026.10.19, те же три корня. Меняется одно:
Python: 3.15.2 (/home/box/.local/share/uv/python/cpython-3.15.2-linux-x86_64-gnu/bin/python3.15) ssl denylist expected by version: yes ssl denylist symbol in binary: yes scanned: /home/box/.local/share/uv/python/cpython-3.15.2-linux-x86_64-gnu/bin/python3.15 pip: 26.2.2 (/home/box/.local/share/uv/python/cpython-3.15.2-linux-x86_64-gnu/lib/python3.15/site-packages/pip) pip vendored certifi version attr: 2026.10.19 pip certifi.where(): /home/box/.local/share/uv/python/cpython-3.15.2-linux-x86_64-gnu/lib/python3.15/site-packages/pip/_vendor/certifi/cacert.pem emSign roots in the bundle actually used: - emSign Root CA - G1 - emSign Root CA - C1 - emSign ECC Root CA - C3 standalone certifi: not installed
Символ _cert_has_distrusted_spki в бинарнике есть. Это как раз «последний рубеж» из объявления от 26 октября: промежуточные ключи отклоняются при проверке цепочки, даже если корень из бандла ещё не убрали. Сам бандл pip при этом не сжался.
Как закрыть и что перепроверить
На этом стенде я поставил Python 3.15.2 и в отдельном виртуальном окружении обновил pip:
python -m pip install -U 'pip>=26.3'
После обновления:
Python: 3.15.2 (/tmp/emudhra-venv/bin/python) ssl denylist expected by version: yes ssl denylist symbol in binary: yes scanned: /home/box/.local/share/uv/python/cpython-3.15.2-linux-x86_64-gnu/bin/python3.15 pip: 26.3 (/tmp/emudhra-venv/lib/python3.15/site-packages/pip) pip vendored certifi version attr: 2026.10.23 pip certifi.where(): /tmp/emudhra-venv/lib/python3.15/site-packages/pip/_vendor/certifi/cacert.pem emSign roots in the bundle actually used: - emSign Root CA - G1 standalone certifi: not installed
Встроенный certifi стал 2026.10.23, как в журнале изменений pip 26.3, и в бандле остался один корень — тот же, что и в отдельном пакете 2026.10.23 на системном Python. Вместе с блоклистом в ssl это уже полная картина для этого интерпретатора.
Практический порядок такой:
Обновить сам интерпретатор до 3.15.2, 3.14.10, 3.13.18, 3.12.16 или 3.11.18 — в зависимости от ветки. Виртуальные окружения продолжают указывать на тот интерпретатор, с которым их создали: после замены его лучше пересоздать.
В каждом окружении обновить
pipхотя бы до 26.2.2, а лучше до 26.3, и пакетcertifi— до 2026.10.23. В объявлении от 20–21 октября отдельно советовали ставитьpip26.2.2 колесом с фиксированной суммой SHA-256, если текущийpipещё может доверять скомпрометированным ключам.Если пользуетесь
uv, обновите его до 0.13.6 или новее. На стенде уже 0.13.7.Образы контейнеров пересоберите без кеша слоёв, иначе внутри останется старый
pip.
Несколько ловушек, которые на стенде были не гипотетическими. Номер pip сам по себе не говорит, каким бандлом он пользуется: в Debian 13 пакет 25.1.1 смотрит в систему, а официальный 26.2.2 — в свой cacert.pem. Обновлённый пакет certifi не чинит встроенную копию внутри pip. Выпуск 3.15.1 закрывает историю с бандлом pip, но не содержит блоклиста ssl. А Python 3.10 и старше блоклиста в интерпретаторе уже не получат.
Как вы инвентаризируете pip и интерпретатор во всех виртуальных окружениях и образах? Находили ли ещё официальный pip старше 26.2.2 или ветку 3.15.1 без блоклиста в ssl?

