Skip to content

Commit 2763690

Browse files
karpetrosyanlovelydinosaurT-256
authored
Add httpx.SSLContext configuration. (#3022)
Co-authored-by: Tom Christie <[email protected]> Co-authored-by: T-256 <[email protected]>
1 parent 4ec069b commit 2763690

File tree

17 files changed

+327
-488
lines changed

17 files changed

+327
-488
lines changed

.github/CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,9 @@ this is where our previously generated `client.pem` comes in:
211211
```
212212
import httpx
213213
214-
proxies = {"all": "http://127.0.0.1:8080/"}
214+
ssl_context = httpx.SSLContext(verify="/path/to/client.pem"))
215215
216-
with httpx.Client(proxies=proxies, verify="/path/to/client.pem") as client:
216+
with httpx.Client(proxy="http://127.0.0.1:8080/", ssl_context=ssl_context) as client:
217217
response = client.get("https://example.org")
218218
print(response.status_code) # should print 200
219219
```

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

77
## Unreleased
88

9+
* Added `httpx.SSLContext` class and `ssl_context` argument. (#3022)
10+
* Removed `cert` and `verify` arguments, you should use the `ssl_context=...` instead. (#3022)
911
* The deprecated `proxies` argument has now been removed.
1012
* The deprecated `app` argument has now been removed.
1113
* The `URL.raw` property has now been removed.

docs/advanced/ssl.md

Lines changed: 172 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,224 @@
11
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).
22

3-
## Changing the verification defaults
3+
### Enabling and disabling verification
44

5-
By default, HTTPX uses the CA bundle provided by [Certifi](https://pypi.org/project/certifi/). This is what you want in most cases, even though some advanced situations may require you to use a different set of certificates.
5+
By default httpx will verify HTTPS connections, and raise an error for invalid SSL cases...
66

7-
If you'd like to use a custom CA bundle, you can use the `verify` parameter.
7+
```pycon
8+
>>> httpx.get("https://expired.badssl.com/")
9+
httpx.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:997)
10+
```
811

9-
```python
10-
import httpx
12+
You can configure the verification using `httpx.SSLContext()`.
1113

12-
r = httpx.get("https://example.org", verify="path/to/client.pem")
14+
```pycon
15+
>>> ssl_context = httpx.SSLContext()
16+
>>> ssl_context
17+
SSLContext(verify=True)
18+
>>> httpx.get("https://www.example.com", ssl_context=ssl_context)
19+
httpx.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:997)
1320
```
1421

15-
Alternatively, you can pass a standard library `ssl.SSLContext`.
22+
For example, you can use this to disable verification completely and allow insecure requests...
1623

1724
```pycon
18-
>>> import ssl
19-
>>> import httpx
20-
>>> context = ssl.create_default_context()
21-
>>> context.load_verify_locations(cafile="/tmp/client.pem")
22-
>>> httpx.get('https://example.org', verify=context)
25+
>>> no_verify = httpx.SSLContext(verify=False)
26+
>>> no_verify
27+
SSLContext(verify=False)
28+
>>> httpx.get("https://expired.badssl.com/", ssl_context=no_verify)
2329
<Response [200 OK]>
2430
```
2531

26-
We also include a helper function for creating properly configured `SSLContext` instances.
32+
### Configuring client instances
33+
34+
If you're using a `Client()` instance, then you should pass any SSL settings when instantiating the client.
2735

2836
```pycon
29-
>>> context = httpx.create_ssl_context()
37+
>>> ssl_context = httpx.SSLContext()
38+
>>> client = httpx.Client(ssl_context=ssl_context)
3039
```
3140

32-
The `create_ssl_context` function accepts the same set of SSL configuration arguments
33-
(`trust_env`, `verify`, `cert` and `http2` arguments)
34-
as `httpx.Client` or `httpx.AsyncClient`
41+
The `client.get(...)` method and other request methods on a `Client` instance *do not* support changing the SSL settings on a per-request basis.
42+
43+
If you need different SSL settings in different cases you should use more that one client instance, with different settings on each. Each client will then be using an isolated connection pool with a specific fixed SSL configuration on all connections within that pool.
44+
45+
### Changing the verification defaults
46+
47+
By default, HTTPX uses the CA bundle provided by [Certifi](https://pypi.org/project/certifi/).
48+
49+
The following all have the same behaviour...
50+
51+
Using the default SSL context.
3552

3653
```pycon
37-
>>> import httpx
38-
>>> context = httpx.create_ssl_context(verify="/tmp/client.pem")
39-
>>> httpx.get('https://example.org', verify=context)
54+
>>> client = httpx.Client()
55+
>>> client.get("https://www.example.com")
4056
<Response [200 OK]>
4157
```
4258

43-
Or you can also disable the SSL verification entirely, which is _not_ recommended.
59+
Using the default SSL context, but specified explicitly.
4460

45-
```python
46-
import httpx
61+
```pycon
62+
>>> default = httpx.SSLContext()
63+
>>> client = httpx.Client(ssl_context=default)
64+
>>> client.get("https://www.example.com")
65+
<Response [200 OK]>
66+
```
4767

48-
r = httpx.get("https://example.org", verify=False)
68+
Using the default SSL context, with `verify=True` specified explicitly.
69+
70+
```pycon
71+
>>> default = httpx.SSLContext(verify=True)
72+
>>> client = httpx.Client(ssl_context=default)
73+
>>> client.get("https://www.example.com")
74+
<Response [200 OK]>
4975
```
5076

51-
## SSL configuration on client instances
77+
Using an SSL context, with `certifi.where()` explicitly specified.
5278

53-
If you're using a `Client()` instance, then you should pass any SSL settings when instantiating the client.
79+
```pycon
80+
>>> default = httpx.SSLContext(verify=certifi.where())
81+
>>> client = httpx.Client(ssl_context=default)
82+
>>> client.get("https://www.example.com")
83+
<Response [200 OK]>
84+
```
5485

55-
```python
56-
client = httpx.Client(verify=False)
86+
For some advanced situations may require you to use a different set of certificates, either by specifying a PEM file:
87+
88+
```pycon
89+
>>> custom_cafile = httpx.SSLContext(verify="path/to/certs.pem")
90+
>>> client = httpx.Client(ssl_context=custom_cafile)
91+
>>> client.get("https://www.example.com")
92+
<Response [200 OK]>
93+
```
94+
95+
Or by providing an certificate directory:
96+
97+
```pycon
98+
>>> custom_capath = httpx.SSLContext(verify="path/to/certs")
99+
>>> client = httpx.Client(ssl_context=custom_capath)
100+
>>> client.get("https://www.example.com")
101+
<Response [200 OK]>
102+
```
103+
104+
These usages are equivelent to using [`.load_verify_locations()`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_verify_locations) with either `cafile=...` or `capath=...`.
105+
106+
### Client side certificates
107+
108+
You can also specify a local cert to use as a client-side certificate, either a path to an SSL certificate file...
109+
110+
```pycon
111+
>>> cert = "path/to/client.pem"
112+
>>> ssl_context = httpx.SSLContext(cert=cert)
113+
>>> httpx.get("https://example.org", ssl_context=ssl_context)
114+
<Response [200 OK]>
57115
```
58116

59-
The `client.get(...)` method and other request methods *do not* support changing the SSL settings on a per-request basis. If you need different SSL settings in different cases you should use more that one client instance, with different settings on each. Each client will then be using an isolated connection pool with a specific fixed SSL configuration on all connections within that pool.
117+
Or two-tuple of (certificate file, key file)...
60118

61-
## Client Side Certificates
119+
```pycon
120+
>>> cert = ("path/to/client.pem", "path/to/client.key")
121+
>>> ssl_context = httpx.SSLContext(cert=cert)
122+
>>> httpx.get("https://example.org", ssl_context=ssl_context)
123+
<Response [200 OK]>
124+
```
62125

63-
You can also specify a local cert to use as a client-side certificate, either a path to an SSL certificate file, or two-tuple of (certificate file, key file), or a three-tuple of (certificate file, key file, password)
126+
Or a three-tuple of (certificate file, key file, password)...
127+
128+
```pycon
129+
>>> cert = ("path/to/client.pem", "path/to/client.key", "password")
130+
>>> ssl_context = httpx.SSLContext(cert=cert)
131+
>>> httpx.get("https://example.org", ssl_context=ssl_context)
132+
<Response [200 OK]>
133+
```
134+
135+
These configurations are equivalent to using [`.load_cert_chain()`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_cert_chain).
136+
137+
### Using alternate SSL contexts
138+
139+
You can also use an alternate `ssl.SSLContext` instances.
140+
141+
For example, [using the `truststore` package](https://truststore.readthedocs.io/)...
64142

65143
```python
66-
cert = "path/to/client.pem"
67-
client = httpx.Client(cert=cert)
68-
response = client.get("https://example.org")
144+
import ssl
145+
import truststore
146+
import httpx
147+
148+
ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
149+
client = httpx.Client(ssl_context=ssl_context)
69150
```
70151

71-
Alternatively...
152+
Or working [directly with Python's standard library](https://docs.python.org/3/library/ssl.html)...
72153

73154
```python
74-
cert = ("path/to/client.pem", "path/to/client.key")
75-
client = httpx.Client(cert=cert)
76-
response = client.get("https://example.org")
155+
import ssl
156+
import httpx
157+
158+
ssl_context = ssl.create_default_context()
159+
client = httpx.Client(ssl_context=ssl_context)
77160
```
78161

79-
Or...
162+
### Working with `SSL_CERT_FILE` and `SSL_CERT_DIR`
163+
164+
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.
165+
166+
For example...
167+
168+
```python
169+
# Use `SSL_CERT_FILE` or `SSL_CERT_DIR` if configured, otherwise use certifi.
170+
verify = os.environ.get("SSL_CERT_FILE", os.environ.get("SSL_CERT_DIR", True))
171+
ssl_context = httpx.SSLContext(verify=verify)
172+
```
173+
174+
## `SSLKEYLOGFILE`
175+
176+
Valid values: a filename
177+
178+
If this environment variable is set, TLS keys will be appended to the specified file, creating it if it doesn't exist, whenever key material is generated or received. The keylog file is designed for debugging purposes only.
179+
180+
Support for `SSLKEYLOGFILE` requires Python 3.8 and OpenSSL 1.1.1 or newer.
181+
182+
Example:
80183

81184
```python
82-
cert = ("path/to/client.pem", "path/to/client.key", "password")
83-
client = httpx.Client(cert=cert)
84-
response = client.get("https://example.org")
185+
# test_script.py
186+
import httpx
187+
188+
with httpx.Client() as client:
189+
r = client.get("https://google.com")
85190
```
86191

87-
## Making HTTPS requests to a local server
192+
```console
193+
SSLKEYLOGFILE=test.log python test_script.py
194+
cat test.log
195+
# TLS secrets log file, generated by OpenSSL / Python
196+
SERVER_HANDSHAKE_TRAFFIC_SECRET XXXX
197+
EXPORTER_SECRET XXXX
198+
SERVER_TRAFFIC_SECRET_0 XXXX
199+
CLIENT_HANDSHAKE_TRAFFIC_SECRET XXXX
200+
CLIENT_TRAFFIC_SECRET_0 XXXX
201+
SERVER_HANDSHAKE_TRAFFIC_SECRET XXXX
202+
EXPORTER_SECRET XXXX
203+
SERVER_TRAFFIC_SECRET_0 XXXX
204+
CLIENT_HANDSHAKE_TRAFFIC_SECRET XXXX
205+
CLIENT_TRAFFIC_SECRET_0 XXXX
206+
```
207+
208+
### Making HTTPS requests to a local server
88209

89210
When making requests to local servers, such as a development server running on `localhost`, you will typically be using unencrypted HTTP connections.
90211

91212
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:
92213

93214
1. Use [trustme](https://github.com/python-trio/trustme) to generate a pair of server key/cert files, and a client cert file.
94-
1. Pass the server key/cert files when starting your local server. (This depends on the particular web server you're using. For example, [Uvicorn](https://www.uvicorn.org) provides the `--ssl-keyfile` and `--ssl-certfile` options.)
95-
1. Tell HTTPX to use the certificates stored in `client.pem`:
215+
2. Pass the server key/cert files when starting your local server. (This depends on the particular web server you're using. For example, [Uvicorn](https://www.uvicorn.org) provides the `--ssl-keyfile` and `--ssl-certfile` options.)
216+
3. Tell HTTPX to use the certificates stored in `client.pem`:
96217

97-
```python
98-
client = httpx.Client(verify="/tmp/client.pem")
99-
response = client.get("https://localhost:8000")
218+
```pycon
219+
>>> import httpx
220+
>>> ssl_context = httpx.SSLContext(verify="/tmp/client.pem")
221+
>>> r = httpx.get("https://localhost:8000", ssl_context=ssl_context)
222+
>>> r
223+
Response <200 OK>
100224
```

docs/compatibility.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,10 @@ Also note that `requests.Session.request(...)` allows a `proxies=...` parameter,
171171

172172
## SSL configuration
173173

174-
When using a `Client` instance, the `trust_env`, `verify`, and `cert` arguments should always be passed on client instantiation, rather than passed to the request method.
174+
When using a `Client` instance, the ssl configurations should always be passed on client instantiation, rather than passed to the request method.
175175

176176
If you need more than one different SSL configuration, you should use different client instances for each SSL configuration.
177177

178-
Requests supports `REQUESTS_CA_BUNDLE` which points to either a file or a directory. HTTPX supports the `SSL_CERT_FILE` (for a file) and `SSL_CERT_DIR` (for a directory) OpenSSL variables instead.
179-
180178
## Request body on HTTP methods
181179

182180
The HTTP `GET`, `DELETE`, `HEAD`, and `OPTIONS` methods are specified as not supporting a request body. To stay in line with this, the `.get`, `.delete`, `.head` and `.options` functions do not support `content`, `files`, `data`, or `json` arguments.

docs/environment_variables.md

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -8,66 +8,6 @@ Environment variables are used by default. To ignore environment variables, `tru
88

99
Here is a list of environment variables that HTTPX recognizes and what function they serve:
1010

11-
## `SSLKEYLOGFILE`
12-
13-
Valid values: a filename
14-
15-
If this environment variable is set, TLS keys will be appended to the specified file, creating it if it doesn't exist, whenever key material is generated or received. The keylog file is designed for debugging purposes only.
16-
17-
Support for `SSLKEYLOGFILE` requires Python 3.8 and OpenSSL 1.1.1 or newer.
18-
19-
Example:
20-
21-
```python
22-
# test_script.py
23-
import httpx
24-
25-
with httpx.AsyncClient() as client:
26-
r = client.get("https://google.com")
27-
```
28-
29-
```console
30-
SSLKEYLOGFILE=test.log python test_script.py
31-
cat test.log
32-
# TLS secrets log file, generated by OpenSSL / Python
33-
SERVER_HANDSHAKE_TRAFFIC_SECRET XXXX
34-
EXPORTER_SECRET XXXX
35-
SERVER_TRAFFIC_SECRET_0 XXXX
36-
CLIENT_HANDSHAKE_TRAFFIC_SECRET XXXX
37-
CLIENT_TRAFFIC_SECRET_0 XXXX
38-
SERVER_HANDSHAKE_TRAFFIC_SECRET XXXX
39-
EXPORTER_SECRET XXXX
40-
SERVER_TRAFFIC_SECRET_0 XXXX
41-
CLIENT_HANDSHAKE_TRAFFIC_SECRET XXXX
42-
CLIENT_TRAFFIC_SECRET_0 XXXX
43-
```
44-
45-
## `SSL_CERT_FILE`
46-
47-
Valid values: a filename
48-
49-
If this environment variable is set then HTTPX will load
50-
CA certificate from the specified file instead of the default
51-
location.
52-
53-
Example:
54-
55-
```console
56-
SSL_CERT_FILE=/path/to/ca-certs/ca-bundle.crt python -c "import httpx; httpx.get('https://example.com')"
57-
```
58-
59-
## `SSL_CERT_DIR`
60-
61-
Valid values: a directory following an [OpenSSL specific layout](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_load_verify_locations.html).
62-
63-
If this environment variable is set and the directory follows an [OpenSSL specific layout](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_load_verify_locations.html) (ie. you ran `c_rehash`) then HTTPX will load CA certificates from this directory instead of the default location.
64-
65-
Example:
66-
67-
```console
68-
SSL_CERT_DIR=/path/to/ca-certs/ python -c "import httpx; httpx.get('https://example.com')"
69-
```
70-
7111
## Proxies
7212

7313
The environment variables documented below are used as a convention by various HTTP tooling, including:

0 commit comments

Comments
 (0)