diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03a8c41..f59842f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] - python-version: ["3.10", "3.11", "3.12-dev"] + python-version: ["3.10", "3.11", "3.12", "pypy3.10"] runs-on: ${{ matrix.os }} name: ${{ fromJson('{"macos-latest":"macOS","windows-latest":"Windows","ubuntu-latest":"Ubuntu"}')[matrix.os] }} Python ${{ matrix.python-version }} @@ -49,9 +49,10 @@ jobs: uses: actions/checkout@v3 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Setup Go uses: actions/setup-go@v3 diff --git a/pyproject.toml b/pyproject.toml index ac871dc..fb56453 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ readme = "README.md" license = {file = "LICENSE"} classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", @@ -21,6 +21,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", ] dynamic = ["version", "description"] requires-python = ">= 3.10" diff --git a/src/truststore/_api.py b/src/truststore/_api.py index 571ea3c..3c28065 100644 --- a/src/truststore/_api.py +++ b/src/truststore/_api.py @@ -6,7 +6,12 @@ import _ssl # type: ignore[import] -from ._ssl_constants import _original_SSLContext, _original_super_SSLContext +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 @@ -49,9 +54,16 @@ def extract_from_ssl() -> None: pass -class SSLContext(ssl.SSLContext): +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) @@ -240,7 +252,7 @@ def protocol(self) -> ssl._SSLMethod: return self._ctx.protocol @property - def security_level(self) -> int: # type: ignore[override] + def security_level(self) -> int: return self._ctx.security_level @property diff --git a/src/truststore/_ssl_constants.py b/src/truststore/_ssl_constants.py index be60f83..b1ee7a3 100644 --- a/src/truststore/_ssl_constants.py +++ b/src/truststore/_ssl_constants.py @@ -1,10 +1,29 @@ import 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 diff --git a/tests/conftest.py b/tests/conftest.py index c9b23a2..938611c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -140,9 +140,13 @@ async def handler(request: web.Request) -> web.Response: 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=mkcert_certs.cert_file, - keyfile=mkcert_certs.key_file, + certfile=str(mkcert_certs.cert_file), + keyfile=str(mkcert_certs.key_file), ) # we need keepalive_timeout=0