mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Switch to intermediate Mozilla cert profile (#15957)
* Allow choosing intermediate SSL profile * Fix tests
This commit is contained in:
parent
69b694ff26
commit
6540d2e073
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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) == [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user