././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692463791.1241834 truststore-0.10.1/LICENSE0000644000000000000000000000207614470171257011777 0ustar00The MIT License (MIT) Copyright (c) 2022 Seth Michael Larson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1724251920.6083498 truststore-0.10.1/README.md0000644000000000000000000000637614661377421012263 0ustar00# Truststore [![PyPI](https://img.shields.io/pypi/v/truststore)](https://pypi.org/project/truststore) [![CI](https://github.com/sethmlarson/truststore/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/sethmlarson/truststore/actions/workflows/ci.yml) Truststore is a library which exposes native system certificate stores (ie "trust stores") through an `ssl.SSLContext`-like API. This means that Python applications no longer need to rely on certifi as a root certificate store. Native system certificate stores have many helpful features compared to a static certificate bundle like certifi: - Automatically update certificates as new CAs are created and removed - Fetch missing intermediate certificates - Check certificates against certificate revocation lists (CRLs) to avoid monster-in-the-middle (MITM) attacks - Managed per-system rather than per-application by a operations/IT team - PyPI is no longer a CA distribution channel 🥳 Right now truststore is a stand-alone library that can be installed globally in your application to immediately take advantage of the benefits in Python 3.10+. Truststore has also been integrated into pip 24.2+ as the default method for verifying HTTPS certificates (with a fallback to certifi). Long-term the hope is to add this functionality into Python itself. Wish us luck! ## Installation Truststore is installed from [PyPI](https://pypi.org/project/truststore) with pip: ```{code-block} shell $ python -m pip install truststore ``` Truststore **requires Python 3.10 or later** and supports the following platforms: - macOS 10.8+ via [Security framework](https://developer.apple.com/documentation/security) - Windows via [CryptoAPI](https://docs.microsoft.com/en-us/windows/win32/seccrypto/cryptography-functions#certificate-verification-functions) - Linux via OpenSSL ## User Guide > **Warning** > **PLEASE READ:** `inject_into_ssl()` **must not be used by libraries or packages** as it will cause issues on import time when integrated with other libraries. > Libraries and packages should instead use `truststore.SSLContext` directly which is detailed below. > > The `inject_into_ssl()` function is intended only for use in applications and scripts. You can inject `truststore` into the standard library `ssl` module so the functionality is used by every library by default. To do so use the `truststore.inject_into_ssl()` function: ```python import truststore truststore.inject_into_ssl() # Automatically works with urllib3, requests, aiohttp, and more: import urllib3 http = urllib3.PoolManager() resp = http.request("GET", "https://example.com") import aiohttp http = aiohttp.ClientSession() resp = await http.request("GET", "https://example.com") import requests resp = requests.get("https://example.com") ``` If you'd like finer-grained control or you're developing a library or package you can create your own `truststore.SSLContext` instance and use it anywhere you'd use an `ssl.SSLContext`: ```python import ssl import truststore ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) import urllib3 http = urllib3.PoolManager(ssl_context=ctx) resp = http.request("GET", "https://example.com") ``` You can read more in the [user guide in the documentation](https://truststore.readthedocs.io/en/latest/#user-guide). ## License MIT ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692463791.1241834 truststore-0.10.1/docs/Makefile0000644000000000000000000000117714470171257013363 0ustar00# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692463791.1241834 truststore-0.10.1/docs/make.bat0000644000000000000000000000144514470171257013326 0ustar00@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=_build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692647981.4590998 truststore-0.10.1/docs/requirements.txt0000644000000000000000000000003714470741055015200 0ustar00sphinx>=7.2.2 furo myst-parser ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692463791.1241834 truststore-0.10.1/docs/source/conf.py0000644000000000000000000000115414470171257014515 0ustar00import datetime import truststore project = "Truststore" author = "Seth Michael Larson, David Glick" copyright = f"{datetime.date.today().year}" release = version = truststore.__version__ extensions = [ "myst_parser", "sphinx.ext.intersphinx", ] html_theme = "furo" html_context = { "display_github": True, "github_user": "sethmlarson", "github_repo": "truststore", "github_version": "main", "conf_py_path": "/docs/source/", } intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "urllib3": ("https://urllib3.readthedocs.io/en/stable", None), } nitpicky = True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1724251920.6083498 truststore-0.10.1/docs/source/index.md0000644000000000000000000001452214661377421014655 0ustar00# Truststore ```{toctree} :maxdepth: 2 :caption: Contents ``` Truststore is a library which exposes native system certificate stores (ie "trust stores") through an `ssl.SSLContext`-like API. This means that Python applications no longer need to rely on certifi as a root certificate store. Native system certificate stores have many helpful features compared to a static certificate bundle like certifi: - Automatically update certificates as new CAs are created and removed - Fetch missing intermediate certificates - Check certificates against certificate revocation lists (CRLs) to avoid monster-in-the-middle (MITM) attacks - Managed per-system rather than per-application by a operations/IT team - PyPI is no longer a CA distribution channel 🥳 Right now truststore is a stand-alone library that can be installed globally in your application to immediately take advantage of the benefits in Python 3.10+. Truststore has also been integrated into pip as an opt-in method for verifying HTTPS certificates with truststore instead of certifi. Long-term the hope is to make truststore the default way to verify HTTPS certificates in pip and to add this functionality into Python itself. Wish us luck! ## Installation Truststore can be installed from [PyPI](https://pypi.org/project/truststore) with pip: ```{code-block} shell $ python -m pip install truststore ``` Truststore **requires Python 3.10 or later** and supports the following platforms: - macOS 10.8+ via [Security framework](https://developer.apple.com/documentation/security) - Windows via [CryptoAPI](https://docs.microsoft.com/en-us/windows/win32/seccrypto/cryptography-functions#certificate-verification-functions) - Linux via OpenSSL ## User Guide ```{warning} **PLEASE READ:** `inject_into_ssl()` **must not be used by libraries or packages** as it will cause issues on import time when integrated with other libraries. Libraries and packages should instead use `truststore.SSLContext` directly which is detailed below. The `inject_into_ssl()` function is intended only for use in applications and scripts. ``` You can inject `truststore` into the standard library `ssl` module so the functionality is used by every library by default. To do so use the `truststore.inject_into_ssl()` function. The call to `truststore.inject_into_ssl()` should be called as early as possible in your program as modules that have already imported `ssl.SSLContext` won't be affected. ```python import truststore truststore.inject_into_ssl() # Automatically works with urllib3, requests, aiohttp, and more: import urllib3 http = urllib3.PoolManager() resp = http.request("GET", "https://example.com") import aiohttp http = aiohttp.ClientSession() resp = await http.request("GET", "https://example.com") import requests resp = requests.get("https://example.com") ``` If you'd like finer-grained control you can create your own `truststore.SSLContext` instance and use it anywhere you'd use an `ssl.SSLContext`: ```python import ssl import truststore ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) import urllib3 http = urllib3.PoolManager(ssl_context=ctx) resp = http.request("GET", "https://example.com") ``` If Truststore can't work for a given platform due to APIs not being available then at import time the exception `ImportError` will be raised with an informative message: ```python # On Python 3.9 and earlier: import truststore # Raises 'ImportError' # On macOS 10.7 and earlier: import truststore # Raises 'ImportError' ``` ### Using truststore with pip [Pip v22.2](https://discuss.python.org/t/announcement-pip-22-2-release/17543) includes experimental support for verifying certificates with system trust stores using `truststore`. To enable the feature, use the flag `--use-feature=truststore` when installing a package like so: ```{code-block} bash # Install Django using system trust stores $ python -m pip install --use-feature=truststore Django ``` This requires `truststore` to be installed in the same environment as the one running pip and to be running Python 3.10 or later. For more information you can [read the pip documentation about the feature](https://pip.pypa.io/en/stable/user_guide/#using-system-trust-stores-for-verifying-https). ### Using truststore with urllib3 ```{code-block} python import urllib3 import truststore truststore.inject_into_ssl() http = urllib3.PoolManager() resp = http.request("GET", "https://example.com") ``` If you'd like to use the `truststore.SSLContext` directly you can pass the instance via the `ssl_context` parameter: ```{code-block} python import ssl import urllib3 import truststore ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) http = urllib3.PoolManager(ssl_context=ctx) resp = http.request("GET", "https://example.com") ``` ### Using truststore with aiohttp Truststore supports wrapping either {py:class}`socket.socket` or {py:class}`ssl.MemoryBIO` which means both synchronous and asynchronous I/O can be used: ```{code-block} python import aiohttp import truststore truststore.inject_into_ssl() async with aiohttp.ClientSession() as http_client: async with http_client.get("https://example.com") as http_response: ... ``` If you'd like to use the `truststore.SSLContext` directly you can pass the instance via the `ssl` parameter: ```{code-block} python import ssl import aiohttp import truststore ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) async with aiohttp.ClientSession(ssl=ctx) as http_client: async with http_client.get("https://example.com") as http_response: ... ``` ### Using truststore with Requests Just like with `urllib3` using `truststore.inject_into_ssl()` is the easiest method for using Truststore with Requests: ```{code-block} python import requests import truststore truststore.inject_into_ssl() resp = requests.request("GET", "https://example.com") ``` ## Prior art * [pip v22.2 with support for `--use-feature=truststore`](https://discuss.python.org/t/announcement-pip-22-2-release/17543) * [The future of trust stores in Python (PyCon US 2022 lightning talk)](https://youtu.be/1IiL31tUEVk?t=698) ([slides](https://speakerdeck.com/sethmlarson/the-future-of-trust-stores-in-python)) * [Experimental APIs in Python 3.10 and the future of trust stores](https://sethmlarson.dev/blog/2021-11-27/experimental-python-3.10-apis-and-trust-stores) * [PEP 543: A Unified TLS API for Python](https://www.python.org/dev/peps/pep-0543) ## License MIT ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1738678727.8250804 truststore-0.10.1/pyproject.toml0000644000000000000000000000263514750420710013677 0ustar00[build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" [project] name = "truststore" authors = [ {name = "Seth Michael Larson", email = "sethmichaellarson@gmail.com"}, {name = "David Glick", email = "david@glicksoftware.com"} ] readme = "README.md" license = {file = "LICENSE"} classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] dynamic = ["version", "description"] requires-python = ">= 3.10" [project.urls] Source = "https://github.com/sethmlarson/truststore" Documentation = "https://truststore.readthedocs.io" [tool.pytest.ini_options] asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" filterwarnings = [ "error", # See: aio-libs/aiohttp#7545 "ignore:.*datetime.utcfromtimestamp().*:DeprecationWarning", ] markers = [ "internet: test requires Internet access" ] [tool.flit.sdist] include = ["docs", "tests"] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1738949240.073142 truststore-0.10.1/src/truststore/__init__.py0000644000000000000000000000245014751441170016120 0ustar00"""Verify certificates using native system trust stores""" import sys as _sys if _sys.version_info < (3, 10): raise ImportError("truststore requires Python 3.10 or later") # Detect Python runtimes which don't implement SSLObject.get_unverified_chain() API # This API only became public in Python 3.13 but was available in CPython and PyPy since 3.10. if _sys.version_info < (3, 13) and _sys.implementation.name not in ("cpython", "pypy"): try: import ssl as _ssl except ImportError: raise ImportError("truststore requires the 'ssl' module") else: _sslmem = _ssl.MemoryBIO() _sslobj = _ssl.create_default_context().wrap_bio( _sslmem, _sslmem, ) try: while not hasattr(_sslobj, "get_unverified_chain"): _sslobj = _sslobj._sslobj # type: ignore[attr-defined] except AttributeError: raise ImportError( "truststore requires peer certificate chain APIs to be available" ) from None del _ssl, _sslobj, _sslmem # noqa: F821 from ._api import SSLContext, extract_from_ssl, inject_into_ssl # noqa: E402 del _api, _sys # type: ignore[name-defined] # noqa: F821 __all__ = ["SSLContext", "inject_into_ssl", "extract_from_ssl"] __version__ = "0.10.1" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1738678727.8250804 truststore-0.10.1/src/truststore/_api.py0000644000000000000000000002564414750420710015277 0ustar00import os import platform import socket import ssl import sys import typing import _ssl from ._ssl_constants import ( _original_SSLContext, _original_super_SSLContext, _truststore_SSLContext_dunder_class, _truststore_SSLContext_super_class, ) if platform.system() == "Windows": from ._windows import _configure_context, _verify_peercerts_impl elif platform.system() == "Darwin": from ._macos import _configure_context, _verify_peercerts_impl else: from ._openssl import _configure_context, _verify_peercerts_impl if typing.TYPE_CHECKING: from typing_extensions import Buffer # From typeshed/stdlib/ssl.pyi _StrOrBytesPath: typing.TypeAlias = str | bytes | os.PathLike[str] | os.PathLike[bytes] _PasswordType: typing.TypeAlias = str | bytes | typing.Callable[[], str | bytes] def inject_into_ssl() -> None: """Injects the :class:`truststore.SSLContext` into the ``ssl`` module by replacing :class:`ssl.SSLContext`. """ setattr(ssl, "SSLContext", SSLContext) # urllib3 holds on to its own reference of ssl.SSLContext # so we need to replace that reference too. try: import urllib3.util.ssl_ as urllib3_ssl setattr(urllib3_ssl, "SSLContext", SSLContext) except ImportError: pass # requests starting with 2.32.0 added a preloaded SSL context to improve concurrent performance; # this unfortunately leads to a RecursionError, which can be avoided by patching the preloaded SSL context with # the truststore patched instance # also see https://github.com/psf/requests/pull/6667 try: import requests.adapters preloaded_context = getattr(requests.adapters, "_preloaded_ssl_context", None) if preloaded_context is not None: setattr( requests.adapters, "_preloaded_ssl_context", SSLContext(ssl.PROTOCOL_TLS_CLIENT), ) except ImportError: pass def extract_from_ssl() -> None: """Restores the :class:`ssl.SSLContext` class to its original state""" setattr(ssl, "SSLContext", _original_SSLContext) try: import urllib3.util.ssl_ as urllib3_ssl urllib3_ssl.SSLContext = _original_SSLContext # type: ignore[assignment] except ImportError: pass class SSLContext(_truststore_SSLContext_super_class): # type: ignore[misc] """SSLContext API that uses system certificates on all platforms""" @property # type: ignore[misc] def __class__(self) -> type: # Dirty hack to get around isinstance() checks # for ssl.SSLContext instances in aiohttp/trustme # when using non-CPython implementations. return _truststore_SSLContext_dunder_class or SSLContext def __init__(self, protocol: int = None) -> None: # type: ignore[assignment] self._ctx = _original_SSLContext(protocol) class TruststoreSSLObject(ssl.SSLObject): # This object exists because wrap_bio() doesn't # immediately do the handshake so we need to do # certificate verifications after SSLObject.do_handshake() def do_handshake(self) -> None: ret = super().do_handshake() _verify_peercerts(self, server_hostname=self.server_hostname) return ret self._ctx.sslobject_class = TruststoreSSLObject def wrap_socket( self, sock: socket.socket, server_side: bool = False, do_handshake_on_connect: bool = True, suppress_ragged_eofs: bool = True, server_hostname: str | None = None, session: ssl.SSLSession | None = None, ) -> ssl.SSLSocket: # Use a context manager here because the # inner SSLContext holds on to our state # but also does the actual handshake. with _configure_context(self._ctx): ssl_sock = self._ctx.wrap_socket( sock, server_side=server_side, server_hostname=server_hostname, do_handshake_on_connect=do_handshake_on_connect, suppress_ragged_eofs=suppress_ragged_eofs, session=session, ) try: _verify_peercerts(ssl_sock, server_hostname=server_hostname) except Exception: ssl_sock.close() raise return ssl_sock def wrap_bio( self, incoming: ssl.MemoryBIO, outgoing: ssl.MemoryBIO, server_side: bool = False, server_hostname: str | None = None, session: ssl.SSLSession | None = None, ) -> ssl.SSLObject: with _configure_context(self._ctx): ssl_obj = self._ctx.wrap_bio( incoming, outgoing, server_hostname=server_hostname, server_side=server_side, session=session, ) return ssl_obj def load_verify_locations( self, cafile: str | bytes | os.PathLike[str] | os.PathLike[bytes] | None = None, capath: str | bytes | os.PathLike[str] | os.PathLike[bytes] | None = None, cadata: typing.Union[str, "Buffer", None] = None, ) -> None: return self._ctx.load_verify_locations( cafile=cafile, capath=capath, cadata=cadata ) def load_cert_chain( self, certfile: _StrOrBytesPath, keyfile: _StrOrBytesPath | None = None, password: _PasswordType | None = None, ) -> None: return self._ctx.load_cert_chain( certfile=certfile, keyfile=keyfile, password=password ) def load_default_certs( self, purpose: ssl.Purpose = ssl.Purpose.SERVER_AUTH ) -> None: return self._ctx.load_default_certs(purpose) def set_alpn_protocols(self, alpn_protocols: typing.Iterable[str]) -> None: return self._ctx.set_alpn_protocols(alpn_protocols) def set_npn_protocols(self, npn_protocols: typing.Iterable[str]) -> None: return self._ctx.set_npn_protocols(npn_protocols) def set_ciphers(self, __cipherlist: str) -> None: return self._ctx.set_ciphers(__cipherlist) def get_ciphers(self) -> typing.Any: return self._ctx.get_ciphers() def session_stats(self) -> dict[str, int]: return self._ctx.session_stats() def cert_store_stats(self) -> dict[str, int]: raise NotImplementedError() def set_default_verify_paths(self) -> None: self._ctx.set_default_verify_paths() @typing.overload def get_ca_certs( self, binary_form: typing.Literal[False] = ... ) -> list[typing.Any]: ... @typing.overload def get_ca_certs(self, binary_form: typing.Literal[True] = ...) -> list[bytes]: ... @typing.overload def get_ca_certs(self, binary_form: bool = ...) -> typing.Any: ... def get_ca_certs(self, binary_form: bool = False) -> list[typing.Any] | list[bytes]: raise NotImplementedError() @property def check_hostname(self) -> bool: return self._ctx.check_hostname @check_hostname.setter def check_hostname(self, value: bool) -> None: self._ctx.check_hostname = value @property def hostname_checks_common_name(self) -> bool: return self._ctx.hostname_checks_common_name @hostname_checks_common_name.setter def hostname_checks_common_name(self, value: bool) -> None: self._ctx.hostname_checks_common_name = value @property def keylog_filename(self) -> str: return self._ctx.keylog_filename @keylog_filename.setter def keylog_filename(self, value: str) -> None: self._ctx.keylog_filename = value @property def maximum_version(self) -> ssl.TLSVersion: return self._ctx.maximum_version @maximum_version.setter def maximum_version(self, value: ssl.TLSVersion) -> None: _original_super_SSLContext.maximum_version.__set__( # type: ignore[attr-defined] self._ctx, value ) @property def minimum_version(self) -> ssl.TLSVersion: return self._ctx.minimum_version @minimum_version.setter def minimum_version(self, value: ssl.TLSVersion) -> None: _original_super_SSLContext.minimum_version.__set__( # type: ignore[attr-defined] self._ctx, value ) @property def options(self) -> ssl.Options: return self._ctx.options @options.setter def options(self, value: ssl.Options) -> None: _original_super_SSLContext.options.__set__( # type: ignore[attr-defined] self._ctx, value ) @property def post_handshake_auth(self) -> bool: return self._ctx.post_handshake_auth @post_handshake_auth.setter def post_handshake_auth(self, value: bool) -> None: self._ctx.post_handshake_auth = value @property def protocol(self) -> ssl._SSLMethod: return self._ctx.protocol @property def security_level(self) -> int: return self._ctx.security_level @property def verify_flags(self) -> ssl.VerifyFlags: return self._ctx.verify_flags @verify_flags.setter def verify_flags(self, value: ssl.VerifyFlags) -> None: _original_super_SSLContext.verify_flags.__set__( # type: ignore[attr-defined] self._ctx, value ) @property def verify_mode(self) -> ssl.VerifyMode: return self._ctx.verify_mode @verify_mode.setter def verify_mode(self, value: ssl.VerifyMode) -> None: _original_super_SSLContext.verify_mode.__set__( # type: ignore[attr-defined] self._ctx, value ) # Python 3.13+ makes get_unverified_chain() a public API that only returns DER # encoded certificates. We detect whether we need to call public_bytes() for 3.10->3.12 # Pre-3.13 returned None instead of an empty list from get_unverified_chain() if sys.version_info >= (3, 13): def _get_unverified_chain_bytes(sslobj: ssl.SSLObject) -> list[bytes]: unverified_chain = sslobj.get_unverified_chain() or () # type: ignore[attr-defined] return [ cert if isinstance(cert, bytes) else cert.public_bytes(_ssl.ENCODING_DER) for cert in unverified_chain ] else: def _get_unverified_chain_bytes(sslobj: ssl.SSLObject) -> list[bytes]: unverified_chain = sslobj.get_unverified_chain() or () # type: ignore[attr-defined] return [cert.public_bytes(_ssl.ENCODING_DER) for cert in unverified_chain] def _verify_peercerts( sock_or_sslobj: ssl.SSLSocket | ssl.SSLObject, server_hostname: str | None ) -> None: """ Verifies the peer certificates from an SSLSocket or SSLObject against the certificates in the OS trust store. """ sslobj: ssl.SSLObject = sock_or_sslobj # type: ignore[assignment] try: while not hasattr(sslobj, "get_unverified_chain"): sslobj = sslobj._sslobj # type: ignore[attr-defined] except AttributeError: pass cert_bytes = _get_unverified_chain_bytes(sslobj) _verify_peercerts_impl( sock_or_sslobj.context, cert_bytes, server_hostname=server_hostname ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729717114.5160537 truststore-0.10.1/src/truststore/_macos.py0000644000000000000000000005002714706261573015634 0ustar00import contextlib import ctypes import platform import ssl import typing from ctypes import ( CDLL, POINTER, c_bool, c_char_p, c_int32, c_long, c_uint32, c_ulong, c_void_p, ) from ctypes.util import find_library from ._ssl_constants import _set_ssl_context_verify_mode _mac_version = platform.mac_ver()[0] _mac_version_info = tuple(map(int, _mac_version.split("."))) if _mac_version_info < (10, 8): raise ImportError( f"Only OS X 10.8 and newer are supported, not {_mac_version_info[0]}.{_mac_version_info[1]}" ) _is_macos_version_10_14_or_later = _mac_version_info >= (10, 14) def _load_cdll(name: str, macos10_16_path: str) -> CDLL: """Loads a CDLL by name, falling back to known path on 10.16+""" try: # Big Sur is technically 11 but we use 10.16 due to the Big Sur # beta being labeled as 10.16. path: str | None if _mac_version_info >= (10, 16): path = macos10_16_path else: path = find_library(name) if not path: raise OSError # Caught and reraised as 'ImportError' return CDLL(path, use_errno=True) except OSError: raise ImportError(f"The library {name} failed to load") from None Security = _load_cdll( "Security", "/System/Library/Frameworks/Security.framework/Security" ) CoreFoundation = _load_cdll( "CoreFoundation", "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", ) Boolean = c_bool CFIndex = c_long CFStringEncoding = c_uint32 CFData = c_void_p CFString = c_void_p CFArray = c_void_p CFMutableArray = c_void_p CFError = c_void_p CFType = c_void_p CFTypeID = c_ulong CFTypeRef = POINTER(CFType) CFAllocatorRef = c_void_p OSStatus = c_int32 CFErrorRef = POINTER(CFError) CFDataRef = POINTER(CFData) CFStringRef = POINTER(CFString) CFArrayRef = POINTER(CFArray) CFMutableArrayRef = POINTER(CFMutableArray) CFArrayCallBacks = c_void_p CFOptionFlags = c_uint32 SecCertificateRef = POINTER(c_void_p) SecPolicyRef = POINTER(c_void_p) SecTrustRef = POINTER(c_void_p) SecTrustResultType = c_uint32 SecTrustOptionFlags = c_uint32 try: Security.SecCertificateCreateWithData.argtypes = [CFAllocatorRef, CFDataRef] Security.SecCertificateCreateWithData.restype = SecCertificateRef Security.SecCertificateCopyData.argtypes = [SecCertificateRef] Security.SecCertificateCopyData.restype = CFDataRef Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p] Security.SecCopyErrorMessageString.restype = CFStringRef Security.SecTrustSetAnchorCertificates.argtypes = [SecTrustRef, CFArrayRef] Security.SecTrustSetAnchorCertificates.restype = OSStatus Security.SecTrustSetAnchorCertificatesOnly.argtypes = [SecTrustRef, Boolean] Security.SecTrustSetAnchorCertificatesOnly.restype = OSStatus Security.SecPolicyCreateRevocation.argtypes = [CFOptionFlags] Security.SecPolicyCreateRevocation.restype = SecPolicyRef Security.SecPolicyCreateSSL.argtypes = [Boolean, CFStringRef] Security.SecPolicyCreateSSL.restype = SecPolicyRef Security.SecTrustCreateWithCertificates.argtypes = [ CFTypeRef, CFTypeRef, POINTER(SecTrustRef), ] Security.SecTrustCreateWithCertificates.restype = OSStatus Security.SecTrustGetTrustResult.argtypes = [ SecTrustRef, POINTER(SecTrustResultType), ] Security.SecTrustGetTrustResult.restype = OSStatus Security.SecTrustEvaluate.argtypes = [ SecTrustRef, POINTER(SecTrustResultType), ] Security.SecTrustEvaluate.restype = OSStatus Security.SecTrustRef = SecTrustRef # type: ignore[attr-defined] Security.SecTrustResultType = SecTrustResultType # type: ignore[attr-defined] Security.OSStatus = OSStatus # type: ignore[attr-defined] kSecRevocationUseAnyAvailableMethod = 3 kSecRevocationRequirePositiveResponse = 8 CoreFoundation.CFRelease.argtypes = [CFTypeRef] CoreFoundation.CFRelease.restype = None CoreFoundation.CFGetTypeID.argtypes = [CFTypeRef] CoreFoundation.CFGetTypeID.restype = CFTypeID CoreFoundation.CFStringCreateWithCString.argtypes = [ CFAllocatorRef, c_char_p, CFStringEncoding, ] CoreFoundation.CFStringCreateWithCString.restype = CFStringRef CoreFoundation.CFStringGetCStringPtr.argtypes = [CFStringRef, CFStringEncoding] CoreFoundation.CFStringGetCStringPtr.restype = c_char_p CoreFoundation.CFStringGetCString.argtypes = [ CFStringRef, c_char_p, CFIndex, CFStringEncoding, ] CoreFoundation.CFStringGetCString.restype = c_bool CoreFoundation.CFDataCreate.argtypes = [CFAllocatorRef, c_char_p, CFIndex] CoreFoundation.CFDataCreate.restype = CFDataRef CoreFoundation.CFDataGetLength.argtypes = [CFDataRef] CoreFoundation.CFDataGetLength.restype = CFIndex CoreFoundation.CFDataGetBytePtr.argtypes = [CFDataRef] CoreFoundation.CFDataGetBytePtr.restype = c_void_p CoreFoundation.CFArrayCreate.argtypes = [ CFAllocatorRef, POINTER(CFTypeRef), CFIndex, CFArrayCallBacks, ] CoreFoundation.CFArrayCreate.restype = CFArrayRef CoreFoundation.CFArrayCreateMutable.argtypes = [ CFAllocatorRef, CFIndex, CFArrayCallBacks, ] CoreFoundation.CFArrayCreateMutable.restype = CFMutableArrayRef CoreFoundation.CFArrayAppendValue.argtypes = [CFMutableArrayRef, c_void_p] CoreFoundation.CFArrayAppendValue.restype = None CoreFoundation.CFArrayGetCount.argtypes = [CFArrayRef] CoreFoundation.CFArrayGetCount.restype = CFIndex CoreFoundation.CFArrayGetValueAtIndex.argtypes = [CFArrayRef, CFIndex] CoreFoundation.CFArrayGetValueAtIndex.restype = c_void_p CoreFoundation.CFErrorGetCode.argtypes = [CFErrorRef] CoreFoundation.CFErrorGetCode.restype = CFIndex CoreFoundation.CFErrorCopyDescription.argtypes = [CFErrorRef] CoreFoundation.CFErrorCopyDescription.restype = CFStringRef CoreFoundation.kCFAllocatorDefault = CFAllocatorRef.in_dll( # type: ignore[attr-defined] CoreFoundation, "kCFAllocatorDefault" ) CoreFoundation.kCFTypeArrayCallBacks = c_void_p.in_dll( # type: ignore[attr-defined] CoreFoundation, "kCFTypeArrayCallBacks" ) CoreFoundation.CFTypeRef = CFTypeRef # type: ignore[attr-defined] CoreFoundation.CFArrayRef = CFArrayRef # type: ignore[attr-defined] CoreFoundation.CFStringRef = CFStringRef # type: ignore[attr-defined] CoreFoundation.CFErrorRef = CFErrorRef # type: ignore[attr-defined] except AttributeError as e: raise ImportError(f"Error initializing ctypes: {e}") from None # SecTrustEvaluateWithError is macOS 10.14+ if _is_macos_version_10_14_or_later: try: Security.SecTrustEvaluateWithError.argtypes = [ SecTrustRef, POINTER(CFErrorRef), ] Security.SecTrustEvaluateWithError.restype = c_bool except AttributeError as e: raise ImportError(f"Error initializing ctypes: {e}") from None def _handle_osstatus(result: OSStatus, _: typing.Any, args: typing.Any) -> typing.Any: """ Raises an error if the OSStatus value is non-zero. """ if int(result) == 0: return args # Returns a CFString which we need to transform # into a UTF-8 Python string. error_message_cfstring = None try: error_message_cfstring = Security.SecCopyErrorMessageString(result, None) # First step is convert the CFString into a C string pointer. # We try the fast no-copy way first. error_message_cfstring_c_void_p = ctypes.cast( error_message_cfstring, ctypes.POINTER(ctypes.c_void_p) ) message = CoreFoundation.CFStringGetCStringPtr( error_message_cfstring_c_void_p, CFConst.kCFStringEncodingUTF8 ) # Quoting the Apple dev docs: # # "A pointer to a C string or NULL if the internal # storage of theString does not allow this to be # returned efficiently." # # So we need to get our hands dirty. if message is None: buffer = ctypes.create_string_buffer(1024) result = CoreFoundation.CFStringGetCString( error_message_cfstring_c_void_p, buffer, 1024, CFConst.kCFStringEncodingUTF8, ) if not result: raise OSError("Error copying C string from CFStringRef") message = buffer.value finally: if error_message_cfstring is not None: CoreFoundation.CFRelease(error_message_cfstring) # If no message can be found for this status we come # up with a generic one that forwards the status code. if message is None or message == "": message = f"SecureTransport operation returned a non-zero OSStatus: {result}" raise ssl.SSLError(message) Security.SecTrustCreateWithCertificates.errcheck = _handle_osstatus # type: ignore[assignment] Security.SecTrustSetAnchorCertificates.errcheck = _handle_osstatus # type: ignore[assignment] Security.SecTrustSetAnchorCertificatesOnly.errcheck = _handle_osstatus # type: ignore[assignment] Security.SecTrustGetTrustResult.errcheck = _handle_osstatus # type: ignore[assignment] Security.SecTrustEvaluate.errcheck = _handle_osstatus # type: ignore[assignment] class CFConst: """CoreFoundation constants""" kCFStringEncodingUTF8 = CFStringEncoding(0x08000100) errSecIncompleteCertRevocationCheck = -67635 errSecHostNameMismatch = -67602 errSecCertificateExpired = -67818 errSecNotTrusted = -67843 def _bytes_to_cf_data_ref(value: bytes) -> CFDataRef: # type: ignore[valid-type] return CoreFoundation.CFDataCreate( # type: ignore[no-any-return] CoreFoundation.kCFAllocatorDefault, value, len(value) ) def _bytes_to_cf_string(value: bytes) -> CFString: """ Given a Python binary data, create a CFString. The string must be CFReleased by the caller. """ c_str = ctypes.c_char_p(value) cf_str = CoreFoundation.CFStringCreateWithCString( CoreFoundation.kCFAllocatorDefault, c_str, CFConst.kCFStringEncodingUTF8, ) return cf_str # type: ignore[no-any-return] def _cf_string_ref_to_str(cf_string_ref: CFStringRef) -> str | None: # type: ignore[valid-type] """ Creates a Unicode string from a CFString object. Used entirely for error reporting. Yes, it annoys me quite a lot that this function is this complex. """ string = CoreFoundation.CFStringGetCStringPtr( cf_string_ref, CFConst.kCFStringEncodingUTF8 ) if string is None: buffer = ctypes.create_string_buffer(1024) result = CoreFoundation.CFStringGetCString( cf_string_ref, buffer, 1024, CFConst.kCFStringEncodingUTF8 ) if not result: raise OSError("Error copying C string from CFStringRef") string = buffer.value if string is not None: string = string.decode("utf-8") return string # type: ignore[no-any-return] def _der_certs_to_cf_cert_array(certs: list[bytes]) -> CFMutableArrayRef: # type: ignore[valid-type] """Builds a CFArray of SecCertificateRefs from a list of DER-encoded certificates. Responsibility of the caller to call CoreFoundation.CFRelease on the CFArray. """ cf_array = CoreFoundation.CFArrayCreateMutable( CoreFoundation.kCFAllocatorDefault, 0, ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks), ) if not cf_array: raise MemoryError("Unable to allocate memory!") for cert_data in certs: cf_data = None sec_cert_ref = None try: cf_data = _bytes_to_cf_data_ref(cert_data) sec_cert_ref = Security.SecCertificateCreateWithData( CoreFoundation.kCFAllocatorDefault, cf_data ) CoreFoundation.CFArrayAppendValue(cf_array, sec_cert_ref) finally: if cf_data: CoreFoundation.CFRelease(cf_data) if sec_cert_ref: CoreFoundation.CFRelease(sec_cert_ref) return cf_array # type: ignore[no-any-return] @contextlib.contextmanager def _configure_context(ctx: ssl.SSLContext) -> typing.Iterator[None]: check_hostname = ctx.check_hostname verify_mode = ctx.verify_mode ctx.check_hostname = False _set_ssl_context_verify_mode(ctx, ssl.CERT_NONE) try: yield finally: ctx.check_hostname = check_hostname _set_ssl_context_verify_mode(ctx, verify_mode) def _verify_peercerts_impl( ssl_context: ssl.SSLContext, cert_chain: list[bytes], server_hostname: str | None = None, ) -> None: certs = None policies = None trust = None try: # Only set a hostname on the policy if we're verifying the hostname # on the leaf certificate. if server_hostname is not None and ssl_context.check_hostname: cf_str_hostname = None try: cf_str_hostname = _bytes_to_cf_string(server_hostname.encode("ascii")) ssl_policy = Security.SecPolicyCreateSSL(True, cf_str_hostname) finally: if cf_str_hostname: CoreFoundation.CFRelease(cf_str_hostname) else: ssl_policy = Security.SecPolicyCreateSSL(True, None) policies = ssl_policy if ssl_context.verify_flags & ssl.VERIFY_CRL_CHECK_CHAIN: # Add explicit policy requiring positive revocation checks policies = CoreFoundation.CFArrayCreateMutable( CoreFoundation.kCFAllocatorDefault, 0, ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks), ) CoreFoundation.CFArrayAppendValue(policies, ssl_policy) CoreFoundation.CFRelease(ssl_policy) revocation_policy = Security.SecPolicyCreateRevocation( kSecRevocationUseAnyAvailableMethod | kSecRevocationRequirePositiveResponse ) CoreFoundation.CFArrayAppendValue(policies, revocation_policy) CoreFoundation.CFRelease(revocation_policy) elif ssl_context.verify_flags & ssl.VERIFY_CRL_CHECK_LEAF: raise NotImplementedError("VERIFY_CRL_CHECK_LEAF not implemented for macOS") certs = None try: certs = _der_certs_to_cf_cert_array(cert_chain) # Now that we have certificates loaded and a SecPolicy # we can finally create a SecTrust object! trust = Security.SecTrustRef() Security.SecTrustCreateWithCertificates( certs, policies, ctypes.byref(trust) ) finally: # The certs are now being held by SecTrust so we can # release our handles for the array. if certs: CoreFoundation.CFRelease(certs) # If there are additional trust anchors to load we need to transform # the list of DER-encoded certificates into a CFArray. ctx_ca_certs_der: list[bytes] | None = ssl_context.get_ca_certs( binary_form=True ) if ctx_ca_certs_der: ctx_ca_certs = None try: ctx_ca_certs = _der_certs_to_cf_cert_array(ctx_ca_certs_der) Security.SecTrustSetAnchorCertificates(trust, ctx_ca_certs) finally: if ctx_ca_certs: CoreFoundation.CFRelease(ctx_ca_certs) # We always want system certificates. Security.SecTrustSetAnchorCertificatesOnly(trust, False) # macOS 10.13 and earlier don't support SecTrustEvaluateWithError() # so we use SecTrustEvaluate() which means we need to construct error # messages ourselves. if _is_macos_version_10_14_or_later: _verify_peercerts_impl_macos_10_14(ssl_context, trust) else: _verify_peercerts_impl_macos_10_13(ssl_context, trust) finally: if policies: CoreFoundation.CFRelease(policies) if trust: CoreFoundation.CFRelease(trust) def _verify_peercerts_impl_macos_10_13( ssl_context: ssl.SSLContext, sec_trust_ref: typing.Any ) -> None: """Verify using 'SecTrustEvaluate' API for macOS 10.13 and earlier. macOS 10.14 added the 'SecTrustEvaluateWithError' API. """ sec_trust_result_type = Security.SecTrustResultType() Security.SecTrustEvaluate(sec_trust_ref, ctypes.byref(sec_trust_result_type)) try: sec_trust_result_type_as_int = int(sec_trust_result_type.value) except (ValueError, TypeError): sec_trust_result_type_as_int = -1 # Apple doesn't document these values in their own API docs. # See: https://github.com/xybp888/iOS-SDKs/blob/master/iPhoneOS13.0.sdk/System/Library/Frameworks/Security.framework/Headers/SecTrust.h#L84 if ( ssl_context.verify_mode == ssl.CERT_REQUIRED and sec_trust_result_type_as_int not in (1, 4) ): # Note that we're not able to ignore only hostname errors # for macOS 10.13 and earlier, so check_hostname=False will # still return an error. sec_trust_result_type_to_message = { 0: "Invalid trust result type", # 1: "Trust evaluation succeeded", 2: "User confirmation required", 3: "User specified that certificate is not trusted", # 4: "Trust result is unspecified", 5: "Recoverable trust failure occurred", 6: "Fatal trust failure occurred", 7: "Other error occurred, certificate may be revoked", } error_message = sec_trust_result_type_to_message.get( sec_trust_result_type_as_int, f"Unknown trust result: {sec_trust_result_type_as_int}", ) err = ssl.SSLCertVerificationError(error_message) err.verify_message = error_message err.verify_code = sec_trust_result_type_as_int raise err def _verify_peercerts_impl_macos_10_14( ssl_context: ssl.SSLContext, sec_trust_ref: typing.Any ) -> None: """Verify using 'SecTrustEvaluateWithError' API for macOS 10.14+.""" cf_error = CoreFoundation.CFErrorRef() sec_trust_eval_result = Security.SecTrustEvaluateWithError( sec_trust_ref, ctypes.byref(cf_error) ) # sec_trust_eval_result is a bool (0 or 1) # where 1 means that the certs are trusted. if sec_trust_eval_result == 1: is_trusted = True elif sec_trust_eval_result == 0: is_trusted = False else: raise ssl.SSLError( f"Unknown result from Security.SecTrustEvaluateWithError: {sec_trust_eval_result!r}" ) cf_error_code = 0 if not is_trusted: cf_error_code = CoreFoundation.CFErrorGetCode(cf_error) # If the error is a known failure that we're # explicitly okay with from SSLContext configuration # we can set is_trusted accordingly. if ssl_context.verify_mode != ssl.CERT_REQUIRED and ( cf_error_code == CFConst.errSecNotTrusted or cf_error_code == CFConst.errSecCertificateExpired ): is_trusted = True # If we're still not trusted then we start to # construct and raise the SSLCertVerificationError. if not is_trusted: cf_error_string_ref = None try: cf_error_string_ref = CoreFoundation.CFErrorCopyDescription(cf_error) # Can this ever return 'None' if there's a CFError? cf_error_message = ( _cf_string_ref_to_str(cf_error_string_ref) or "Certificate verification failed" ) # TODO: Not sure if we need the SecTrustResultType for anything? # We only care whether or not it's a success or failure for now. sec_trust_result_type = Security.SecTrustResultType() Security.SecTrustGetTrustResult( sec_trust_ref, ctypes.byref(sec_trust_result_type) ) err = ssl.SSLCertVerificationError(cf_error_message) err.verify_message = cf_error_message err.verify_code = cf_error_code raise err finally: if cf_error_string_ref: CoreFoundation.CFRelease(cf_error_string_ref) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692465732.8431914 truststore-0.10.1/src/truststore/_openssl.py0000644000000000000000000000442414470175105016206 0ustar00import contextlib import os import re import ssl import typing # candidates based on https://github.com/tiran/certifi-system-store by Christian Heimes _CA_FILE_CANDIDATES = [ # Alpine, Arch, Fedora 34+, OpenWRT, RHEL 9+, BSD "/etc/ssl/cert.pem", # Fedora <= 34, RHEL <= 9, CentOS <= 9 "/etc/pki/tls/cert.pem", # Debian, Ubuntu (requires ca-certificates) "/etc/ssl/certs/ca-certificates.crt", # SUSE "/etc/ssl/ca-bundle.pem", ] _HASHED_CERT_FILENAME_RE = re.compile(r"^[0-9a-fA-F]{8}\.[0-9]$") @contextlib.contextmanager def _configure_context(ctx: ssl.SSLContext) -> typing.Iterator[None]: # First, check whether the default locations from OpenSSL # seem like they will give us a usable set of CA certs. # ssl.get_default_verify_paths already takes care of: # - getting cafile from either the SSL_CERT_FILE env var # or the path configured when OpenSSL was compiled, # and verifying that that path exists # - getting capath from either the SSL_CERT_DIR env var # or the path configured when OpenSSL was compiled, # and verifying that that path exists # In addition we'll check whether capath appears to contain certs. defaults = ssl.get_default_verify_paths() if defaults.cafile or (defaults.capath and _capath_contains_certs(defaults.capath)): ctx.set_default_verify_paths() else: # cafile from OpenSSL doesn't exist # and capath from OpenSSL doesn't contain certs. # Let's search other common locations instead. for cafile in _CA_FILE_CANDIDATES: if os.path.isfile(cafile): ctx.load_verify_locations(cafile=cafile) break yield def _capath_contains_certs(capath: str) -> bool: """Check whether capath exists and contains certs in the expected format.""" if not os.path.isdir(capath): return False for name in os.listdir(capath): if _HASHED_CERT_FILENAME_RE.match(name): return True return False def _verify_peercerts_impl( ssl_context: ssl.SSLContext, cert_chain: list[bytes], server_hostname: str | None = None, ) -> None: # This is a no-op because we've enabled SSLContext's built-in # verification via verify_mode=CERT_REQUIRED, and don't need to repeat it. pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1706894732.5145478 truststore-0.10.1/src/truststore/_ssl_constants.py0000644000000000000000000000215214557222615017421 0ustar00import ssl import sys import typing # Hold on to the original class so we can create it consistently # even if we inject our own SSLContext into the ssl module. _original_SSLContext = ssl.SSLContext _original_super_SSLContext = super(_original_SSLContext, _original_SSLContext) # CPython is known to be good, but non-CPython implementations # may implement SSLContext differently so to be safe we don't # subclass the SSLContext. # This is returned by truststore.SSLContext.__class__() _truststore_SSLContext_dunder_class: typing.Optional[type] # This value is the superclass of truststore.SSLContext. _truststore_SSLContext_super_class: type if sys.implementation.name == "cpython": _truststore_SSLContext_super_class = _original_SSLContext _truststore_SSLContext_dunder_class = None else: _truststore_SSLContext_super_class = object _truststore_SSLContext_dunder_class = _original_SSLContext def _set_ssl_context_verify_mode( ssl_context: ssl.SSLContext, verify_mode: ssl.VerifyMode ) -> None: _original_super_SSLContext.verify_mode.__set__(ssl_context, verify_mode) # type: ignore[attr-defined] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729717114.5160537 truststore-0.10.1/src/truststore/_windows.py0000644000000000000000000004311114706261573016220 0ustar00import contextlib import ssl import typing from ctypes import WinDLL # type: ignore from ctypes import WinError # type: ignore from ctypes import ( POINTER, Structure, c_char_p, c_ulong, c_void_p, c_wchar_p, cast, create_unicode_buffer, pointer, sizeof, ) from ctypes.wintypes import ( BOOL, DWORD, HANDLE, LONG, LPCSTR, LPCVOID, LPCWSTR, LPFILETIME, LPSTR, LPWSTR, ) from typing import TYPE_CHECKING, Any from ._ssl_constants import _set_ssl_context_verify_mode HCERTCHAINENGINE = HANDLE HCERTSTORE = HANDLE HCRYPTPROV_LEGACY = HANDLE class CERT_CONTEXT(Structure): _fields_ = ( ("dwCertEncodingType", DWORD), ("pbCertEncoded", c_void_p), ("cbCertEncoded", DWORD), ("pCertInfo", c_void_p), ("hCertStore", HCERTSTORE), ) PCERT_CONTEXT = POINTER(CERT_CONTEXT) PCCERT_CONTEXT = POINTER(PCERT_CONTEXT) class CERT_ENHKEY_USAGE(Structure): _fields_ = ( ("cUsageIdentifier", DWORD), ("rgpszUsageIdentifier", POINTER(LPSTR)), ) PCERT_ENHKEY_USAGE = POINTER(CERT_ENHKEY_USAGE) class CERT_USAGE_MATCH(Structure): _fields_ = ( ("dwType", DWORD), ("Usage", CERT_ENHKEY_USAGE), ) class CERT_CHAIN_PARA(Structure): _fields_ = ( ("cbSize", DWORD), ("RequestedUsage", CERT_USAGE_MATCH), ("RequestedIssuancePolicy", CERT_USAGE_MATCH), ("dwUrlRetrievalTimeout", DWORD), ("fCheckRevocationFreshnessTime", BOOL), ("dwRevocationFreshnessTime", DWORD), ("pftCacheResync", LPFILETIME), ("pStrongSignPara", c_void_p), ("dwStrongSignFlags", DWORD), ) if TYPE_CHECKING: PCERT_CHAIN_PARA = pointer[CERT_CHAIN_PARA] # type: ignore[misc] else: PCERT_CHAIN_PARA = POINTER(CERT_CHAIN_PARA) class CERT_TRUST_STATUS(Structure): _fields_ = ( ("dwErrorStatus", DWORD), ("dwInfoStatus", DWORD), ) class CERT_CHAIN_ELEMENT(Structure): _fields_ = ( ("cbSize", DWORD), ("pCertContext", PCERT_CONTEXT), ("TrustStatus", CERT_TRUST_STATUS), ("pRevocationInfo", c_void_p), ("pIssuanceUsage", PCERT_ENHKEY_USAGE), ("pApplicationUsage", PCERT_ENHKEY_USAGE), ("pwszExtendedErrorInfo", LPCWSTR), ) PCERT_CHAIN_ELEMENT = POINTER(CERT_CHAIN_ELEMENT) class CERT_SIMPLE_CHAIN(Structure): _fields_ = ( ("cbSize", DWORD), ("TrustStatus", CERT_TRUST_STATUS), ("cElement", DWORD), ("rgpElement", POINTER(PCERT_CHAIN_ELEMENT)), ("pTrustListInfo", c_void_p), ("fHasRevocationFreshnessTime", BOOL), ("dwRevocationFreshnessTime", DWORD), ) PCERT_SIMPLE_CHAIN = POINTER(CERT_SIMPLE_CHAIN) class CERT_CHAIN_CONTEXT(Structure): _fields_ = ( ("cbSize", DWORD), ("TrustStatus", CERT_TRUST_STATUS), ("cChain", DWORD), ("rgpChain", POINTER(PCERT_SIMPLE_CHAIN)), ("cLowerQualityChainContext", DWORD), ("rgpLowerQualityChainContext", c_void_p), ("fHasRevocationFreshnessTime", BOOL), ("dwRevocationFreshnessTime", DWORD), ) PCERT_CHAIN_CONTEXT = POINTER(CERT_CHAIN_CONTEXT) PCCERT_CHAIN_CONTEXT = POINTER(PCERT_CHAIN_CONTEXT) class SSL_EXTRA_CERT_CHAIN_POLICY_PARA(Structure): _fields_ = ( ("cbSize", DWORD), ("dwAuthType", DWORD), ("fdwChecks", DWORD), ("pwszServerName", LPCWSTR), ) class CERT_CHAIN_POLICY_PARA(Structure): _fields_ = ( ("cbSize", DWORD), ("dwFlags", DWORD), ("pvExtraPolicyPara", c_void_p), ) PCERT_CHAIN_POLICY_PARA = POINTER(CERT_CHAIN_POLICY_PARA) class CERT_CHAIN_POLICY_STATUS(Structure): _fields_ = ( ("cbSize", DWORD), ("dwError", DWORD), ("lChainIndex", LONG), ("lElementIndex", LONG), ("pvExtraPolicyStatus", c_void_p), ) PCERT_CHAIN_POLICY_STATUS = POINTER(CERT_CHAIN_POLICY_STATUS) class CERT_CHAIN_ENGINE_CONFIG(Structure): _fields_ = ( ("cbSize", DWORD), ("hRestrictedRoot", HCERTSTORE), ("hRestrictedTrust", HCERTSTORE), ("hRestrictedOther", HCERTSTORE), ("cAdditionalStore", DWORD), ("rghAdditionalStore", c_void_p), ("dwFlags", DWORD), ("dwUrlRetrievalTimeout", DWORD), ("MaximumCachedCertificates", DWORD), ("CycleDetectionModulus", DWORD), ("hExclusiveRoot", HCERTSTORE), ("hExclusiveTrustedPeople", HCERTSTORE), ("dwExclusiveFlags", DWORD), ) PCERT_CHAIN_ENGINE_CONFIG = POINTER(CERT_CHAIN_ENGINE_CONFIG) PHCERTCHAINENGINE = POINTER(HCERTCHAINENGINE) X509_ASN_ENCODING = 0x00000001 PKCS_7_ASN_ENCODING = 0x00010000 CERT_STORE_PROV_MEMORY = b"Memory" CERT_STORE_ADD_USE_EXISTING = 2 USAGE_MATCH_TYPE_OR = 1 OID_PKIX_KP_SERVER_AUTH = c_char_p(b"1.3.6.1.5.5.7.3.1") CERT_CHAIN_REVOCATION_CHECK_END_CERT = 0x10000000 CERT_CHAIN_REVOCATION_CHECK_CHAIN = 0x20000000 CERT_CHAIN_POLICY_IGNORE_ALL_NOT_TIME_VALID_FLAGS = 0x00000007 CERT_CHAIN_POLICY_IGNORE_INVALID_BASIC_CONSTRAINTS_FLAG = 0x00000008 CERT_CHAIN_POLICY_ALLOW_UNKNOWN_CA_FLAG = 0x00000010 CERT_CHAIN_POLICY_IGNORE_INVALID_NAME_FLAG = 0x00000040 CERT_CHAIN_POLICY_IGNORE_WRONG_USAGE_FLAG = 0x00000020 CERT_CHAIN_POLICY_IGNORE_INVALID_POLICY_FLAG = 0x00000080 CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS = 0x00000F00 CERT_CHAIN_POLICY_ALLOW_TESTROOT_FLAG = 0x00008000 CERT_CHAIN_POLICY_TRUST_TESTROOT_FLAG = 0x00004000 SECURITY_FLAG_IGNORE_CERT_CN_INVALID = 0x00001000 AUTHTYPE_SERVER = 2 CERT_CHAIN_POLICY_SSL = 4 FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200 # Flags to set for SSLContext.verify_mode=CERT_NONE CERT_CHAIN_POLICY_VERIFY_MODE_NONE_FLAGS = ( CERT_CHAIN_POLICY_IGNORE_ALL_NOT_TIME_VALID_FLAGS | CERT_CHAIN_POLICY_IGNORE_INVALID_BASIC_CONSTRAINTS_FLAG | CERT_CHAIN_POLICY_ALLOW_UNKNOWN_CA_FLAG | CERT_CHAIN_POLICY_IGNORE_INVALID_NAME_FLAG | CERT_CHAIN_POLICY_IGNORE_WRONG_USAGE_FLAG | CERT_CHAIN_POLICY_IGNORE_INVALID_POLICY_FLAG | CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS | CERT_CHAIN_POLICY_ALLOW_TESTROOT_FLAG | CERT_CHAIN_POLICY_TRUST_TESTROOT_FLAG ) wincrypt = WinDLL("crypt32.dll") kernel32 = WinDLL("kernel32.dll") def _handle_win_error(result: bool, _: Any, args: Any) -> Any: if not result: # Note, actually raises OSError after calling GetLastError and FormatMessage raise WinError() return args CertCreateCertificateChainEngine = wincrypt.CertCreateCertificateChainEngine CertCreateCertificateChainEngine.argtypes = ( PCERT_CHAIN_ENGINE_CONFIG, PHCERTCHAINENGINE, ) CertCreateCertificateChainEngine.errcheck = _handle_win_error CertOpenStore = wincrypt.CertOpenStore CertOpenStore.argtypes = (LPCSTR, DWORD, HCRYPTPROV_LEGACY, DWORD, c_void_p) CertOpenStore.restype = HCERTSTORE CertOpenStore.errcheck = _handle_win_error CertAddEncodedCertificateToStore = wincrypt.CertAddEncodedCertificateToStore CertAddEncodedCertificateToStore.argtypes = ( HCERTSTORE, DWORD, c_char_p, DWORD, DWORD, PCCERT_CONTEXT, ) CertAddEncodedCertificateToStore.restype = BOOL CertCreateCertificateContext = wincrypt.CertCreateCertificateContext CertCreateCertificateContext.argtypes = (DWORD, c_char_p, DWORD) CertCreateCertificateContext.restype = PCERT_CONTEXT CertCreateCertificateContext.errcheck = _handle_win_error CertGetCertificateChain = wincrypt.CertGetCertificateChain CertGetCertificateChain.argtypes = ( HCERTCHAINENGINE, PCERT_CONTEXT, LPFILETIME, HCERTSTORE, PCERT_CHAIN_PARA, DWORD, c_void_p, PCCERT_CHAIN_CONTEXT, ) CertGetCertificateChain.restype = BOOL CertGetCertificateChain.errcheck = _handle_win_error CertVerifyCertificateChainPolicy = wincrypt.CertVerifyCertificateChainPolicy CertVerifyCertificateChainPolicy.argtypes = ( c_ulong, PCERT_CHAIN_CONTEXT, PCERT_CHAIN_POLICY_PARA, PCERT_CHAIN_POLICY_STATUS, ) CertVerifyCertificateChainPolicy.restype = BOOL CertCloseStore = wincrypt.CertCloseStore CertCloseStore.argtypes = (HCERTSTORE, DWORD) CertCloseStore.restype = BOOL CertCloseStore.errcheck = _handle_win_error CertFreeCertificateChain = wincrypt.CertFreeCertificateChain CertFreeCertificateChain.argtypes = (PCERT_CHAIN_CONTEXT,) CertFreeCertificateContext = wincrypt.CertFreeCertificateContext CertFreeCertificateContext.argtypes = (PCERT_CONTEXT,) CertFreeCertificateChainEngine = wincrypt.CertFreeCertificateChainEngine CertFreeCertificateChainEngine.argtypes = (HCERTCHAINENGINE,) FormatMessageW = kernel32.FormatMessageW FormatMessageW.argtypes = ( DWORD, LPCVOID, DWORD, DWORD, LPWSTR, DWORD, c_void_p, ) FormatMessageW.restype = DWORD def _verify_peercerts_impl( ssl_context: ssl.SSLContext, cert_chain: list[bytes], server_hostname: str | None = None, ) -> None: """Verify the cert_chain from the server using Windows APIs.""" # If the peer didn't send any certificates then # we can't do verification. Raise an error. if not cert_chain: raise ssl.SSLCertVerificationError("Peer sent no certificates to verify") pCertContext = None hIntermediateCertStore = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, None, 0, None) try: # Add intermediate certs to an in-memory cert store for cert_bytes in cert_chain[1:]: CertAddEncodedCertificateToStore( hIntermediateCertStore, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, cert_bytes, len(cert_bytes), CERT_STORE_ADD_USE_EXISTING, None, ) # Cert context for leaf cert leaf_cert = cert_chain[0] pCertContext = CertCreateCertificateContext( X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, leaf_cert, len(leaf_cert) ) # Chain params to match certs for serverAuth extended usage cert_enhkey_usage = CERT_ENHKEY_USAGE() cert_enhkey_usage.cUsageIdentifier = 1 cert_enhkey_usage.rgpszUsageIdentifier = (c_char_p * 1)(OID_PKIX_KP_SERVER_AUTH) cert_usage_match = CERT_USAGE_MATCH() cert_usage_match.Usage = cert_enhkey_usage chain_params = CERT_CHAIN_PARA() chain_params.RequestedUsage = cert_usage_match chain_params.cbSize = sizeof(chain_params) pChainPara = pointer(chain_params) if ssl_context.verify_flags & ssl.VERIFY_CRL_CHECK_CHAIN: chain_flags = CERT_CHAIN_REVOCATION_CHECK_CHAIN elif ssl_context.verify_flags & ssl.VERIFY_CRL_CHECK_LEAF: chain_flags = CERT_CHAIN_REVOCATION_CHECK_END_CERT else: chain_flags = 0 try: # First attempt to verify using the default Windows system trust roots # (default chain engine). _get_and_verify_cert_chain( ssl_context, None, hIntermediateCertStore, pCertContext, pChainPara, server_hostname, chain_flags=chain_flags, ) except ssl.SSLCertVerificationError as e: # If that fails but custom CA certs have been added # to the SSLContext using load_verify_locations, # try verifying using a custom chain engine # that trusts the custom CA certs. custom_ca_certs: list[bytes] | None = ssl_context.get_ca_certs( binary_form=True ) if custom_ca_certs: try: _verify_using_custom_ca_certs( ssl_context, custom_ca_certs, hIntermediateCertStore, pCertContext, pChainPara, server_hostname, chain_flags=chain_flags, ) # Raise the original error, not the new error. except ssl.SSLCertVerificationError: raise e from None else: raise finally: CertCloseStore(hIntermediateCertStore, 0) if pCertContext: CertFreeCertificateContext(pCertContext) def _get_and_verify_cert_chain( ssl_context: ssl.SSLContext, hChainEngine: HCERTCHAINENGINE | None, hIntermediateCertStore: HCERTSTORE, pPeerCertContext: c_void_p, pChainPara: PCERT_CHAIN_PARA, # type: ignore[valid-type] server_hostname: str | None, chain_flags: int, ) -> None: ppChainContext = None try: # Get cert chain ppChainContext = pointer(PCERT_CHAIN_CONTEXT()) CertGetCertificateChain( hChainEngine, # chain engine pPeerCertContext, # leaf cert context None, # current system time hIntermediateCertStore, # additional in-memory cert store pChainPara, # chain-building parameters chain_flags, None, # reserved ppChainContext, # the resulting chain context ) pChainContext = ppChainContext.contents # Verify cert chain ssl_extra_cert_chain_policy_para = SSL_EXTRA_CERT_CHAIN_POLICY_PARA() ssl_extra_cert_chain_policy_para.cbSize = sizeof( ssl_extra_cert_chain_policy_para ) ssl_extra_cert_chain_policy_para.dwAuthType = AUTHTYPE_SERVER ssl_extra_cert_chain_policy_para.fdwChecks = 0 if ssl_context.check_hostname is False: ssl_extra_cert_chain_policy_para.fdwChecks = ( SECURITY_FLAG_IGNORE_CERT_CN_INVALID ) if server_hostname: ssl_extra_cert_chain_policy_para.pwszServerName = c_wchar_p(server_hostname) chain_policy = CERT_CHAIN_POLICY_PARA() chain_policy.pvExtraPolicyPara = cast( pointer(ssl_extra_cert_chain_policy_para), c_void_p ) if ssl_context.verify_mode == ssl.CERT_NONE: chain_policy.dwFlags |= CERT_CHAIN_POLICY_VERIFY_MODE_NONE_FLAGS chain_policy.cbSize = sizeof(chain_policy) pPolicyPara = pointer(chain_policy) policy_status = CERT_CHAIN_POLICY_STATUS() policy_status.cbSize = sizeof(policy_status) pPolicyStatus = pointer(policy_status) CertVerifyCertificateChainPolicy( CERT_CHAIN_POLICY_SSL, pChainContext, pPolicyPara, pPolicyStatus, ) # Check status error_code = policy_status.dwError if error_code: # Try getting a human readable message for an error code. error_message_buf = create_unicode_buffer(1024) error_message_chars = FormatMessageW( FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, None, error_code, 0, error_message_buf, sizeof(error_message_buf), None, ) # See if we received a message for the error, # otherwise we use a generic error with the # error code and hope that it's search-able. if error_message_chars <= 0: error_message = f"Certificate chain policy error {error_code:#x} [{policy_status.lElementIndex}]" else: error_message = error_message_buf.value.strip() err = ssl.SSLCertVerificationError(error_message) err.verify_message = error_message err.verify_code = error_code raise err from None finally: if ppChainContext: CertFreeCertificateChain(ppChainContext.contents) def _verify_using_custom_ca_certs( ssl_context: ssl.SSLContext, custom_ca_certs: list[bytes], hIntermediateCertStore: HCERTSTORE, pPeerCertContext: c_void_p, pChainPara: PCERT_CHAIN_PARA, # type: ignore[valid-type] server_hostname: str | None, chain_flags: int, ) -> None: hChainEngine = None hRootCertStore = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, None, 0, None) try: # Add custom CA certs to an in-memory cert store for cert_bytes in custom_ca_certs: CertAddEncodedCertificateToStore( hRootCertStore, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, cert_bytes, len(cert_bytes), CERT_STORE_ADD_USE_EXISTING, None, ) # Create a custom cert chain engine which exclusively trusts # certs from our hRootCertStore cert_chain_engine_config = CERT_CHAIN_ENGINE_CONFIG() cert_chain_engine_config.cbSize = sizeof(cert_chain_engine_config) cert_chain_engine_config.hExclusiveRoot = hRootCertStore pConfig = pointer(cert_chain_engine_config) phChainEngine = pointer(HCERTCHAINENGINE()) CertCreateCertificateChainEngine( pConfig, phChainEngine, ) hChainEngine = phChainEngine.contents # Get and verify a cert chain using the custom chain engine _get_and_verify_cert_chain( ssl_context, hChainEngine, hIntermediateCertStore, pPeerCertContext, pChainPara, server_hostname, chain_flags, ) finally: if hChainEngine: CertFreeCertificateChainEngine(hChainEngine) CertCloseStore(hRootCertStore, 0) @contextlib.contextmanager def _configure_context(ctx: ssl.SSLContext) -> typing.Iterator[None]: check_hostname = ctx.check_hostname verify_mode = ctx.verify_mode ctx.check_hostname = False _set_ssl_context_verify_mode(ctx, ssl.CERT_NONE) try: yield finally: ctx.check_hostname = check_hostname _set_ssl_context_verify_mode(ctx, verify_mode) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692463791.1241834 truststore-0.10.1/src/truststore/py.typed0000644000000000000000000000000014470171257015477 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692463791.1241834 truststore-0.10.1/tests/__init__.py0000644000000000000000000000106114470171257014236 0ustar00import requests import requests.adapters class SSLContextAdapter(requests.adapters.HTTPAdapter): # HTTPAdapter for Requests that allows for injecting an SSLContext # into the lower-level urllib3.PoolManager. def __init__(self, *, ssl_context=None, **kwargs): self._ssl_context = ssl_context super().__init__(**kwargs) def init_poolmanager(self, *args, **kwargs): if self._ssl_context is not None: kwargs.setdefault("ssl_context", self._ssl_context) return super().init_poolmanager(*args, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729716774.7134526 truststore-0.10.1/tests/aiohttp_with_inject.py0000644000000000000000000000060214706261047016535 0ustar00# Used by the test: test_inject.py::test_aiohttp_work_with_inject import asyncio import sys import truststore truststore.inject_into_ssl() import aiohttp # noqa: E402 async def main(): async with aiohttp.ClientSession() as client: resp = await client.get("https://localhost:9999") assert resp.status == 200 sys.exit(resp.status) asyncio.run(main()) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1729709483.261988 truststore-0.10.1/tests/conftest.py0000644000000000000000000001225514706242653014334 0ustar00import asyncio import logging import pathlib import ssl import typing from dataclasses import dataclass from tempfile import TemporaryDirectory import pytest import pytest_asyncio from aiohttp import web MKCERT_CA_NOT_INSTALLED = b"local CA is not installed in the system trust store" MKCERT_CA_ALREADY_INSTALLED = b"local CA is now installed in the system trust store" SUBPROCESS_TIMEOUT = 5 # To avoid getting the SSLContext injected by truststore. original_SSLContext = ssl.SSLContext def decorator_requires_internet(decorator): """Mark a decorator with the "internet" mark""" def wrapper(f): return pytest.mark.internet(decorator(f)) return wrapper successful_hosts = decorator_requires_internet( pytest.mark.parametrize("host", ["example.com", "1.1.1.1"]) ) logger = logging.getLogger("aiohttp.web") @pytest_asyncio.fixture async def mkcert() -> typing.AsyncIterator[None]: async def is_mkcert_available() -> bool: try: p = await asyncio.create_subprocess_exec( "mkcert", "-help", stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) except FileNotFoundError: return False await asyncio.wait_for(p.wait(), timeout=SUBPROCESS_TIMEOUT) return p.returncode == 0 # Checks to see if mkcert is available at all. if not await is_mkcert_available(): pytest.skip("Install mkcert to run custom CA tests") # Now we attempt to install the root certificate # to the system trust store. Keep track if we should # call mkcert -uninstall at the end. should_mkcert_uninstall = False try: p = await asyncio.create_subprocess_exec( "mkcert", "-install", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, ) await p.wait() assert p.returncode == 0 # See if the root cert was installed for the first # time, if so we want to leave no trace. stdout, _ = await p.communicate() should_mkcert_uninstall = MKCERT_CA_ALREADY_INSTALLED in stdout yield finally: # Only uninstall mkcert root cert if it wasn't # installed before our attempt to install. if should_mkcert_uninstall: p = await asyncio.create_subprocess_exec( "mkcert", "-uninstall", stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) await p.wait() @dataclass class CertFiles: key_file: pathlib.Path cert_file: pathlib.Path @pytest_asyncio.fixture async def mkcert_certs(mkcert: None) -> typing.AsyncIterator[CertFiles]: with TemporaryDirectory() as tmp_dir: # Create the structure we'll eventually return # as long as mkcert succeeds in creating the certs. tmpdir_path = pathlib.Path(tmp_dir) certs = CertFiles( cert_file=tmpdir_path / "localhost.pem", key_file=tmpdir_path / "localhost-key.pem", ) cmd = ( "mkcert" f" -cert-file {certs.cert_file}" f" -key-file {certs.key_file}" " localhost" ) p = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, ) await asyncio.wait_for(p.wait(), timeout=SUBPROCESS_TIMEOUT) # Check for any signs that mkcert wasn't able to issue certs # or that the CA isn't installed stdout, _ = await p.communicate() if MKCERT_CA_NOT_INSTALLED in stdout or p.returncode != 0: raise RuntimeError( f"mkcert couldn't issue certificates " f"(exited with {p.returncode}): {stdout.decode()}" ) yield certs @dataclass class Server: host: str port: int @property def base_url(self) -> str: return f"https://{self.host}:{self.port}" @pytest_asyncio.fixture(scope="function") async def server(mkcert_certs: CertFiles) -> typing.AsyncIterator[Server]: async def handler(request: web.Request) -> web.Response: # Check the request was served over HTTPS. assert request.scheme == "https" return web.Response(status=200) app = web.Application() app.add_routes([web.get("/", handler)]) ctx = original_SSLContext(ssl.PROTOCOL_TLS_SERVER) # We use str(pathlib.Path) here because PyPy doesn't accept Path objects. # TODO: This is a bug in PyPy and should be reported to them, but their # GitLab instance was offline when we found this bug. :'( ctx.load_cert_chain( certfile=str(mkcert_certs.cert_file), keyfile=str(mkcert_certs.key_file), ) # we need keepalive_timeout=0 # see https://github.com/aio-libs/aiohttp/issues/5426 runner = web.AppRunner(app, keepalive_timeout=0) await runner.setup() port = 9999 # Arbitrary choice. site = web.TCPSite(runner, ssl_context=ctx, port=port) await site.start() try: yield Server(host="localhost", port=port) finally: await site.stop() await runner.cleanup() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729716774.7134526 truststore-0.10.1/tests/requests_with_inject.py0000644000000000000000000000042414706261047016742 0ustar00# Used by the test: test_inject.py::test_requests_work_with_inject import sys import truststore truststore.inject_into_ssl() import requests # noqa: E402 resp = requests.request("GET", "https://localhost:9999") assert resp.status_code == 200 sys.exit(resp.status_code) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1738949240.073142 truststore-0.10.1/tests/test_api.py0000644000000000000000000003546614751441170014323 0ustar00import importlib import os import platform import socket import ssl import tempfile from dataclasses import dataclass from operator import attrgetter from unittest import mock import aiohttp import aiohttp.client_exceptions import pytest import requests import urllib3 import urllib3.exceptions from pytest_httpserver import HTTPServer import truststore from tests import SSLContextAdapter from tests.conftest import decorator_requires_internet pytestmark = pytest.mark.flaky # Make sure the httpserver doesn't hang # if the client drops the connection due to a cert verification error socket.setdefaulttimeout(10) successful_hosts = decorator_requires_internet( pytest.mark.parametrize("host", ["example.com", "1.1.1.1"]) ) @dataclass class FailureHost: host: str error_messages: list[str] wrong_host_failure_host = FailureHost( host="wrong.host.badssl.com", error_messages=[ # OpenSSL "Hostname mismatch, certificate is not valid for 'wrong.host.badssl.com'", # macOS "certificate name does not match", # macOS with revocation checks "certificates do not meet pinning requirements", # macOS 10.13 "Recoverable trust failure occurred", # Windows "The certificate's CN name does not match the passed value.", ], ) failure_hosts_list = [ wrong_host_failure_host, FailureHost( host="expired.badssl.com", error_messages=[ # OpenSSL "certificate has expired", # macOS "“*.badssl.com” certificate is expired", # macOS with revocation checks "certificates do not meet pinning requirements", # macOS 10.13 "Recoverable trust failure occurred", # Windows ( "A required certificate is not within its validity period when verifying " "against the current system clock or the timestamp in the signed file." ), ], ), FailureHost( host="self-signed.badssl.com", error_messages=[ # OpenSSL "self-signed certificate", "self signed certificate", # macOS "“*.badssl.com” certificate is not trusted", # macOS with revocation checks "certificates do not meet pinning requirements", # macOS 10.13 "Recoverable trust failure occurred", # Windows ( "A certificate chain processed, but terminated in a root " "certificate which is not trusted by the trust provider." ), ], ), FailureHost( host="untrusted-root.badssl.com", error_messages=[ # OpenSSL "self-signed certificate in certificate chain", "self signed certificate in certificate chain", # macOS "“BadSSL Untrusted Root Certificate Authority” certificate is not trusted", # macOS with revocation checks "certificates do not meet pinning requirements", # macOS 10.13 "Recoverable trust failure occurred", # Windows ( "A certificate chain processed, but terminated in a root " "certificate which is not trusted by the trust provider." ), ], ), FailureHost( host="superfish.badssl.com", error_messages=[ # OpenSSL "unable to get local issuer certificate", # macOS "“superfish.badssl.com” certificate is not trusted", # macOS with revocation checks "certificates do not meet pinning requirements", # macOS 10.13 "Recoverable trust failure occurred", # Windows ( "A certificate chain processed, but terminated in a root " "certificate which is not trusted by the trust provider." ), ], ), ] failure_hosts_no_revocation = decorator_requires_internet( pytest.mark.parametrize( "failure", failure_hosts_list.copy(), ids=attrgetter("host") ) ) if platform.system() != "Linux": failure_hosts_list.append( FailureHost( host="revoked.badssl.com", error_messages=[ # macOS "“revoked.badssl.com” certificate is revoked", # macOS 10.13 "Fatal trust failure occurred", # Windows "The certificate is revoked.", ], ) ) failure_hosts = decorator_requires_internet( pytest.mark.parametrize("failure", failure_hosts_list, ids=attrgetter("host")) ) # Fixture which tests both the SecTrustEvaluate (macOS <=10.13) and # SecTrustEvaluateWithError (macOS >=10.14) APIs # on macOS versions that support both APIs. if platform.system() == "Darwin" and tuple( map(int, platform.mac_ver()[0].split(".")) ) >= (10, 14): @pytest.fixture(autouse=True, params=[True, False]) def mock_macos_version_10_13(request): import truststore._macos prev = truststore._macos._is_macos_version_10_14_or_later truststore._macos._is_macos_version_10_14_or_later = request.param try: yield finally: truststore._macos._is_macos_version_10_14_or_later = prev @pytest.fixture def trustme_ca(): # 'trustme' is optional to allow testing on Python 3.13 try: import trustme except ImportError: pytest.skip("Test requires 'trustme' to be installed") ca = trustme.CA() yield ca @pytest.fixture def httpserver_ssl_context(trustme_ca): server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) server_cert = trustme_ca.issue_cert("localhost") server_cert.configure_cert(server_context) return server_context # Changes pytest-httpserver fixture to be scope='function' instead of 'session'. @pytest.fixture(scope="function") def make_httpserver(httpserver_listen_address, httpserver_ssl_context): host, port = httpserver_listen_address if not host: host = HTTPServer.DEFAULT_LISTEN_HOST if not port: port = HTTPServer.DEFAULT_LISTEN_PORT server = HTTPServer(host=host, port=port, ssl_context=httpserver_ssl_context) server.start() yield server server.clear() if server.is_running(): server.stop() def connect_to_host( host: str, use_server_hostname: bool = True, verify_flags=ssl.VERIFY_CRL_CHECK_CHAIN ): with socket.create_connection((host, 443)) as sock: ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) if verify_flags and platform.system() != "Linux": ctx.verify_flags |= verify_flags with ctx.wrap_socket( sock, server_hostname=host if use_server_hostname else None ): pass @successful_hosts def test_success(host): connect_to_host(host) @failure_hosts def test_failures(failure): with pytest.raises(ssl.SSLCertVerificationError) as e: connect_to_host(failure.host) error_repr = repr(e.value) assert any(message in error_repr for message in failure.error_messages), error_repr @successful_hosts def test_success_after_loading_additional_anchors(host, trustme_ca): with socket.create_connection((host, 443)) as sock: ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # See if loading additional anchors still uses system anchors. trustme_ca.configure_trust(ctx) with ctx.wrap_socket(sock, server_hostname=host): pass @failure_hosts def test_failure_after_loading_additional_anchors(failure, trustme_ca): with ( pytest.raises(ssl.SSLCertVerificationError) as e, socket.create_connection((failure.host, 443)) as sock, ): ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) if platform.system() != "Linux": ctx.verify_flags |= ssl.VERIFY_CRL_CHECK_CHAIN # See if loading additional anchors still fails. trustme_ca.configure_trust(ctx) with ctx.wrap_socket(sock, server_hostname=failure.host): pass error_repr = repr(e.value) assert any(message in error_repr for message in failure.error_messages), error_repr @failure_hosts_no_revocation def test_failures_without_revocation_checks(failure): # On macOS with revocation checks required, we get a # "certificates do not meet pinning requirements" # error for some of the badssl certs. So let's also test # with revocation checks disabled and make sure we get the # expected error messages in that case. with pytest.raises(ssl.SSLCertVerificationError) as e: connect_to_host(failure.host, verify_flags=None) error_repr = repr(e.value) assert any(message in error_repr for message in failure.error_messages), error_repr @successful_hosts def test_sslcontext_api_success(host): if host == "1.1.1.1": pytest.skip("urllib3 doesn't pass server_hostname for IP addresses") ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) with urllib3.PoolManager(ssl_context=ctx, retries=5) as http: resp = http.request("GET", f"https://{host}") # Cloudflare rejects some of our CI requests and # that's okay because we only care about the TLS handshake. assert resp.status in (200, 403) assert len(resp.data) > 0 @successful_hosts @pytest.mark.filterwarnings("ignore:enable_cleanup_closed ignored.*:DeprecationWarning") @pytest.mark.asyncio async def test_sslcontext_api_success_async(host): ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # connector avoids https://github.com/aio-libs/aiohttp/issues/5426 async with aiohttp.ClientSession( connector=aiohttp.TCPConnector(force_close=True, enable_cleanup_closed=True) ) as http: resp = await http.request("GET", f"https://{host}", ssl=ctx) assert resp.status == 200 assert len(await resp.text()) > 0 @failure_hosts def test_sslcontext_api_failures(failure): host = failure.host ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) if platform.system() != "Linux": ctx.verify_flags |= ssl.VERIFY_CRL_CHECK_CHAIN with urllib3.PoolManager(ssl_context=ctx) as http: with pytest.raises(urllib3.exceptions.SSLError) as e: http.request("GET", f"https://{host}", retries=False) assert "cert" in repr(e.value).lower() and "verif" in repr(e.value).lower() @failure_hosts @pytest.mark.filterwarnings("ignore:enable_cleanup_closed ignored.*:DeprecationWarning") @pytest.mark.asyncio async def test_sslcontext_api_failures_async(failure): ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) if platform.system() != "Linux": ctx.verify_flags |= ssl.VERIFY_CRL_CHECK_CHAIN # connector avoids https://github.com/aio-libs/aiohttp/issues/5426 async with aiohttp.ClientSession( connector=aiohttp.TCPConnector(force_close=True, enable_cleanup_closed=True) ) as http: with pytest.raises( aiohttp.client_exceptions.ClientConnectorCertificateError ) as e: await http.request("GET", f"https://{failure.host}", ssl=ctx) assert "cert" in repr(e.value).lower() and "verif" in repr(e.value).lower() @successful_hosts def test_requests_sslcontext_api_success(host): if host == "1.1.1.1": pytest.skip("urllib3 doesn't pass server_hostname for IP addresses") ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) with requests.Session() as http: http.mount("https://", SSLContextAdapter(ssl_context=ctx)) resp = http.request("GET", f"https://{host}") assert resp.status_code == 200 assert len(resp.content) > 0 @failure_hosts def test_requests_sslcontext_api_failures(failure): host = failure.host ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) if platform.system() != "Linux": ctx.verify_flags |= ssl.VERIFY_CRL_CHECK_CHAIN with requests.Session() as http: http.mount("https://", SSLContextAdapter(ssl_context=ctx)) with pytest.raises(requests.exceptions.SSLError) as e: http.request("GET", f"https://{host}") assert "cert" in repr(e.value).lower() and "verif" in repr(e.value).lower() @pytest.mark.internet def test_wrong_host_succeeds_with_hostname_verification_disabled() -> None: global wrong_host_failure_host ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False assert ctx.check_hostname is False with urllib3.PoolManager(ssl_context=ctx, retries=5, assert_hostname=False) as http: resp = http.request("GET", f"https://{wrong_host_failure_host.host}") assert resp.status == 200 assert len(resp.data) > 0 assert ctx.check_hostname is False def test_trustme_cert(trustme_ca, httpserver): ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) trustme_ca.configure_trust(ctx) httpserver.expect_request("/", method="GET").respond_with_json({}) with urllib3.PoolManager(ssl_context=ctx) as http: resp = http.request("GET", httpserver.url_for("/")) assert resp.status == 200 assert len(resp.data) > 0 def test_trustme_cert_loaded_via_capath(trustme_ca, httpserver): from OpenSSL.crypto import X509 ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) with tempfile.TemporaryDirectory() as capath: with open(f"{capath}/cert.pem", "wb") as certfile: certfile.write(trustme_ca.cert_pem.bytes()) cert_hash = X509.from_cryptography(trustme_ca._certificate).subject_name_hash() os.symlink(f"{capath}/cert.pem", f"{capath}/{cert_hash:x}.0") assert set(os.listdir(capath)) == {"cert.pem", f"{cert_hash:x}.0"} ctx.load_verify_locations(capath=capath) httpserver.expect_request("/", method="GET").respond_with_json({}) with urllib3.PoolManager(ssl_context=ctx) as http: resp = http.request("GET", httpserver.url_for("/")) assert resp.status == 200 assert len(resp.data) > 0 @pytest.mark.internet def test_trustme_cert_still_uses_system_certs(trustme_ca): ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) trustme_ca.configure_trust(ctx) with urllib3.PoolManager(ssl_context=ctx) as http: resp = http.request("GET", "https://example.com") assert resp.status == 200 assert len(resp.data) > 0 def test_macos_10_7_import_error(): with mock.patch("platform.mac_ver") as mac_ver: # This isn't the full structure, but the version is the first element. mac_ver.return_value = ("10.7",) with pytest.raises(ImportError) as e: # We want to force a fresh import, so either we get it on the # first try because the OS isn't macOS or we get it on # the call to importlib.reload(...). import truststore._macos importlib.reload(truststore._macos) assert str(e.value) == "Only OS X 10.8 and newer are supported, not 10.7" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692463791.1241834 truststore-0.10.1/tests/test_custom_ca.py0000644000000000000000000000241014470171257015512 0ustar00import asyncio import ssl import pytest import requests import urllib3 from aiohttp import ClientSession import truststore from tests import SSLContextAdapter from tests.conftest import Server @pytest.mark.asyncio async def test_urllib3_custom_ca(server: Server) -> None: def test_urllib3(): ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) with urllib3.PoolManager(ssl_context=ctx) as client: resp = client.request("GET", server.base_url) assert resp.status == 200 thread = asyncio.to_thread(test_urllib3) await thread @pytest.mark.asyncio async def test_aiohttp_custom_ca(server: Server) -> None: ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) async with ClientSession() as client: resp = await client.get(server.base_url, ssl=ctx) assert resp.status == 200 @pytest.mark.asyncio async def test_requests_custom_ca(server: Server) -> None: def test_requests(): ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) with requests.Session() as http: http.mount("https://", SSLContextAdapter(ssl_context=ctx)) resp = http.request("GET", server.base_url) assert resp.status_code == 200 thread = asyncio.to_thread(test_requests) await thread ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1729716774.7134526 truststore-0.10.1/tests/test_inject.py0000644000000000000000000000776314706261047015030 0ustar00import asyncio import pathlib import ssl import sys import httpx import pytest import urllib3 import truststore from tests.conftest import Server, successful_hosts @pytest.fixture(scope="function") def inject_truststore(): truststore.inject_into_ssl() try: yield finally: truststore.extract_from_ssl() def test_inject_and_extract(): assert ssl.SSLContext is not truststore.SSLContext try: original_SSLContext = ssl.SSLContext ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) assert isinstance(ctx._ctx, original_SSLContext) truststore.inject_into_ssl() assert ssl.SSLContext is truststore.SSLContext assert urllib3.util.ssl_.SSLContext is truststore.SSLContext ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) assert isinstance(ctx._ctx, original_SSLContext) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) assert isinstance(ctx, truststore.SSLContext) ctx = ssl.create_default_context() assert isinstance(ctx, truststore.SSLContext) truststore.extract_from_ssl() assert ssl.SSLContext is original_SSLContext assert urllib3.util.ssl_.SSLContext is original_SSLContext ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) assert isinstance(ctx._ctx, original_SSLContext) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) assert isinstance(ctx, original_SSLContext) ctx = ssl.create_default_context() assert isinstance(ctx, original_SSLContext) finally: truststore.extract_from_ssl() @successful_hosts @pytest.mark.usefixtures("inject_truststore") def test_success_with_inject(host): with urllib3.PoolManager() as http: resp = http.request("GET", f"https://{host}") assert resp.status == 200 @pytest.mark.usefixtures("inject_truststore") def test_inject_set_values(): ctx = ssl.create_default_context() assert isinstance(ctx, truststore.SSLContext) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE assert ctx.check_hostname is False assert ctx.verify_mode == ssl.CERT_NONE @pytest.mark.asyncio @pytest.mark.usefixtures("inject_truststore") async def test_urllib3_works_with_inject(server: Server) -> None: def test_urllib3(): with urllib3.PoolManager() as client: resp = client.request("GET", server.base_url) assert resp.status == 200 thread = asyncio.to_thread(test_urllib3) await thread @pytest.mark.asyncio async def test_aiohttp_works_with_inject(server: Server) -> None: # We completely isolate the requests module because # pytest or some other part of our test infra is messing # with the order it's loaded into modules. script = str(pathlib.Path(__file__).parent / "aiohttp_with_inject.py") proc = await asyncio.create_subprocess_exec(sys.executable, script) await proc.wait() assert proc.returncode == 200 @pytest.mark.asyncio async def test_requests_works_with_inject(server: Server) -> None: # We completely isolate the requests module because # pytest or some other part of our test infra is messing # with the order it's loaded into modules. script = str(pathlib.Path(__file__).parent / "requests_with_inject.py") proc = await asyncio.create_subprocess_exec(sys.executable, script) await proc.wait() assert proc.returncode == 200 @pytest.mark.asyncio @pytest.mark.usefixtures("inject_truststore") async def test_sync_httpx_works_with_inject(server: Server) -> None: def test_httpx(): with httpx.Client() as client: resp = client.request("GET", server.base_url) assert resp.status_code == 200 thread = asyncio.to_thread(test_httpx) await thread @pytest.mark.usefixtures("inject_truststore") @pytest.mark.asyncio async def test_async_httpx_works_with_inject(server: Server) -> None: async with httpx.AsyncClient() as client: resp = await client.request("GET", server.base_url) assert resp.status_code == 200 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1715015092.7446237 truststore-0.10.1/tests/test_sslcontext.py0000644000000000000000000000317714616206665015762 0ustar00import json import ssl import pytest import urllib3 from urllib3.exceptions import InsecureRequestWarning, SSLError import truststore @pytest.mark.internet def test_minimum_maximum_version(): ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.maximum_version = ssl.TLSVersion.TLSv1_2 with urllib3.PoolManager(ssl_context=ctx) as http: resp = http.request("GET", "https://howsmyssl.com/a/check") data = json.loads(resp.data) assert data["tls_version"] == "TLS 1.2" assert ctx.minimum_version in ( ssl.TLSVersion.TLSv1_2, ssl.TLSVersion.MINIMUM_SUPPORTED, ) assert ctx.maximum_version == ssl.TLSVersion.TLSv1_2 @pytest.mark.internet def test_check_hostname_false(): ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) assert ctx.check_hostname is True assert ctx.verify_mode == ssl.CERT_REQUIRED with urllib3.PoolManager(ssl_context=ctx, retries=False) as http: with pytest.raises(SSLError) as e: http.request("GET", "https://wrong.host.badssl.com/") assert "match" in str(e.value) @pytest.mark.internet def test_verify_mode_cert_none(): ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) assert ctx.check_hostname is True assert ctx.verify_mode == ssl.CERT_REQUIRED ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE assert ctx.check_hostname is False assert ctx.verify_mode == ssl.CERT_NONE with ( urllib3.PoolManager(ssl_context=ctx) as http, pytest.warns(InsecureRequestWarning) as w, ): http.request("GET", "https://expired.badssl.com/") assert len(w) == 1 truststore-0.10.1/PKG-INFO0000644000000000000000000001050200000000000012011 0ustar00Metadata-Version: 2.3 Name: truststore Version: 0.10.1 Summary: Verify certificates using native system trust stores Author-email: Seth Michael Larson , David Glick Requires-Python: >= 3.10 Description-Content-Type: text/markdown Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Project-URL: Documentation, https://truststore.readthedocs.io Project-URL: Source, https://github.com/sethmlarson/truststore # Truststore [![PyPI](https://img.shields.io/pypi/v/truststore)](https://pypi.org/project/truststore) [![CI](https://github.com/sethmlarson/truststore/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/sethmlarson/truststore/actions/workflows/ci.yml) Truststore is a library which exposes native system certificate stores (ie "trust stores") through an `ssl.SSLContext`-like API. This means that Python applications no longer need to rely on certifi as a root certificate store. Native system certificate stores have many helpful features compared to a static certificate bundle like certifi: - Automatically update certificates as new CAs are created and removed - Fetch missing intermediate certificates - Check certificates against certificate revocation lists (CRLs) to avoid monster-in-the-middle (MITM) attacks - Managed per-system rather than per-application by a operations/IT team - PyPI is no longer a CA distribution channel 🥳 Right now truststore is a stand-alone library that can be installed globally in your application to immediately take advantage of the benefits in Python 3.10+. Truststore has also been integrated into pip 24.2+ as the default method for verifying HTTPS certificates (with a fallback to certifi). Long-term the hope is to add this functionality into Python itself. Wish us luck! ## Installation Truststore is installed from [PyPI](https://pypi.org/project/truststore) with pip: ```{code-block} shell $ python -m pip install truststore ``` Truststore **requires Python 3.10 or later** and supports the following platforms: - macOS 10.8+ via [Security framework](https://developer.apple.com/documentation/security) - Windows via [CryptoAPI](https://docs.microsoft.com/en-us/windows/win32/seccrypto/cryptography-functions#certificate-verification-functions) - Linux via OpenSSL ## User Guide > **Warning** > **PLEASE READ:** `inject_into_ssl()` **must not be used by libraries or packages** as it will cause issues on import time when integrated with other libraries. > Libraries and packages should instead use `truststore.SSLContext` directly which is detailed below. > > The `inject_into_ssl()` function is intended only for use in applications and scripts. You can inject `truststore` into the standard library `ssl` module so the functionality is used by every library by default. To do so use the `truststore.inject_into_ssl()` function: ```python import truststore truststore.inject_into_ssl() # Automatically works with urllib3, requests, aiohttp, and more: import urllib3 http = urllib3.PoolManager() resp = http.request("GET", "https://example.com") import aiohttp http = aiohttp.ClientSession() resp = await http.request("GET", "https://example.com") import requests resp = requests.get("https://example.com") ``` If you'd like finer-grained control or you're developing a library or package you can create your own `truststore.SSLContext` instance and use it anywhere you'd use an `ssl.SSLContext`: ```python import ssl import truststore ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) import urllib3 http = urllib3.PoolManager(ssl_context=ctx) resp = http.request("GET", "https://example.com") ``` You can read more in the [user guide in the documentation](https://truststore.readthedocs.io/en/latest/#user-guide). ## License MIT