/
/
/
1"""Helper to create SSL contexts."""
2
3import contextlib
4import ssl
5from enum import StrEnum
6from functools import cache
7from os import environ
8
9import certifi
10
11
12class SSLCipherList(StrEnum):
13 """SSL cipher lists."""
14
15 PYTHON_DEFAULT = "python_default"
16 INTERMEDIATE = "intermediate"
17 MODERN = "modern"
18 INSECURE = "insecure"
19
20
21SSL_CIPHER_LISTS = {
22 SSLCipherList.INTERMEDIATE: (
23 "ECDHE-ECDSA-CHACHA20-POLY1305:"
24 "ECDHE-RSA-CHACHA20-POLY1305:"
25 "ECDHE-ECDSA-AES128-GCM-SHA256:"
26 "ECDHE-RSA-AES128-GCM-SHA256:"
27 "ECDHE-ECDSA-AES256-GCM-SHA384:"
28 "ECDHE-RSA-AES256-GCM-SHA384:"
29 "DHE-RSA-AES128-GCM-SHA256:"
30 "DHE-RSA-AES256-GCM-SHA384:"
31 "ECDHE-ECDSA-AES128-SHA256:"
32 "ECDHE-RSA-AES128-SHA256:"
33 "ECDHE-ECDSA-AES128-SHA:"
34 "ECDHE-RSA-AES256-SHA384:"
35 "ECDHE-RSA-AES128-SHA:"
36 "ECDHE-ECDSA-AES256-SHA384:"
37 "ECDHE-ECDSA-AES256-SHA:"
38 "ECDHE-RSA-AES256-SHA:"
39 "DHE-RSA-AES128-SHA256:"
40 "DHE-RSA-AES128-SHA:"
41 "DHE-RSA-AES256-SHA256:"
42 "DHE-RSA-AES256-SHA:"
43 "ECDHE-ECDSA-DES-CBC3-SHA:"
44 "ECDHE-RSA-DES-CBC3-SHA:"
45 "EDH-RSA-DES-CBC3-SHA:"
46 "AES128-GCM-SHA256:"
47 "AES256-GCM-SHA384:"
48 "AES128-SHA256:"
49 "AES256-SHA256:"
50 "AES128-SHA:"
51 "AES256-SHA:"
52 "DES-CBC3-SHA:"
53 "!DSS"
54 ),
55 SSLCipherList.MODERN: (
56 "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:"
57 "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:"
58 "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:"
59 "ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:"
60 "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
61 ),
62 SSLCipherList.INSECURE: "DEFAULT:@SECLEVEL=0",
63}
64
65
66@cache
67def _client_context_no_verify(ssl_cipher_list: SSLCipherList) -> ssl.SSLContext:
68 # This is a copy of aiohttp's create_default_context() function, with the
69 # ssl verify turned off.
70 # https://github.com/aio-libs/aiohttp/blob/33953f110e97eecc707e1402daa8d543f38a189b/aiohttp/connector.py#L911
71
72 sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
73 sslcontext.check_hostname = False
74 sslcontext.verify_mode = ssl.CERT_NONE
75 with contextlib.suppress(AttributeError):
76 # This only works for OpenSSL >= 1.0.0
77 sslcontext.options |= ssl.OP_NO_COMPRESSION
78 sslcontext.set_default_verify_paths()
79 if ssl_cipher_list != SSLCipherList.PYTHON_DEFAULT:
80 sslcontext.set_ciphers(SSL_CIPHER_LISTS[ssl_cipher_list])
81
82 return sslcontext
83
84
85def _create_client_context(
86 ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
87) -> ssl.SSLContext:
88 """Return an independent SSL context for making requests."""
89 # Reuse environment variable definition from requests, since it's already a
90 # requirement. If the environment variable has no value, fall back to using
91 # certs from certifi package.
92 cafile = environ.get("REQUESTS_CA_BUNDLE", certifi.where())
93
94 sslcontext = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=cafile)
95 if ssl_cipher_list != SSLCipherList.PYTHON_DEFAULT:
96 sslcontext.set_ciphers(SSL_CIPHER_LISTS[ssl_cipher_list])
97
98 return sslcontext
99
100
101@cache
102def _client_context(
103 ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
104) -> ssl.SSLContext:
105 # Cached version of _create_client_context
106 return _create_client_context(ssl_cipher_list)
107
108
109# Create this only once and reuse it
110_DEFAULT_SSL_CONTEXT = _client_context(SSLCipherList.PYTHON_DEFAULT)
111_DEFAULT_NO_VERIFY_SSL_CONTEXT = _client_context_no_verify(SSLCipherList.PYTHON_DEFAULT)
112_NO_VERIFY_SSL_CONTEXTS = {
113 SSLCipherList.INTERMEDIATE: _client_context_no_verify(SSLCipherList.INTERMEDIATE),
114 SSLCipherList.MODERN: _client_context_no_verify(SSLCipherList.MODERN),
115 SSLCipherList.INSECURE: _client_context_no_verify(SSLCipherList.INSECURE),
116}
117_SSL_CONTEXTS = {
118 SSLCipherList.INTERMEDIATE: _client_context(SSLCipherList.INTERMEDIATE),
119 SSLCipherList.MODERN: _client_context(SSLCipherList.MODERN),
120 SSLCipherList.INSECURE: _client_context(SSLCipherList.INSECURE),
121}
122
123
124def get_default_context() -> ssl.SSLContext:
125 """Return the default SSL context."""
126 return _DEFAULT_SSL_CONTEXT
127
128
129def get_default_no_verify_context() -> ssl.SSLContext:
130 """Return the default SSL context that does not verify the server certificate."""
131 return _DEFAULT_NO_VERIFY_SSL_CONTEXT
132
133
134def client_context_no_verify(
135 ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
136) -> ssl.SSLContext:
137 """Return a SSL context with no verification with a specific ssl cipher."""
138 return _NO_VERIFY_SSL_CONTEXTS.get(ssl_cipher_list, _DEFAULT_NO_VERIFY_SSL_CONTEXT)
139
140
141def client_context(
142 ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
143) -> ssl.SSLContext:
144 """Return an SSL context for making requests."""
145 return _SSL_CONTEXTS.get(ssl_cipher_list, _DEFAULT_SSL_CONTEXT)
146
147
148def create_client_context(
149 ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
150) -> ssl.SSLContext:
151 """Return an independent SSL context for making requests."""
152 # This explicitly uses the non-cached version to create a client context
153 return _create_client_context(ssl_cipher_list)
154
155
156def create_no_verify_ssl_context(
157 ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
158) -> ssl.SSLContext:
159 """Return an SSL context that does not verify the server certificate."""
160 return _client_context_no_verify(ssl_cipher_list)
161
162
163def server_context_modern() -> ssl.SSLContext:
164 """Return an SSL context following the Mozilla recommendations.
165
166 TLS configuration follows the best-practice guidelines specified here:
167 https://wiki.mozilla.org/Security/Server_Side_TLS
168 Modern guidelines are followed.
169 """
170 context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
171 context.minimum_version = ssl.TLSVersion.TLSv1_2
172
173 context.options |= ssl.OP_CIPHER_SERVER_PREFERENCE
174 if hasattr(ssl, "OP_NO_COMPRESSION"):
175 context.options |= ssl.OP_NO_COMPRESSION
176
177 context.set_ciphers(SSL_CIPHER_LISTS[SSLCipherList.MODERN])
178
179 return context
180
181
182def server_context_intermediate() -> ssl.SSLContext:
183 """Return an SSL context following the Mozilla recommendations.
184
185 TLS configuration follows the best-practice guidelines specified here:
186 https://wiki.mozilla.org/Security/Server_Side_TLS
187 Intermediate guidelines are followed.
188 """
189 context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
190
191 context.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_CIPHER_SERVER_PREFERENCE
192 if hasattr(ssl, "OP_NO_COMPRESSION"):
193 context.options |= ssl.OP_NO_COMPRESSION
194
195 context.set_ciphers(SSL_CIPHER_LISTS[SSLCipherList.INTERMEDIATE])
196
197 return context
198