diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index ce3df5db81..bf8d788369 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: "actions/checkout@v4" diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e2afe2e62..94ebae0afc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## 0.28.0 (...) +TODO... writeup `truststore` switch & 3.10+ requirement. + The 0.28 release includes a limited set of backwards incompatible changes. **Backwards incompatible changes**: SSL configuration has been significantly simplified. -* The `verify` argument no longer accepts string arguments. -* The `cert` argument has now been removed. -* The `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables are no longer automatically used. +* The `verify` argument no longer accepts string arguments. Explicitly specified certificate stores can still be enabled through the SSL configuration API. +* The `cert` argument has now been removed. Client side certificates can still be enabled through the SSL configuration API. +* The `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables are no longer used. These environment variables can be enabled manually although should be obsoleted by our switch to `truststore`. For users of the standard `verify=True` or `verify=False` cases this should require no changes. diff --git a/README.md b/README.md index 23992d9c24..b2709c322b 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Or, to include the optional HTTP/2 support, use: $ pip install httpx[http2] ``` -HTTPX requires Python 3.8+. +HTTPX requires Python 3.10+. ## Documentation @@ -125,7 +125,7 @@ The HTTPX project relies on these excellent libraries: * `httpcore` - The underlying transport implementation for `httpx`. * `h11` - HTTP/1.1 support. -* `certifi` - SSL certificates. +* `truststore` - System SSL certificates. * `idna` - Internationalized domain name support. * `sniffio` - Async library autodetection. diff --git a/docs/advanced/ssl.md b/docs/advanced/ssl.md index da40ed2843..3acddc7b40 100644 --- a/docs/advanced/ssl.md +++ b/docs/advanced/ssl.md @@ -1,26 +1,28 @@ -When making a request over HTTPS, HTTPX needs to verify the identity of the requested host. To do this, it uses a bundle of SSL certificates (a.k.a. CA bundle) delivered by a trusted certificate authority (CA). +When making a request over HTTPS we need to verify the identity of the requested host. We rely on the [`truststore`](https://truststore.readthedocs.io/en/latest/) package to load the system certificates, ensuring that `httpx` has the same behaviour on SSL sites as your browser. -### Enabling and disabling verification +### SSL verification By default httpx will verify HTTPS connections, and raise an error for invalid SSL cases... -```pycon +```python >>> httpx.get("https://expired.badssl.com/") httpx.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:997) ``` -You can disable SSL verification completely and allow insecure requests... +If you're confident that you want to visit a site with an invalid certificate you can disable SSL verification completely... -```pycon +```python >>> httpx.get("https://expired.badssl.com/", verify=False) ``` -### Configuring client instances +### Custom SSL configurations -If you're using a `Client()` instance you should pass any `verify=<...>` configuration when instantiating the client. +If you're using a `Client()` instance you can pass the `verify=<...>` configuration when instantiating the client. -By default the [certifi CA bundle](https://certifiio.readthedocs.io/en/latest/) is used for SSL verification. +```python +>>> client = httpx.Client(verify=True) +``` For more complex configurations you can pass an [SSL Context](https://docs.python.org/3/library/ssl.html) instance... @@ -28,35 +30,13 @@ For more complex configurations you can pass an [SSL Context](https://docs.pytho import certifi import httpx import ssl +import certifi -# This SSL context is equivelent to the default `verify=True`. +# Use certifi for certificate validation, rather than the system truststore. ctx = ssl.create_default_context(cafile=certifi.where()) client = httpx.Client(verify=ctx) ``` -Using [the `truststore` package](https://truststore.readthedocs.io/) to support system certificate stores... - -```python -import ssl -import truststore -import httpx - -# Use system certificate stores. -ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) -client = httpx.Client(verify=ctx) -``` - -Loding an alternative certificate verification store using [the standard SSL context API](https://docs.python.org/3/library/ssl.html)... - -```python -import httpx -import ssl - -# Use an explicitly configured certificate store. -ctx = ssl.create_default_context(cafile="path/to/certs.pem") # Either cafile or capath. -client = httpx.Client(verify=ctx) -``` - ### Client side certificates Client side certificates allow a remote server to verify the client. They tend to be used within private organizations to authenticate requests to remote servers. @@ -71,9 +51,9 @@ client = httpx.Client(verify=ctx) ### Working with `SSL_CERT_FILE` and `SSL_CERT_DIR` -Unlike `requests`, the `httpx` package does not automatically pull in [the environment variables `SSL_CERT_FILE` or `SSL_CERT_DIR`](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_default_verify_paths.html). If you want to use these they need to be enabled explicitly. +Unlike `requests`, the `httpx` package does not automatically pull in [the environment variables `SSL_CERT_FILE` or `SSL_CERT_DIR`](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_default_verify_paths.html). -For example... +These environment variables shouldn't be necessary since they're obsoleted by `truststore`. They can be enabled if required like so... ```python # Use `SSL_CERT_FILE` or `SSL_CERT_DIR` if configured. @@ -87,7 +67,7 @@ client = httpx.Client(verify=ctx) ### Making HTTPS requests to a local server -When making requests to local servers, such as a development server running on `localhost`, you will typically be using unencrypted HTTP connections. +When making requests to local servers such as a development server running on `localhost`, you will typically be using unencrypted HTTP connections. If you do need to make HTTPS connections to a local server, for example to test an HTTPS-only service, you will need to create and use your own certificates. Here's one way to do it... diff --git a/httpx/_config.py b/httpx/_config.py index 9318de3c13..590a27a297 100644 --- a/httpx/_config.py +++ b/httpx/_config.py @@ -22,10 +22,10 @@ class UnsetType: def create_ssl_context(verify: ssl.SSLContext | bool = True) -> ssl.SSLContext: import ssl - import certifi + import truststore if verify is True: - return ssl.create_default_context(cafile=certifi.where()) + return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) elif verify is False: ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.check_hostname = False diff --git a/pyproject.toml b/pyproject.toml index 9e67191135..46c147caa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "certifi", + "truststore==0.10.0", "httpcore==1.*", "anyio", "idna", diff --git a/tests/test_config.py b/tests/test_config.py index 22abd4c22c..d4e67cec35 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,8 +1,5 @@ import ssl -import typing -from pathlib import Path -import certifi import pytest import httpx @@ -20,26 +17,6 @@ def test_load_ssl_config_verify_non_existing_file(): context.load_verify_locations(cafile="/path/to/nowhere") -def test_load_ssl_with_keylog(monkeypatch: typing.Any) -> None: - monkeypatch.setenv("SSLKEYLOGFILE", "test") - context = httpx.create_ssl_context() - assert context.keylog_filename == "test" - - -def test_load_ssl_config_verify_existing_file(): - context = httpx.create_ssl_context() - context.load_verify_locations(capath=certifi.where()) - assert context.verify_mode == ssl.VerifyMode.CERT_REQUIRED - assert context.check_hostname is True - - -def test_load_ssl_config_verify_directory(): - context = httpx.create_ssl_context() - context.load_verify_locations(capath=Path(certifi.where()).parent) - assert context.verify_mode == ssl.VerifyMode.CERT_REQUIRED - assert context.check_hostname is True - - def test_load_ssl_config_cert_and_key(cert_pem_file, cert_private_key_file): context = httpx.create_ssl_context() context.load_cert_chain(cert_pem_file, cert_private_key_file)