diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 2fbfa09920937..9ded94fa6fd2c 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -49,6 +49,10 @@ CONF_TRUSTED_NETWORKS = 'trusted_networks' CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' CONF_IP_BAN_ENABLED = 'ip_ban_enabled' +CONF_SSL_PROFILE = 'ssl_profile' + +SSL_MODERN = 'modern' +SSL_INTERMEDIATE = 'intermediate' _LOGGER = logging.getLogger(__name__) @@ -74,7 +78,9 @@ vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), - vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean + vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean, + vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): + vol.In([SSL_INTERMEDIATE, SSL_MODERN]), }) CONFIG_SCHEMA = vol.Schema({ @@ -123,6 +129,7 @@ async def async_setup(hass, config): trusted_networks = conf[CONF_TRUSTED_NETWORKS] is_ban_enabled = conf[CONF_IP_BAN_ENABLED] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] + ssl_profile = conf[CONF_SSL_PROFILE] if api_password is not None: logging.getLogger('aiohttp.access').addFilter( @@ -141,7 +148,8 @@ async def async_setup(hass, config): trusted_proxies=trusted_proxies, trusted_networks=trusted_networks, login_threshold=login_threshold, - is_ban_enabled=is_ban_enabled + is_ban_enabled=is_ban_enabled, + ssl_profile=ssl_profile, ) async def stop_server(event): @@ -181,7 +189,7 @@ def __init__(self, hass, api_password, ssl_certificate, ssl_peer_certificate, ssl_key, server_host, server_port, cors_origins, use_x_forwarded_for, trusted_proxies, trusted_networks, - login_threshold, is_ban_enabled): + login_threshold, is_ban_enabled, ssl_profile): """Initialize the HTTP Home Assistant server.""" app = self.app = web.Application( middlewares=[staticresource_middleware]) @@ -222,6 +230,7 @@ def __init__(self, hass, api_password, self.server_port = server_port self.trusted_networks = trusted_networks self.is_ban_enabled = is_ban_enabled + self.ssl_profile = ssl_profile self._handler = None self.server = None @@ -308,7 +317,10 @@ async def start(self): if self.ssl_certificate: try: - context = ssl_util.server_context() + if self.ssl_profile == SSL_INTERMEDIATE: + context = ssl_util.server_context_intermediate() + else: + context = ssl_util.server_context_modern() context.load_cert_chain(self.ssl_certificate, self.ssl_key) except OSError as error: _LOGGER.error("Could not read SSL certificate from %s: %s", diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 392c5986c8914..b78395cdb0d34 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -13,7 +13,7 @@ def client_context() -> ssl.SSLContext: return context -def server_context() -> ssl.SSLContext: +def server_context_modern() -> ssl.SSLContext: """Return an SSL context following the Mozilla recommendations. TLS configuration follows the best-practice guidelines specified here: @@ -37,4 +37,58 @@ def server_context() -> ssl.SSLContext: "ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" ) + + 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) # pylint: disable=no-member + + 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( + "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" + ) + return context diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index c52f60a5f1b0f..9f6441c52386f 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,10 +1,13 @@ """The tests for the Home Assistant HTTP component.""" import logging import unittest +from unittest.mock import patch from homeassistant.setup import async_setup_component import homeassistant.components.http as http +from homeassistant.util.ssl import ( + server_context_modern, server_context_intermediate) class TestView(http.HomeAssistantView): @@ -169,3 +172,56 @@ async def test_proxy_config_only_trust_proxies(hass): http.CONF_TRUSTED_PROXIES: ['127.0.0.1'] } }) is not True + + +async def test_ssl_profile_defaults_modern(hass): + """Test default ssl profile.""" + assert await async_setup_component(hass, 'http', {}) is True + + hass.http.ssl_certificate = 'bla' + + with patch('ssl.SSLContext.load_cert_chain'), \ + patch('homeassistant.util.ssl.server_context_modern', + side_effect=server_context_modern) as mock_context: + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_context.mock_calls) == 1 + + +async def test_ssl_profile_change_intermediate(hass): + """Test setting ssl profile to intermediate.""" + assert await async_setup_component(hass, 'http', { + 'http': { + 'ssl_profile': 'intermediate' + } + }) is True + + hass.http.ssl_certificate = 'bla' + + with patch('ssl.SSLContext.load_cert_chain'), \ + patch('homeassistant.util.ssl.server_context_intermediate', + side_effect=server_context_intermediate) as mock_context: + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_context.mock_calls) == 1 + + +async def test_ssl_profile_change_modern(hass): + """Test setting ssl profile to modern.""" + assert await async_setup_component(hass, 'http', { + 'http': { + 'ssl_profile': 'modern' + } + }) is True + + hass.http.ssl_certificate = 'bla' + + with patch('ssl.SSLContext.load_cert_chain'), \ + patch('homeassistant.util.ssl.server_context_modern', + side_effect=server_context_modern) as mock_context: + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_context.mock_calls) == 1 diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 59d8e27a672ee..532197b407227 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -159,7 +159,9 @@ def test_secrets(self, isfile_patch): 'login_attempts_threshold': -1, 'server_host': '0.0.0.0', 'server_port': 8123, - 'trusted_networks': []} + 'trusted_networks': [], + 'ssl_profile': 'modern', + } assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}} assert res['secrets'] == {'http_pw': 'abc123'} assert normalize_yaml_files(res) == [