Home Assistant Unofficial Reference 2024.12.1
ssl.py
Go to the documentation of this file.
1 """Helper to create SSL contexts."""
2 
3 import contextlib
4 from enum import StrEnum
5 from functools import cache
6 from os import environ
7 import ssl
8 
9 import certifi
10 
11 
12 class SSLCipherList(StrEnum):
13  """SSL cipher lists."""
14 
15  PYTHON_DEFAULT = "python_default"
16  INTERMEDIATE = "intermediate"
17  MODERN = "modern"
18  INSECURE = "insecure"
19 
20 
21 SSL_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
67 def _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 
85 @cache
87  ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
88 ) -> ssl.SSLContext:
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(
95  purpose=ssl.Purpose.SERVER_AUTH, cafile=cafile
96  )
97  if ssl_cipher_list != SSLCipherList.PYTHON_DEFAULT:
98  sslcontext.set_ciphers(SSL_CIPHER_LISTS[ssl_cipher_list])
99 
100  return sslcontext
101 
102 
103 # Create this only once and reuse it
104 _DEFAULT_SSL_CONTEXT = _client_context(SSLCipherList.PYTHON_DEFAULT)
105 _DEFAULT_NO_VERIFY_SSL_CONTEXT = _client_context_no_verify(SSLCipherList.PYTHON_DEFAULT)
106 _NO_VERIFY_SSL_CONTEXTS = {
107  SSLCipherList.INTERMEDIATE: _client_context_no_verify(SSLCipherList.INTERMEDIATE),
108  SSLCipherList.MODERN: _client_context_no_verify(SSLCipherList.MODERN),
109  SSLCipherList.INSECURE: _client_context_no_verify(SSLCipherList.INSECURE),
110 }
111 _SSL_CONTEXTS = {
112  SSLCipherList.INTERMEDIATE: _client_context(SSLCipherList.INTERMEDIATE),
113  SSLCipherList.MODERN: _client_context(SSLCipherList.MODERN),
114  SSLCipherList.INSECURE: _client_context(SSLCipherList.INSECURE),
115 }
116 
117 
118 def get_default_context() -> ssl.SSLContext:
119  """Return the default SSL context."""
120  return _DEFAULT_SSL_CONTEXT
121 
122 
123 def get_default_no_verify_context() -> ssl.SSLContext:
124  """Return the default SSL context that does not verify the server certificate."""
125  return _DEFAULT_NO_VERIFY_SSL_CONTEXT
126 
127 
129  ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
130 ) -> ssl.SSLContext:
131  """Return a SSL context with no verification with a specific ssl cipher."""
132  return _NO_VERIFY_SSL_CONTEXTS.get(ssl_cipher_list, _DEFAULT_NO_VERIFY_SSL_CONTEXT)
133 
134 
136  ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
137 ) -> ssl.SSLContext:
138  """Return an SSL context for making requests."""
139  return _SSL_CONTEXTS.get(ssl_cipher_list, _DEFAULT_SSL_CONTEXT)
140 
141 
143  ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
144 ) -> ssl.SSLContext:
145  """Return an SSL context that does not verify the server certificate."""
146  return _client_context_no_verify(ssl_cipher_list)
147 
148 
149 def server_context_modern() -> ssl.SSLContext:
150  """Return an SSL context following the Mozilla recommendations.
151 
152  TLS configuration follows the best-practice guidelines specified here:
153  https://wiki.mozilla.org/Security/Server_Side_TLS
154  Modern guidelines are followed.
155  """
156  context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
157  context.minimum_version = ssl.TLSVersion.TLSv1_2
158 
159  context.options |= ssl.OP_CIPHER_SERVER_PREFERENCE
160  if hasattr(ssl, "OP_NO_COMPRESSION"):
161  context.options |= ssl.OP_NO_COMPRESSION
162 
163  context.set_ciphers(SSL_CIPHER_LISTS[SSLCipherList.MODERN])
164 
165  return context
166 
167 
168 def server_context_intermediate() -> ssl.SSLContext:
169  """Return an SSL context following the Mozilla recommendations.
170 
171  TLS configuration follows the best-practice guidelines specified here:
172  https://wiki.mozilla.org/Security/Server_Side_TLS
173  Intermediate guidelines are followed.
174  """
175  context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
176 
177  context.options |= (
178  ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_CIPHER_SERVER_PREFERENCE
179  )
180  if hasattr(ssl, "OP_NO_COMPRESSION"):
181  context.options |= ssl.OP_NO_COMPRESSION
182 
183  context.set_ciphers(SSL_CIPHER_LISTS[SSLCipherList.INTERMEDIATE])
184 
185  return context
ssl.SSLContext client_context(SSLCipherList ssl_cipher_list=SSLCipherList.PYTHON_DEFAULT)
Definition: ssl.py:137
ssl.SSLContext server_context_modern()
Definition: ssl.py:149
ssl.SSLContext create_no_verify_ssl_context(SSLCipherList ssl_cipher_list=SSLCipherList.PYTHON_DEFAULT)
Definition: ssl.py:144
ssl.SSLContext get_default_context()
Definition: ssl.py:118
ssl.SSLContext client_context_no_verify(SSLCipherList ssl_cipher_list=SSLCipherList.PYTHON_DEFAULT)
Definition: ssl.py:130
ssl.SSLContext _client_context(SSLCipherList ssl_cipher_list=SSLCipherList.PYTHON_DEFAULT)
Definition: ssl.py:88
ssl.SSLContext get_default_no_verify_context()
Definition: ssl.py:123
ssl.SSLContext _client_context_no_verify(SSLCipherList ssl_cipher_list)
Definition: ssl.py:67
ssl.SSLContext server_context_intermediate()
Definition: ssl.py:168