Skip to content

Commit

Permalink
Add support for injecting truststore.SSLContext into the ssl module
Browse files Browse the repository at this point in the history
  • Loading branch information
sethmlarson authored Mar 5, 2023
1 parent 63dc9e1 commit fcd59c8
Show file tree
Hide file tree
Showing 12 changed files with 513 additions and 253 deletions.
67 changes: 39 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,60 @@
[![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)

Verify certificates using OS trust stores. Supports macOS, Windows, and Linux (with OpenSSL). **This project should be considered experimental.**
Verify certificates using OS trust stores. This is useful when your system contains
custom certificate authorities such as when using a corporate proxy or using test certificates.
Supports macOS, Windows, and Linux (with OpenSSL).

## Usage
## Installation

```python
# The following code works on Linux, macOS, and Windows without dependencies.
import socket
import ssl
import truststore
Truststore is installed from [PyPI](https://pypi.org/project/truststore) with pip:

# Create an SSLContext for the system trust store
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
```{code-block} shell
$ python -m pip install truststore
```

# Connect to the peer and initiate a TLS handshake
sock = socket.create_connection(("example.com", 443))
sock = ctx.wrap_socket(sock, server_hostname="example.com")
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

# Also works with libraries that accept an SSLContext object
import urllib3
## User Guide

http = urllib3.PoolManager(ssl_context=ctx)
http.request("GET", "https://example.com")
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:

# Works with ssl.MemoryBIO objects for async I/O
import aiohttp
```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()
await http.request("GET", "https://example.com", ssl=ctx)
resp = await http.request("GET", "https://example.com")

import requests
resp = requests.get("https://example.com")
```

## Platforms
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`:

Works in the following configurations:
```python
import ssl
import truststore

- 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
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

## Prior art
import urllib3
http = urllib3.PoolManager(ssl_context=ctx)
resp = http.request("GET", "https://example.com")
```

- [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)
You can read more in the [user guide in the documentation](https://truststore.readthedocs.io/en/latest/#user-guide).

## License

Expand Down
142 changes: 93 additions & 49 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,103 +5,147 @@
:caption: Contents
```

Verify certificates using OS trust stores. Supports macOS, Windows, and Linux (with OpenSSL).
Verify certificates using OS trust stores. This is useful when your system contains
custom certificate authorities such as when using a corporate proxy or using test certificates.
Supports macOS, Windows, and Linux (with OpenSSL).

```{warning}
This project should be considered experimental so shouldn't be used in production.
```

## Platforms
## Installation

- Requires Python 3.10 or later
- Supports macOS 10.8+ via [Security framework](https://developer.apple.com/documentation/security)
- Supports Windows via [CryptoAPI](https://docs.microsoft.com/en-us/windows/win32/seccrypto/cryptography-functions#certificate-verification-functions)
- Supports Linux via OpenSSL
Truststore can be installed from [PyPI](https://pypi.org/project/truststore) with pip:

## Usage
```{code-block} shell
$ python -m pip install truststore
```

The `truststore` module has a single API: `truststore.SSLContext`
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

```{code-block} python
import truststore
## User Guide

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)

ctx = truststore.SSLContext()
import urllib3
http = urllib3.PoolManager(ssl_context=ctx)
resp = http.request("GET", "https://example.com")
```

### Using truststore with pip

Pip v22.2 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:
[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
# 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

This `SSLContext` works the same as an {py:class}`ssl.SSLContext`.
You can use it anywhere you would use an {py:class}`ssl.SSLContext` and
system trust stores are automatically used to verify peer certificates:
```{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
import ssl
import urllib3
import truststore
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
http = urllib3.PoolManager(ssl_context=ctx)
resp = http.request("GET", "https://example.com")
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 ssl
import aiohttp
import truststore
import aiohttp
import truststore
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
truststore.inject_into_ssl()
http = aiohttp.ClientSession(ssl=ctx)
resp = await http.request("GET", "https://example.com")
http = aiohttp.ClientSession()
resp = await http.request("GET", "https://example.com")
```

### Using truststore with Requests

Requests doesn't support passing an {py:class}`ssl.SSLContext` object to a `requests.Session` object directly so there's an additional class you need to inject the `truststore.SSLContext` instance to the lower-level {py:class}`urllib3.PoolManager` instance:
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 requests
import requests.adapters
import truststore
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")
```

class SSLContextAdapter(requests.adapters.HTTPAdapter):
def __init__(self, *, ssl_context=None, **kwargs):
self._ssl_context = ssl_context
super().__init__(**kwargs)
### Using truststore with Requests

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)
Just like with `urllib3` using `truststore.inject_into_ssl()` is the easiest method for using Truststore with Requests:

ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
```{code-block} python
import requests
import truststore
http = requests.Session()
adapter = SSLContextAdapter(ssl_context=ctx)
http.mount("https://", adapter)
truststore.inject_into_ssl()
resp = http.request("GET", "https://example.com")
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)
Expand Down
11 changes: 9 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ def lint(session):
session.run("flake8", "--ignore=E501,W503", *SOURCE_PATHS)
session.run("black", "--check", *SOURCE_PATHS)
session.run("isort", "--check", "--profile=black", *SOURCE_PATHS)
session.run("mypy", "--strict", "--show-error-codes", "src/")
session.run(
"mypy",
"--strict",
"--show-error-codes",
"--install-types",
"--non-interactive",
"src/",
)


@nox.session(python=PYTHONS)
Expand All @@ -52,7 +59,7 @@ def test(session):
"-rs",
"--no-flaky-report",
"--max-runs=3",
*(session.posargs or ("tests/",))
*(session.posargs or ("tests/",)),
)


Expand Down
12 changes: 8 additions & 4 deletions src/truststore/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
"""Verify certificates using OS trust stores"""
"""Verify certificates using OS trust stores. This is useful when your system contains
custom certificate authorities such as when using a corporate proxy or using test certificates.
Supports macOS, Windows, and Linux (with OpenSSL).
"""

import sys as _sys

if _sys.version_info < (3, 10):
raise ImportError("truststore requires Python 3.10 or later")
del _sys

from ._api import SSLContext # noqa: E402
from ._api import SSLContext, extract_from_ssl, inject_into_ssl # noqa: E402

__all__ = ["SSLContext"]
del _api, _sys # type: ignore[name-defined] # noqa: F821

__all__ = ["SSLContext", "inject_into_ssl", "extract_from_ssl"]
__version__ = "0.5.0"
Loading

0 comments on commit fcd59c8

Please sign in to comment.