mirror of https://github.com/home-assistant/core
186 lines
6.1 KiB
Python
186 lines
6.1 KiB
Python
"""Helper to create SSL contexts."""
|
|
|
|
import contextlib
|
|
from enum import StrEnum
|
|
from functools import cache
|
|
from os import environ
|
|
import ssl
|
|
|
|
import certifi
|
|
|
|
|
|
class SSLCipherList(StrEnum):
|
|
"""SSL cipher lists."""
|
|
|
|
PYTHON_DEFAULT = "python_default"
|
|
INTERMEDIATE = "intermediate"
|
|
MODERN = "modern"
|
|
INSECURE = "insecure"
|
|
|
|
|
|
SSL_CIPHER_LISTS = {
|
|
SSLCipherList.INTERMEDIATE: (
|
|
"ECDHE-ECDSA-CHACHA20-POLY1305:"
|
|
"ECDHE-RSA-CHACHA20-POLY1305:"
|
|
"ECDHE-ECDSA-AES128-GCM-SHA256:"
|
|
"ECDHE-RSA-AES128-GCM-SHA256:"
|
|
"ECDHE-ECDSA-AES256-GCM-SHA384:"
|
|
"ECDHE-RSA-AES256-GCM-SHA384:"
|
|
"DHE-RSA-AES128-GCM-SHA256:"
|
|
"DHE-RSA-AES256-GCM-SHA384:"
|
|
"ECDHE-ECDSA-AES128-SHA256:"
|
|
"ECDHE-RSA-AES128-SHA256:"
|
|
"ECDHE-ECDSA-AES128-SHA:"
|
|
"ECDHE-RSA-AES256-SHA384:"
|
|
"ECDHE-RSA-AES128-SHA:"
|
|
"ECDHE-ECDSA-AES256-SHA384:"
|
|
"ECDHE-ECDSA-AES256-SHA:"
|
|
"ECDHE-RSA-AES256-SHA:"
|
|
"DHE-RSA-AES128-SHA256:"
|
|
"DHE-RSA-AES128-SHA:"
|
|
"DHE-RSA-AES256-SHA256:"
|
|
"DHE-RSA-AES256-SHA:"
|
|
"ECDHE-ECDSA-DES-CBC3-SHA:"
|
|
"ECDHE-RSA-DES-CBC3-SHA:"
|
|
"EDH-RSA-DES-CBC3-SHA:"
|
|
"AES128-GCM-SHA256:"
|
|
"AES256-GCM-SHA384:"
|
|
"AES128-SHA256:"
|
|
"AES256-SHA256:"
|
|
"AES128-SHA:"
|
|
"AES256-SHA:"
|
|
"DES-CBC3-SHA:"
|
|
"!DSS"
|
|
),
|
|
SSLCipherList.MODERN: (
|
|
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:"
|
|
"ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:"
|
|
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:"
|
|
"ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:"
|
|
"ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
|
|
),
|
|
SSLCipherList.INSECURE: "DEFAULT:@SECLEVEL=0",
|
|
}
|
|
|
|
|
|
@cache
|
|
def _client_context_no_verify(ssl_cipher_list: SSLCipherList) -> ssl.SSLContext:
|
|
# This is a copy of aiohttp's create_default_context() function, with the
|
|
# ssl verify turned off.
|
|
# https://github.com/aio-libs/aiohttp/blob/33953f110e97eecc707e1402daa8d543f38a189b/aiohttp/connector.py#L911
|
|
|
|
sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
sslcontext.check_hostname = False
|
|
sslcontext.verify_mode = ssl.CERT_NONE
|
|
with contextlib.suppress(AttributeError):
|
|
# This only works for OpenSSL >= 1.0.0
|
|
sslcontext.options |= ssl.OP_NO_COMPRESSION
|
|
sslcontext.set_default_verify_paths()
|
|
if ssl_cipher_list != SSLCipherList.PYTHON_DEFAULT:
|
|
sslcontext.set_ciphers(SSL_CIPHER_LISTS[ssl_cipher_list])
|
|
|
|
return sslcontext
|
|
|
|
|
|
@cache
|
|
def _client_context(
|
|
ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
|
|
) -> ssl.SSLContext:
|
|
# Reuse environment variable definition from requests, since it's already a
|
|
# requirement. If the environment variable has no value, fall back to using
|
|
# certs from certifi package.
|
|
cafile = environ.get("REQUESTS_CA_BUNDLE", certifi.where())
|
|
|
|
sslcontext = ssl.create_default_context(
|
|
purpose=ssl.Purpose.SERVER_AUTH, cafile=cafile
|
|
)
|
|
if ssl_cipher_list != SSLCipherList.PYTHON_DEFAULT:
|
|
sslcontext.set_ciphers(SSL_CIPHER_LISTS[ssl_cipher_list])
|
|
|
|
return sslcontext
|
|
|
|
|
|
# Create this only once and reuse it
|
|
_DEFAULT_SSL_CONTEXT = _client_context(SSLCipherList.PYTHON_DEFAULT)
|
|
_DEFAULT_NO_VERIFY_SSL_CONTEXT = _client_context_no_verify(SSLCipherList.PYTHON_DEFAULT)
|
|
_NO_VERIFY_SSL_CONTEXTS = {
|
|
SSLCipherList.INTERMEDIATE: _client_context_no_verify(SSLCipherList.INTERMEDIATE),
|
|
SSLCipherList.MODERN: _client_context_no_verify(SSLCipherList.MODERN),
|
|
SSLCipherList.INSECURE: _client_context_no_verify(SSLCipherList.INSECURE),
|
|
}
|
|
_SSL_CONTEXTS = {
|
|
SSLCipherList.INTERMEDIATE: _client_context(SSLCipherList.INTERMEDIATE),
|
|
SSLCipherList.MODERN: _client_context(SSLCipherList.MODERN),
|
|
SSLCipherList.INSECURE: _client_context(SSLCipherList.INSECURE),
|
|
}
|
|
|
|
|
|
def get_default_context() -> ssl.SSLContext:
|
|
"""Return the default SSL context."""
|
|
return _DEFAULT_SSL_CONTEXT
|
|
|
|
|
|
def get_default_no_verify_context() -> ssl.SSLContext:
|
|
"""Return the default SSL context that does not verify the server certificate."""
|
|
return _DEFAULT_NO_VERIFY_SSL_CONTEXT
|
|
|
|
|
|
def client_context_no_verify(
|
|
ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
|
|
) -> ssl.SSLContext:
|
|
"""Return a SSL context with no verification with a specific ssl cipher."""
|
|
return _NO_VERIFY_SSL_CONTEXTS.get(ssl_cipher_list, _DEFAULT_NO_VERIFY_SSL_CONTEXT)
|
|
|
|
|
|
def client_context(
|
|
ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
|
|
) -> ssl.SSLContext:
|
|
"""Return an SSL context for making requests."""
|
|
return _SSL_CONTEXTS.get(ssl_cipher_list, _DEFAULT_SSL_CONTEXT)
|
|
|
|
|
|
def create_no_verify_ssl_context(
|
|
ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
|
|
) -> ssl.SSLContext:
|
|
"""Return an SSL context that does not verify the server certificate."""
|
|
return _client_context_no_verify(ssl_cipher_list)
|
|
|
|
|
|
def server_context_modern() -> ssl.SSLContext:
|
|
"""Return an SSL context following the Mozilla recommendations.
|
|
|
|
TLS configuration follows the best-practice guidelines specified here:
|
|
https://wiki.mozilla.org/Security/Server_Side_TLS
|
|
Modern guidelines are followed.
|
|
"""
|
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
|
|
context.options |= ssl.OP_CIPHER_SERVER_PREFERENCE
|
|
if hasattr(ssl, "OP_NO_COMPRESSION"):
|
|
context.options |= ssl.OP_NO_COMPRESSION
|
|
|
|
context.set_ciphers(SSL_CIPHER_LISTS[SSLCipherList.MODERN])
|
|
|
|
return context
|
|
|
|
|
|
def server_context_intermediate() -> ssl.SSLContext:
|
|
"""Return an SSL context following the Mozilla recommendations.
|
|
|
|
TLS configuration follows the best-practice guidelines specified here:
|
|
https://wiki.mozilla.org/Security/Server_Side_TLS
|
|
Intermediate guidelines are followed.
|
|
"""
|
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
|
|
context.options |= (
|
|
ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_CIPHER_SERVER_PREFERENCE
|
|
)
|
|
if hasattr(ssl, "OP_NO_COMPRESSION"):
|
|
context.options |= ssl.OP_NO_COMPRESSION
|
|
|
|
context.set_ciphers(SSL_CIPHER_LISTS[SSLCipherList.INTERMEDIATE])
|
|
|
|
return context
|