mirror of
https://github.com/certbot/certbot.git
synced 2026-04-26 16:47:40 -04:00
This PR adds the functionality to enhance Apache configuration to include HTTP Strict Transport Security header with a low initial max-age value. The max-age value will get increased on every (scheduled) run of certbot renew regardless of the certificate actually getting renewed, if the last increase took place longer than ten hours ago. The increase steps are visible in constants.AUTOHSTS_STEPS. Upon the first actual renewal after reaching the maximum increase step, the max-age value will be made "permanent" and will get value of one year. To achieve accurate VirtualHost discovery on subsequent runs, a comment with unique id string will be added to each enhanced VirtualHost. * AutoHSTS code rebased on master * Fixes to match the changes in master * Make linter happy with metaclass registration * Address small review comments * Use new enhancement interfaces * New style enhancement changes * Do not allow --hsts and --auto-hsts simultaneuously * MyPy annotation fixes and added test * Change oldest requrements to point to local certbot core version * Enable new style enhancements for run and install verbs * Test refactor * New test class for main.install tests * Move a test to a correct test class
181 lines
8.5 KiB
Python
181 lines
8.5 KiB
Python
# pylint: disable=too-many-public-methods,too-many-lines
|
|
"""Test for certbot_apache.configurator AutoHSTS functionality"""
|
|
import re
|
|
import unittest
|
|
import mock
|
|
# six is used in mock.patch()
|
|
import six # pylint: disable=unused-import
|
|
|
|
from certbot import errors
|
|
from certbot_apache import constants
|
|
from certbot_apache.tests import util
|
|
|
|
|
|
class AutoHSTSTest(util.ApacheTest):
|
|
"""Tests for AutoHSTS feature"""
|
|
# pylint: disable=protected-access
|
|
|
|
def setUp(self): # pylint: disable=arguments-differ
|
|
super(AutoHSTSTest, self).setUp()
|
|
|
|
self.config = util.get_apache_configurator(
|
|
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
|
|
self.config.parser.modules.add("headers_module")
|
|
self.config.parser.modules.add("mod_headers.c")
|
|
self.config.parser.modules.add("ssl_module")
|
|
self.config.parser.modules.add("mod_ssl.c")
|
|
|
|
self.vh_truth = util.get_vh_truth(
|
|
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
|
|
|
|
def get_autohsts_value(self, vh_path):
|
|
""" Get value from Strict-Transport-Security header """
|
|
header_path = self.config.parser.find_dir("Header", None, vh_path)
|
|
if header_path:
|
|
pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
|
|
for head in header_path:
|
|
if re.search(pat, self.config.parser.aug.get(head).lower()):
|
|
return self.config.parser.aug.get(head.replace("arg[3]",
|
|
"arg[4]"))
|
|
|
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
|
|
def test_autohsts_enable_headers_mod(self, mock_enable, _restart):
|
|
self.config.parser.modules.discard("headers_module")
|
|
self.config.parser.modules.discard("mod_header.c")
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
self.assertTrue(mock_enable.called)
|
|
|
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
|
def test_autohsts_deploy_already_exists(self, _restart):
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
self.assertRaises(errors.PluginEnhancementAlreadyPresent,
|
|
self.config.enable_autohsts,
|
|
mock.MagicMock(), ["ocspvhost.com"])
|
|
|
|
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
|
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
|
def test_autohsts_increase(self, _mock_restart):
|
|
maxage = "\"max-age={0}\""
|
|
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
|
|
inc_val = maxage.format(constants.AUTOHSTS_STEPS[1])
|
|
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
# Verify initial value
|
|
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
|
initial_val)
|
|
# Increase
|
|
self.config.update_autohsts(mock.MagicMock())
|
|
# Verify increased value
|
|
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
|
inc_val)
|
|
|
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator._autohsts_increase")
|
|
def test_autohsts_increase_noop(self, mock_increase, _restart):
|
|
maxage = "\"max-age={0}\""
|
|
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
# Verify initial value
|
|
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
|
initial_val)
|
|
|
|
self.config.update_autohsts(mock.MagicMock())
|
|
# Freq not patched, so value shouldn't increase
|
|
self.assertFalse(mock_increase.called)
|
|
|
|
|
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
|
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
|
|
def test_autohsts_increase_no_header(self, _restart):
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
# Remove the header
|
|
dir_locs = self.config.parser.find_dir("Header", None,
|
|
self.vh_truth[7].path)
|
|
dir_loc = "/".join(dir_locs[0].split("/")[:-1])
|
|
self.config.parser.aug.remove(dir_loc)
|
|
self.assertRaises(errors.PluginError,
|
|
self.config.update_autohsts,
|
|
mock.MagicMock())
|
|
|
|
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
|
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
|
def test_autohsts_increase_and_make_permanent(self, _mock_restart):
|
|
maxage = "\"max-age={0}\""
|
|
max_val = maxage.format(constants.AUTOHSTS_PERMANENT)
|
|
mock_lineage = mock.MagicMock()
|
|
mock_lineage.key_path = "/etc/apache2/ssl/key-certbot_15.pem"
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
for i in range(len(constants.AUTOHSTS_STEPS)-1):
|
|
# Ensure that value is not made permanent prematurely
|
|
self.config.deploy_autohsts(mock_lineage)
|
|
self.assertNotEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
|
max_val)
|
|
self.config.update_autohsts(mock.MagicMock())
|
|
# Value should match pre-permanent increment step
|
|
cur_val = maxage.format(constants.AUTOHSTS_STEPS[i+1])
|
|
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
|
cur_val)
|
|
# Make permanent
|
|
self.config.deploy_autohsts(mock_lineage)
|
|
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
|
|
max_val)
|
|
|
|
def test_autohsts_update_noop(self):
|
|
with mock.patch("time.time") as mock_time:
|
|
# Time mock is used to make sure that the execution does not
|
|
# continue when no autohsts entries exist in pluginstorage
|
|
self.config.update_autohsts(mock.MagicMock())
|
|
self.assertFalse(mock_time.called)
|
|
|
|
def test_autohsts_make_permanent_noop(self):
|
|
self.config.storage.put = mock.MagicMock()
|
|
self.config.deploy_autohsts(mock.MagicMock())
|
|
# Make sure that the execution does not continue when no entries in store
|
|
self.assertFalse(self.config.storage.put.called)
|
|
|
|
@mock.patch("certbot_apache.display_ops.select_vhost")
|
|
def test_autohsts_no_ssl_vhost(self, mock_select):
|
|
mock_select.return_value = self.vh_truth[0]
|
|
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
|
|
self.assertRaises(errors.PluginError,
|
|
self.config.enable_autohsts,
|
|
mock.MagicMock(), "invalid.example.com")
|
|
self.assertTrue(
|
|
"Certbot was not able to find SSL" in mock_log.call_args[0][0])
|
|
|
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.add_vhost_id")
|
|
def test_autohsts_dont_enhance_twice(self, mock_id, _restart):
|
|
mock_id.return_value = "1234567"
|
|
self.config.enable_autohsts(mock.MagicMock(),
|
|
["ocspvhost.com", "ocspvhost.com"])
|
|
self.assertEquals(mock_id.call_count, 1)
|
|
|
|
def test_autohsts_remove_orphaned(self):
|
|
# pylint: disable=protected-access
|
|
self.config._autohsts_fetch_state()
|
|
self.config._autohsts["orphan_id"] = {"laststep": 0, "timestamp": 0}
|
|
|
|
self.config._autohsts_save_state()
|
|
self.config.update_autohsts(mock.MagicMock())
|
|
self.assertFalse("orphan_id" in self.config._autohsts)
|
|
# Make sure it's removed from the pluginstorage file as well
|
|
self.config._autohsts = None
|
|
self.config._autohsts_fetch_state()
|
|
self.assertFalse(self.config._autohsts)
|
|
|
|
def test_autohsts_make_permanent_vhost_not_found(self):
|
|
# pylint: disable=protected-access
|
|
self.config._autohsts_fetch_state()
|
|
self.config._autohsts["orphan_id"] = {"laststep": 999, "timestamp": 0}
|
|
self.config._autohsts_save_state()
|
|
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
|
|
self.config.deploy_autohsts(mock.MagicMock())
|
|
self.assertTrue(mock_log.called)
|
|
self.assertTrue(
|
|
"VirtualHost with id orphan_id was not" in mock_log.call_args[0][0])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main() # pragma: no cover
|