Skip to content

Commit

Permalink
Switch to intermediate Mozilla cert profile (#15957)
Browse files Browse the repository at this point in the history
* Allow choosing intermediate SSL profile

* Fix tests
  • Loading branch information
balloob authored Aug 14, 2018
1 parent 69b694f commit 6540d2e
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 6 deletions.
20 changes: 16 additions & 4 deletions homeassistant/components/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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({
Expand Down Expand Up @@ -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(
Expand All @@ -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):
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand Down
56 changes: 55 additions & 1 deletion homeassistant/util/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
56 changes: 56 additions & 0 deletions tests/components/http/test_init.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion tests/scripts/test_check_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) == [
Expand Down

0 comments on commit 6540d2e

Please sign in to comment.