Switch to intermediate Mozilla cert profile (#15957)

* Allow choosing intermediate SSL profile

* Fix tests
This commit is contained in:
Paulus Schoutsen 2018-08-14 08:20:17 +02:00 committed by GitHub
parent 69b694ff26
commit 6540d2e073
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 130 additions and 6 deletions

View File

@ -49,6 +49,10 @@ CONF_TRUSTED_PROXIES = 'trusted_proxies'
CONF_TRUSTED_NETWORKS = 'trusted_networks' CONF_TRUSTED_NETWORKS = 'trusted_networks'
CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold'
CONF_IP_BAN_ENABLED = 'ip_ban_enabled' CONF_IP_BAN_ENABLED = 'ip_ban_enabled'
CONF_SSL_PROFILE = 'ssl_profile'
SSL_MODERN = 'modern'
SSL_INTERMEDIATE = 'intermediate'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -74,7 +78,9 @@ HTTP_SCHEMA = vol.Schema({
vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD,
default=NO_LOGIN_ATTEMPT_THRESHOLD): default=NO_LOGIN_ATTEMPT_THRESHOLD):
vol.Any(cv.positive_int, 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({ CONFIG_SCHEMA = vol.Schema({
@ -123,6 +129,7 @@ async def async_setup(hass, config):
trusted_networks = conf[CONF_TRUSTED_NETWORKS] trusted_networks = conf[CONF_TRUSTED_NETWORKS]
is_ban_enabled = conf[CONF_IP_BAN_ENABLED] is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
ssl_profile = conf[CONF_SSL_PROFILE]
if api_password is not None: if api_password is not None:
logging.getLogger('aiohttp.access').addFilter( logging.getLogger('aiohttp.access').addFilter(
@ -141,7 +148,8 @@ async def async_setup(hass, config):
trusted_proxies=trusted_proxies, trusted_proxies=trusted_proxies,
trusted_networks=trusted_networks, trusted_networks=trusted_networks,
login_threshold=login_threshold, 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): async def stop_server(event):
@ -181,7 +189,7 @@ class HomeAssistantHTTP:
ssl_certificate, ssl_peer_certificate, ssl_certificate, ssl_peer_certificate,
ssl_key, server_host, server_port, cors_origins, ssl_key, server_host, server_port, cors_origins,
use_x_forwarded_for, trusted_proxies, trusted_networks, 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.""" """Initialize the HTTP Home Assistant server."""
app = self.app = web.Application( app = self.app = web.Application(
middlewares=[staticresource_middleware]) middlewares=[staticresource_middleware])
@ -222,6 +230,7 @@ class HomeAssistantHTTP:
self.server_port = server_port self.server_port = server_port
self.trusted_networks = trusted_networks self.trusted_networks = trusted_networks
self.is_ban_enabled = is_ban_enabled self.is_ban_enabled = is_ban_enabled
self.ssl_profile = ssl_profile
self._handler = None self._handler = None
self.server = None self.server = None
@ -308,7 +317,10 @@ class HomeAssistantHTTP:
if self.ssl_certificate: if self.ssl_certificate:
try: 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) context.load_cert_chain(self.ssl_certificate, self.ssl_key)
except OSError as error: except OSError as error:
_LOGGER.error("Could not read SSL certificate from %s: %s", _LOGGER.error("Could not read SSL certificate from %s: %s",

View File

@ -13,7 +13,7 @@ def client_context() -> ssl.SSLContext:
return context return context
def server_context() -> ssl.SSLContext: def server_context_modern() -> ssl.SSLContext:
"""Return an SSL context following the Mozilla recommendations. """Return an SSL context following the Mozilla recommendations.
TLS configuration follows the best-practice guidelines specified here: 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-AES256-SHA384:ECDHE-RSA-AES256-SHA384:"
"ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" "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 return context

View File

@ -1,10 +1,13 @@
"""The tests for the Home Assistant HTTP component.""" """The tests for the Home Assistant HTTP component."""
import logging import logging
import unittest import unittest
from unittest.mock import patch
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.components.http as http import homeassistant.components.http as http
from homeassistant.util.ssl import (
server_context_modern, server_context_intermediate)
class TestView(http.HomeAssistantView): 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'] http.CONF_TRUSTED_PROXIES: ['127.0.0.1']
} }
}) is not True }) 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

View File

@ -159,7 +159,9 @@ class TestCheckConfig(unittest.TestCase):
'login_attempts_threshold': -1, 'login_attempts_threshold': -1,
'server_host': '0.0.0.0', 'server_host': '0.0.0.0',
'server_port': 8123, 'server_port': 8123,
'trusted_networks': []} 'trusted_networks': [],
'ssl_profile': 'modern',
}
assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}} assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}}
assert res['secrets'] == {'http_pw': 'abc123'} assert res['secrets'] == {'http_pw': 'abc123'}
assert normalize_yaml_files(res) == [ assert normalize_yaml_files(res) == [