Switch to intermediate Mozilla cert profile (#15957)
* Allow choosing intermediate SSL profile * Fix testspull/16027/head
parent
1b384c322a
commit
899c2057b7
|
@ -49,6 +49,10 @@ CONF_TRUSTED_PROXIES = 'trusted_proxies'
|
|||
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 @@ HTTP_SCHEMA = vol.Schema({
|
|||
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 @@ class HomeAssistantHTTP:
|
|||
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])
|
||||
|
@ -221,6 +229,7 @@ class HomeAssistantHTTP:
|
|||
self.server_host = server_host
|
||||
self.server_port = server_port
|
||||
self.is_ban_enabled = is_ban_enabled
|
||||
self.ssl_profile = ssl_profile
|
||||
self._handler = None
|
||||
self.server = None
|
||||
|
||||
|
@ -307,7 +316,10 @@ class HomeAssistantHTTP:
|
|||
|
||||
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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -159,7 +159,9 @@ class TestCheckConfig(unittest.TestCase):
|
|||
'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) == [
|
||||
|
|
Loading…
Reference in New Issue