truststore-0.9.1/LICENSE0000644000000000000000000000207614616213065011723 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. truststore-0.9.1/README.md0000644000000000000000000000650714616213065012200 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 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 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 truststore-0.9.1/docs/Makefile0000644000000000000000000000117714616213065013307 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) truststore-0.9.1/docs/make.bat0000644000000000000000000000144514616213065013252 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 truststore-0.9.1/docs/requirements.txt0000644000000000000000000000003714616213065015125 0ustar00sphinx>=7.2.2 furo myst-parser truststore-0.9.1/docs/source/conf.py0000644000000000000000000000115414616213065014441 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 truststore-0.9.1/docs/source/index.md0000644000000000000000000001436214616213065014600 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() http = aiohttp.ClientSession() resp = await http.request("GET", "https://example.com") ``` 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) http = aiohttp.ClientSession(ssl=ctx) resp = await http.request("GET", "https://example.com") ``` ### 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 truststore-0.9.1/pyproject.toml0000644000000000000000000000255514616213065013634 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" filterwarnings = [ "error", # See: aio-libs/aiohttp#7545 "ignore:.*datetime.utcfromtimestamp().*:DeprecationWarning", ] markers = [ "internet: test requires Internet access" ] [tool.flit.sdist] include = ["docs", "tests"] truststore-0.9.1/src/truststore/__init__.py0000644000000000000000000000062314616213065016050 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") 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.9.1" truststore-0.9.1/src/truststore/_api.py0000644000000000000000000002427114616213065015226 0ustar00import os import platform import socket import ssl import sys import typing import _ssl # type: ignore[import-not-found] 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 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() @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 ) truststore-0.9.1/src/truststore/_macos.py0000644000000000000000000004231014616213065015551 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]}" ) 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.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: raise ImportError("Error initializing ctypes") 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] 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 cf_error = None try: if server_hostname is not None: 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) cf_error = CoreFoundation.CFErrorRef() sec_trust_eval_result = Security.SecTrustEvaluateWithError( trust, 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 elif ( not ssl_context.check_hostname and cf_error_code == CFConst.errSecHostNameMismatch ): 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( trust, 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) finally: if policies: CoreFoundation.CFRelease(policies) if trust: CoreFoundation.CFRelease(trust) truststore-0.9.1/src/truststore/_openssl.py0000644000000000000000000000442414616213065016136 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 truststore-0.9.1/src/truststore/_ssl_constants.py0000644000000000000000000000215214616213065017344 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] truststore-0.9.1/src/truststore/_windows.py0000644000000000000000000004274314616213065016153 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 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 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 if not ssl_context.check_hostname: chain_policy.dwFlags |= CERT_CHAIN_POLICY_IGNORE_INVALID_NAME_FLAG 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) truststore-0.9.1/src/truststore/py.typed0000644000000000000000000000000014616213065015423 0ustar00truststore-0.9.1/tests/__init__.py0000644000000000000000000000106114616213065014162 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) truststore-0.9.1/tests/conftest.py0000644000000000000000000001225514616213065014257 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() truststore-0.9.1/tests/test_api.py0000644000000000000000000003243014616213065014237 0ustar00import asyncio import 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] failure_hosts_list = [ 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", # Windows "The certificate's CN name does not match the passed value.", ], ), 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", # 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", # 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", # 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", # 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", # Windows # "The certificate is revoked.", # TODO: Temporary while certificate is expired on badssl.com. # Test will start failing against once the certificate is fixed. # macOS '"revoked.badssl.com","RapidSSL TLS DV RSA Mixed SHA256 2020 CA-1","DigiCert Global Root CA" certificates do not meet pinning requirements', "“revoked.badssl.com” certificate is expired", # Windows "A required certificate is not within its validity period when verifying against the current system clock or the timestamp in the signed file.", ], ) ) failure_hosts = decorator_requires_internet( pytest.mark.parametrize("failure", failure_hosts_list, ids=attrgetter("host")) ) @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) # 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}") assert resp.status == 200 assert len(resp.data) > 0 @successful_hosts @pytest.mark.asyncio async def test_sslcontext_api_success_async(host): ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) async with aiohttp.ClientSession() as http: resp = await http.request("GET", f"https://{host}", ssl=ctx) assert resp.status == 200 assert len(await resp.text()) > 0 # workaround https://github.com/aio-libs/aiohttp/issues/5426 await asyncio.sleep(0.2) @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.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 async with aiohttp.ClientSession() as http: with pytest.raises( aiohttp.client_exceptions.ClientConnectorCertificateError ) as e: await http.request("GET", f"https://{failure.host}", ssl=ctx) # workaround https://github.com/aio-libs/aiohttp/issues/5426 await asyncio.sleep(0.2) 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() 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" truststore-0.9.1/tests/test_custom_ca.py0000644000000000000000000000241014616213065015436 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 truststore-0.9.1/tests/test_inject.py0000644000000000000000000000734614616213065014752 0ustar00import asyncio import ssl import httpx import pytest import requests import urllib3 from aiohttp import ClientSession 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 @pytest.mark.usefixtures("inject_truststore") async def test_aiohttp_works_with_inject(server: Server) -> None: async with ClientSession() as client: resp = await client.get(server.base_url) assert resp.status == 200 @pytest.mark.asyncio @pytest.mark.usefixtures("inject_truststore") async def test_requests_works_with_inject(server: Server) -> None: def test_requests(): with requests.Session() as http: resp = http.request("GET", server.base_url) assert resp.status_code == 200 thread = asyncio.to_thread(test_requests) await thread @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 truststore-0.9.1/tests/test_sslcontext.py0000644000000000000000000000317714616213065015702 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.9.1/PKG-INFO0000644000000000000000000001061200000000000011743 0ustar00Metadata-Version: 2.1 Name: truststore Version: 0.9.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 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 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