From 0b57daf473f5f742eacbad7c999631987b4f9064 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 12:10:22 +0000 Subject: [PATCH 01/24] Renewer dynamic dirs based on --config-dir/--work-dir (fixes #469). --- letsencrypt/cli.py | 4 +- letsencrypt/client.py | 17 +++-- letsencrypt/configuration.py | 28 +++++++++ letsencrypt/constants.py | 11 ++-- letsencrypt/renewer.py | 20 ++++-- letsencrypt/storage.py | 37 +++++------ letsencrypt/tests/renewer_test.py | 101 +++++++++++++++--------------- 7 files changed, 137 insertions(+), 81 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 1a87f0c60..5978c4d21 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -283,7 +283,7 @@ def create_parser(plugins): help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.") - _paths_parser(parser.add_argument_group("paths")) + _paths_parser(parser) # _plugins_parsing should be the last thing to act upon the main # parser (--help should display plugin-specific options last) _plugins_parsing(parser, plugins) @@ -342,7 +342,7 @@ def _create_subparsers(parser): def _paths_parser(parser): - add = parser.add_argument + add = parser.add_argument_group("paths").add_argument add("--config-dir", default=flag_default("config_dir"), help=config_help("config_dir")) add("--work-dir", default=flag_default("work_dir"), diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 02159f5d2..54077d644 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -11,6 +11,8 @@ from acme.jose import jwk from letsencrypt import account from letsencrypt import auth_handler +from letsencrypt import configuration +from letsencrypt import constants from letsencrypt import continuity_auth from letsencrypt import crypto_util from letsencrypt import errors @@ -193,10 +195,17 @@ class Client(object): # ideally should be a ConfigObj, but in this case a dict will be # accepted in practice.) params = vars(self.config.namespace) - config = {"renewer_config_file": - params["renewer_config_file"]} if "renewer_config_file" in params else None - return storage.RenewableCert.new_lineage(domains[0], cert, privkey, - chain, params, config) + config = {} + cli_config = configuration.RenewerConfiguration(self.config.namespace) + + if (cli_config.config_dir != constants.CLI_DEFAULTS["config_dir"] or + cli_config.work_dir != constants.CLI_DEFAULTS["work_dir"]): + logging.warning( + "Non-standard path(s), might not work with crontab installed " + "by your operating system package manager") + + return storage.RenewableCert.new_lineage( + domains[0], cert, privkey, chain, params, config, cli_config) def save_certificate(self, certr, cert_path, chain_path): diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 00b45040a..6f05b2b49 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -90,3 +90,31 @@ class NamespaceConfig(object): def temp_checkpoint_dir(self): # pylint: disable=missing-docstring return os.path.join( self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR) + + +class RenewerConfiguration(object): + """Configuration wrapper for renewer.""" + + def __init__(self, namespace): + self.namespace = namespace + + def __getattr__(self, name): + return getattr(self.namespace, name) + + @property + def archive_dir(self): # pylint: disable=missing-docstring + return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR) + + @property + def live_dir(self): # pylint: disable=missing-docstring + return os.path.join(self.namespace.config_dir, constants.LIVE_DIR) + + @property + def renewal_configs_dir(self): # pylint: disable=missing-docstring + return os.path.join( + self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR) + + @property + def renewer_config_file(self): # pylint: disable=missing-docstring + return os.path.join( + self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 9d04fb4c2..56d91f0c9 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -22,10 +22,6 @@ CLI_DEFAULTS = dict( RENEWER_DEFAULTS = dict( - renewer_config_file="/etc/letsencrypt/renewer.conf", - renewal_configs_dir="/etc/letsencrypt/configs", - archive_dir="/etc/letsencrypt/archive", - live_dir="/etc/letsencrypt/live", renewer_enabled="yes", renew_before_expiry="30 days", deploy_before_expiry="20 days", @@ -50,6 +46,8 @@ List of expected options parameters: """ +ARCHIVE_DIR = "archive" +"""TODO relative to `IConfig.config_dir`.""" CONFIG_DIRS_MODE = 0o755 """Directory mode for ``.IConfig.config_dir`` et al.""" @@ -77,6 +75,9 @@ IN_PROGRESS_DIR = "IN_PROGRESS" KEY_DIR = "keys" """Directory (relative to `IConfig.config_dir`) where keys are saved.""" +LIVE_DIR = "live" +"""TODO relative to `IConfig.config_dir`.""" + TEMP_CHECKPOINT_DIR = "temp_checkpoint" """Temporary checkpoint directory (relative to `IConfig.work_dir`).""" @@ -84,6 +85,8 @@ REC_TOKEN_DIR = "recovery_tokens" """Directory where all recovery tokens are saved (relative to `IConfig.work_dir`).""" +RENEWAL_CONFIGS_DIR = "configs" +"""TODO relative to `IConfig.config_dir`.""" RENEWER_CONFIG_FILENAME = "renewer.conf" """Renewer config file name (relative to `IConfig.config_dir`).""" diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 6e61fd893..b27f5fa4c 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -7,15 +7,19 @@ within lineages of successor certificates, according to configuration. .. todo:: Call new installer API to restart servers after deployment """ +import argparse import os +import sys import configobj from letsencrypt import configuration +from letsencrypt import cli from letsencrypt import client from letsencrypt import crypto_util from letsencrypt import notify from letsencrypt import storage + from letsencrypt.plugins import disco as plugins_disco @@ -92,7 +96,12 @@ def renew(cert, old_version): # (where fewer than all names were renewed) -def main(config=None): +def _create_parser(): + parser = argparse.ArgumentParser() + #parser.add_argument("--cron", action="store_true", help="Run as cronjob.") + return cli._paths_parser(parser) # pylint: disable=protected-access + +def main(config=None, args=sys.argv[1:]): """Main function for autorenewer script.""" # TODO: Distinguish automated invocation from manual invocation, # perhaps by looking at sys.argv[0] and inhibiting automated @@ -100,6 +109,9 @@ def main(config=None): # turned it off. (The boolean parameter should probably be # called renewer_enabled.) + cli_config = configuration.RenewerConfiguration( + _create_parser().parse_args(args)) + config = storage.config_with_defaults(config) # Now attempt to read the renewer config file and augment or replace # the renewer defaults with any options contained in that file. If @@ -108,14 +120,14 @@ def main(config=None): # elaborate renewer command line, we will presumably also be able to # specify a config file on the command line, which, if provided, should # take precedence over this one. - config.merge(configobj.ConfigObj(config.get("renewer_config_file", ""))) + config.merge(configobj.ConfigObj(cli_config.renewer_config_file)) - for i in os.listdir(config["renewal_configs_dir"]): + for i in os.listdir(cli_config.renewal_configs_dir): print "Processing", i if not i.endswith(".conf"): continue rc_config = configobj.ConfigObj( - os.path.join(config["renewal_configs_dir"], i)) + os.path.join(cli_config.renewal_configs_dir, i)) try: # TODO: Before trying to initialize the RenewableCert object, # we could check here whether the combination of the config diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index 2648be3ba..c314e3b00 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -78,14 +78,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes renewal configuration file and/or systemwide defaults. """ - def __init__(self, configfile, config_opts=None): + def __init__(self, configfile, config_opts=None, cli_config=None): """Instantiate a RenewableCert object from an existing lineage. :param configobj.ConfigObj configfile: an already-parsed - ConfigObj object made from reading the renewal config file - that defines this lineage. :param configobj.ConfigObj - config_opts: systemwide defaults for renewal properties not - otherwise specified in the individual renewal config file. + ConfigObj object made from reading the renewal config file + that defines this lineage. + + :param configobj.ConfigObj config_opts: systemwide defaults for + renewal properties not otherwise specified in the individual + renewal config file. :raises ValueError: if the configuration file's name didn't end in ".conf", or the file is missing or broken. @@ -93,6 +95,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes ConfigObj object. """ + self.cli_config = cli_config if isinstance(configfile, configobj.ConfigObj): if not os.path.basename(configfile.filename).endswith(".conf"): raise ValueError("renewal config file name must end in .conf") @@ -149,7 +152,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # Each element's link must point within the cert lineage's # directory within the official archive directory desired_directory = os.path.join( - self.configuration["archive_dir"], self.lineagename) + self.cli_config.archive_dir, self.lineagename) if not os.path.samefile(os.path.dirname(target), desired_directory): return False @@ -499,7 +502,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes @classmethod def new_lineage(cls, lineagename, cert, privkey, chain, - renewalparams=None, config=None): + renewalparams=None, config=None, cli_config=None): # pylint: disable=too-many-locals,too-many-arguments """Create a new certificate lineage. @@ -536,17 +539,15 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # the renewer defaults with any options contained in that file. If # renewer_config_file is undefined or if the file is nonexistent or # empty, this .merge() will have no effect. - config.merge(configobj.ConfigObj(config.get("renewer_config_file", ""))) + config.merge(configobj.ConfigObj(cli_config.renewer_config_file)) # Examine the configuration and find the new lineage's name - configs_dir = config["renewal_configs_dir"] - archive_dir = config["archive_dir"] - live_dir = config["live_dir"] - for i in (configs_dir, archive_dir, live_dir): + for i in (cli_config.renewal_configs_dir, cli_config.archive_dir, + cli_config.live_dir): if not os.path.exists(i): os.makedirs(i, 0700) - config_file, config_filename = le_util.unique_lineage_name(configs_dir, - lineagename) + config_file, config_filename = le_util.unique_lineage_name( + cli_config.renewal_configs_dir, lineagename) if not config_filename.endswith(".conf"): raise ValueError("renewal config file name must end in .conf") @@ -554,8 +555,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # lineagename will now potentially be modified based on which # renewal configuration file could actually be created lineagename = os.path.basename(config_filename)[:-len(".conf")] - archive = os.path.join(archive_dir, lineagename) - live_dir = os.path.join(live_dir, lineagename) + archive = os.path.join(cli_config.archive_dir, lineagename) + live_dir = os.path.join(cli_config.live_dir, lineagename) if os.path.exists(archive): raise ValueError("archive directory exists for " + lineagename) if os.path.exists(live_dir): @@ -593,7 +594,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # TODO: add human-readable comments explaining other available # parameters new_config.write() - return cls(new_config, config) + return cls(new_config, config, cli_config) def save_successor(self, prior_version, new_cert, new_privkey, new_chain): @@ -624,7 +625,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # Figure out what the new version is and hence where to save things target_version = self.next_free_version() - archive = self.configuration["archive_dir"] + archive = self.cli_config.archive_dir prefix = os.path.join(archive, self.lineagename) target = dict( [(kind, diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 0f85674d4..d68078c18 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -10,6 +10,7 @@ import configobj import mock import pytz +from letsencrypt import configuration from letsencrypt.storage import ALL_FOUR @@ -31,22 +32,24 @@ class RenewableCertTests(unittest.TestCase): def setUp(self): from letsencrypt import storage self.tempdir = tempfile.mkdtemp() + + self.cli_config = configuration.RenewerConfiguration( + namespace=mock.MagicMock(config_dir=self.tempdir)) + # TODO: maybe provide RenewerConfiguration.make_dirs? os.makedirs(os.path.join(self.tempdir, "live", "example.org")) os.makedirs(os.path.join(self.tempdir, "archive", "example.org")) os.makedirs(os.path.join(self.tempdir, "configs")) - defaults = configobj.ConfigObj() - defaults["live_dir"] = os.path.join(self.tempdir, "live") - defaults["archive_dir"] = os.path.join(self.tempdir, "archive") - defaults["renewal_configs_dir"] = os.path.join(self.tempdir, - "configs") + config = configobj.ConfigObj() for kind in ALL_FOUR: config[kind] = os.path.join(self.tempdir, "live", "example.org", kind + ".pem") config.filename = os.path.join(self.tempdir, "configs", "example.org.conf") - self.defaults = defaults # for main() test - self.test_rc = storage.RenewableCert(config, defaults) + + self.defaults = configobj.ConfigObj() + self.test_rc = storage.RenewableCert( + config, self.defaults, self.cli_config) def tearDown(self): shutil.rmtree(self.tempdir) @@ -457,60 +460,57 @@ class RenewableCertTests(unittest.TestCase): def test_new_lineage(self): """Test for new_lineage() class method.""" from letsencrypt import storage - config_dir = self.defaults["renewal_configs_dir"] - archive_dir = self.defaults["archive_dir"] - live_dir = self.defaults["live_dir"] - result = storage.RenewableCert.new_lineage("the-lineage.com", "cert", - "privkey", "chain", None, - self.defaults) + result = storage.RenewableCert.new_lineage( + "the-lineage.com", "cert", "privkey", "chain", None, + self.defaults, self.cli_config) # This consistency check tests most relevant properties about the # newly created cert lineage. self.assertTrue(result.consistent()) - self.assertTrue(os.path.exists(os.path.join(config_dir, - "the-lineage.com.conf"))) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) with open(result.fullchain) as f: self.assertEqual(f.read(), "cert" + "chain") # Let's do it again and make sure it makes a different lineage - result = storage.RenewableCert.new_lineage("the-lineage.com", "cert2", - "privkey2", "chain2", None, - self.defaults) - self.assertTrue(os.path.exists( - os.path.join(config_dir, "the-lineage.com-0001.conf"))) + result = storage.RenewableCert.new_lineage( + "the-lineage.com", "cert2", "privkey2", "chain2", None, + self.defaults, self.cli_config) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf"))) # Now trigger the detection of already existing files - os.mkdir(os.path.join(live_dir, "the-lineage.com-0002")) + os.mkdir(os.path.join( + self.cli_config.live_dir, "the-lineage.com-0002")) self.assertRaises(ValueError, storage.RenewableCert.new_lineage, "the-lineage.com", "cert3", "privkey3", "chain3", - None, self.defaults) - os.mkdir(os.path.join(archive_dir, "other-example.com")) + None, self.defaults, self.cli_config) + os.mkdir(os.path.join(self.cli_config.archive_dir, "other-example.com")) self.assertRaises(ValueError, storage.RenewableCert.new_lineage, "other-example.com", "cert4", "privkey4", "chain4", - None, self.defaults) + None, self.defaults, self.cli_config) # Make sure it can accept renewal parameters params = {"stuff": "properties of stuff", "great": "awesome"} - result = storage.RenewableCert.new_lineage("the-lineage.com", "cert2", - "privkey2", "chain2", - params, self.defaults) + result = storage.RenewableCert.new_lineage( + "the-lineage.com", "cert2", "privkey2", "chain2", + params, self.defaults, self.cli_config) # TODO: Conceivably we could test that the renewal parameters actually # got saved def test_new_lineage_nonexistent_dirs(self): """Test that directories can be created if they don't exist.""" from letsencrypt import storage - config_dir = self.defaults["renewal_configs_dir"] - archive_dir = self.defaults["archive_dir"] - live_dir = self.defaults["live_dir"] - shutil.rmtree(config_dir) - shutil.rmtree(archive_dir) - shutil.rmtree(live_dir) - storage.RenewableCert.new_lineage("the-lineage.com", "cert2", - "privkey2", "chain2", - None, self.defaults) + shutil.rmtree(self.cli_config.renewal_configs_dir) + shutil.rmtree(self.cli_config.archive_dir) + shutil.rmtree(self.cli_config.live_dir) + + storage.RenewableCert.new_lineage( + "the-lineage.com", "cert2", "privkey2", "chain2", + None, self.defaults, self.cli_config) self.assertTrue(os.path.exists( - os.path.join(config_dir, "the-lineage.com.conf"))) - self.assertTrue(os.path.exists( - os.path.join(live_dir, "the-lineage.com", "privkey.pem"))) - self.assertTrue(os.path.exists( - os.path.join(archive_dir, "the-lineage.com", "privkey1.pem"))) + os.path.join( + self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.live_dir, "the-lineage.com", "privkey.pem"))) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.archive_dir, "the-lineage.com", "privkey1.pem"))) @mock.patch("letsencrypt.storage.le_util.unique_lineage_name") def test_invalid_config_filename(self, mock_uln): @@ -518,7 +518,7 @@ class RenewableCertTests(unittest.TestCase): mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes" self.assertRaises(ValueError, storage.RenewableCert.new_lineage, "example.com", "cert", "privkey", "chain", - None, self.defaults) + None, self.defaults, self.cli_config) def test_bad_kind(self): self.assertRaises(ValueError, self.test_rc.current_target, "elephant") @@ -602,22 +602,23 @@ class RenewableCertTests(unittest.TestCase): mock_rc_instance.should_autorenew.return_value = True mock_rc_instance.latest_common_version.return_value = 10 mock_rc.return_value = mock_rc_instance - with open(os.path.join(self.defaults["renewal_configs_dir"], + with open(os.path.join(self.cli_config.renewal_configs_dir, "README"), "w") as f: f.write("This is a README file to make sure that the renewer is") f.write("able to correctly ignore files that don't end in .conf.") - with open(os.path.join(self.defaults["renewal_configs_dir"], + with open(os.path.join(self.cli_config.renewal_configs_dir, "example.org.conf"), "w") as f: # This isn't actually parsed in this test; we have a separate # test_initialization that tests the initialization, assuming # that configobj can correctly parse the config file. f.write("cert = cert.pem\nprivkey = privkey.pem\n") f.write("chain = chain.pem\nfullchain = fullchain.pem\n") - with open(os.path.join(self.defaults["renewal_configs_dir"], + with open(os.path.join(self.cli_config.renewal_configs_dir, "example.com.conf"), "w") as f: f.write("cert = cert.pem\nprivkey = privkey.pem\n") f.write("chain = chain.pem\nfullchain = fullchain.pem\n") - renewer.main(self.defaults) + renewer.main(self.defaults, args=[ + '--config-dir', self.cli_config.config_dir]) self.assertEqual(mock_rc.call_count, 2) self.assertEqual(mock_rc_instance.update_all_links_to.call_count, 2) self.assertEqual(mock_notify.notify.call_count, 4) @@ -630,7 +631,8 @@ class RenewableCertTests(unittest.TestCase): mock_happy_instance.should_autorenew.return_value = False mock_happy_instance.latest_common_version.return_value = 10 mock_rc.return_value = mock_happy_instance - renewer.main(self.defaults) + renewer.main(self.defaults, args=[ + '--config-dir', self.cli_config.config_dir]) self.assertEqual(mock_rc.call_count, 4) self.assertEqual(mock_happy_instance.update_all_links_to.call_count, 0) self.assertEqual(mock_notify.notify.call_count, 4) @@ -638,10 +640,11 @@ class RenewableCertTests(unittest.TestCase): def test_bad_config_file(self): from letsencrypt import renewer - with open(os.path.join(self.defaults["renewal_configs_dir"], + with open(os.path.join(self.cli_config.renewal_configs_dir, "bad.conf"), "w") as f: f.write("incomplete = configfile\n") - renewer.main(self.defaults) + renewer.main(self.defaults, args=[ + '--config-dir', self.cli_config.config_dir]) # The ValueError is caught inside and nothing happens. From ad79d7c8b6471c56c05c8ccccdaaffbbd2f4deb0 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 17:16:04 +0000 Subject: [PATCH 02/24] Adjust client reports to use RenewerConfiguration. Fix docs. --- letsencrypt/client.py | 5 ++--- letsencrypt/configuration.py | 5 ----- letsencrypt/constants.py | 6 +++--- letsencrypt/storage.py | 1 + letsencrypt/tests/client_test.py | 15 +++++++------- letsencrypt/tests/configuration_test.py | 26 ++++++++++++++++++++++--- 6 files changed, 37 insertions(+), 21 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index be89ea8b7..d04116de2 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -236,8 +236,7 @@ class Client(object): # pylint: disable=no-self-use """Informs the user about automatic renewal and deployment. - :param cert: Newly issued certificate - :type cert: :class:`letsencrypt.storage.RenewableCert` + :param .RenewableCert cert: Newly issued certificate """ if ("autorenew" not in cert.configuration @@ -256,7 +255,7 @@ class Client(object): msg += ("been enabled for your certificate. These settings can be " "configured in the directories under {0}.").format( - cert.configuration["renewal_configs_dir"]) + cert.cli_config.renewal_configs_dir) reporter = zope.component.getUtility(interfaces.IReporter) reporter.add_message(msg, reporter.LOW_PRIORITY, True) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 522d697d9..7bd5c2ca4 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -81,11 +81,6 @@ class NamespaceConfig(object): def rec_token_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.work_dir, constants.REC_TOKEN_DIR) - @property - def renewer_config_file(self): # pylint: disable=missing-docstring - return os.path.join( - self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME) - @property def temp_checkpoint_dir(self): # pylint: disable=missing-docstring return os.path.join( diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index d949c60ec..202871144 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -47,7 +47,7 @@ List of expected options parameters: """ ARCHIVE_DIR = "archive" -"""TODO relative to `IConfig.config_dir`.""" +"""Archive directory, relative to `IConfig.config_dir`.""" CONFIG_DIRS_MODE = 0o755 """Directory mode for ``.IConfig.config_dir`` et al.""" @@ -76,7 +76,7 @@ KEY_DIR = "keys" """Directory (relative to `IConfig.config_dir`) where keys are saved.""" LIVE_DIR = "live" -"""TODO relative to `IConfig.config_dir`.""" +"""Live directory, relative to `IConfig.config_dir`.""" TEMP_CHECKPOINT_DIR = "temp_checkpoint" """Temporary checkpoint directory (relative to `IConfig.work_dir`).""" @@ -86,7 +86,7 @@ REC_TOKEN_DIR = "recovery_tokens" `IConfig.work_dir`).""" RENEWAL_CONFIGS_DIR = "configs" -"""TODO relative to `IConfig.config_dir`.""" +"""Renewal configs directory, relative to `IConfig.config_dir`.""" RENEWER_CONFIG_FILENAME = "renewer.conf" """Renewer config file name (relative to `IConfig.config_dir`).""" diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index c314e3b00..4ad1216e6 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -88,6 +88,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes :param configobj.ConfigObj config_opts: systemwide defaults for renewal properties not otherwise specified in the individual renewal config file. + :param .RenewerConfiguration cli_config: :raises ValueError: if the configuration file's name didn't end in ".conf", or the file is missing or broken. diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 1fb9c2a03..59657b627 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -21,7 +21,8 @@ class ClientTest(unittest.TestCase): """Tests for letsencrypt.client.Client.""" def setUp(self): - self.config = mock.MagicMock(no_verify_ssl=False) + self.config = mock.MagicMock( + no_verify_ssl=False, config_dir="/etc/letsencrypt") # pylint: disable=star-args self.account = mock.MagicMock(**{"key.pem": KEY}) @@ -39,7 +40,6 @@ class ClientTest(unittest.TestCase): @mock.patch("letsencrypt.client.zope.component.getUtility") def test_report_new_account(self, mock_zope): # pylint: disable=protected-access - self.config.config_dir = "/usr/bin/coffee" self.account.recovery_token = "ECCENTRIC INVISIBILITY RHINOCEROS" self.account.email = "rhino@jungle.io" @@ -54,32 +54,33 @@ class ClientTest(unittest.TestCase): # pylint: disable=protected-access cert = mock.MagicMock() cert.configuration = configobj.ConfigObj() - cert.configuration["renewal_configs_dir"] = "/etc/letsencrypt/configs" + cert.cli_config = configuration.RenewerConfiguration(self.config) cert.configuration["autorenew"] = "True" cert.configuration["autodeploy"] = "True" self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("renewal and deployment has been" in msg) - self.assertTrue(cert.configuration["renewal_configs_dir"] in msg) + self.assertTrue(cert.cli_config.renewal_configs_dir in msg) cert.configuration["autorenew"] = "False" self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("deployment but not automatic renewal" in msg) - self.assertTrue(cert.configuration["renewal_configs_dir"] in msg) + self.assertTrue(cert.cli_config.renewal_configs_dir in msg) cert.configuration["autodeploy"] = "False" self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("renewal and deployment has not" in msg) - self.assertTrue(cert.configuration["renewal_configs_dir"] in msg) + self.assertTrue(cert.cli_config.renewal_configs_dir in msg) cert.configuration["autorenew"] = "True" self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("renewal but not automatic deployment" in msg) - self.assertTrue(cert.configuration["renewal_configs_dir"] in msg) + self.assertTrue(cert.cli_config.renewal_configs_dir in msg) + class DetermineAccountTest(unittest.TestCase): """Tests for letsencrypt.client.determine_authenticator.""" diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 38fea140a..3dee41d85 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -9,10 +9,10 @@ class NamespaceConfigTest(unittest.TestCase): """Tests for letsencrypt.configuration.NamespaceConfig.""" def setUp(self): - from letsencrypt.configuration import NamespaceConfig self.namespace = mock.MagicMock( config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar', server='https://acme-server.org:443/new') + from letsencrypt.configuration import NamespaceConfig self.config = NamespaceConfig(self.namespace) def test_proxy_getattr(self): @@ -38,7 +38,6 @@ class NamespaceConfigTest(unittest.TestCase): constants.IN_PROGRESS_DIR = '../p' constants.KEY_DIR = 'keys' constants.REC_TOKEN_DIR = '/r' - constants.RENEWER_CONFIG_FILENAME = 'r.conf' constants.TEMP_CHECKPOINT_DIR = 't' self.assertEqual( @@ -53,9 +52,30 @@ class NamespaceConfigTest(unittest.TestCase): self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p') self.assertEqual(self.config.key_dir, '/tmp/config/keys') self.assertEqual(self.config.rec_token_dir, '/r') - self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf') self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') +class RenewerConfigurationTest(unittest.TestCase): + """Test for letsencrypt.configuration.RenewerConfiguration.""" + + def setUp(self): + self.namespace = mock.MagicMock(config_dir='/tmp/config') + from letsencrypt.configuration import RenewerConfiguration + self.config = RenewerConfiguration(self.namespace) + + @mock.patch('letsencrypt.configuration.constants') + def test_dynamic_dirs(self, constants): + constants.ARCHIVE_DIR = "a" + constants.LIVE_DIR = 'l' + constants.RENEWAL_CONFIGS_DIR = "renewal_configs" + constants.RENEWER_CONFIG_FILENAME = 'r.conf' + + self.assertEqual(self.config.archive_dir, '/tmp/config/a') + self.assertEqual(self.config.live_dir, '/tmp/config/l') + self.assertEqual( + self.config.renewal_configs_dir, '/tmp/config/renewal_configs') + self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf') + + if __name__ == '__main__': unittest.main() # pragma: no cover From 5f01a90ce60b32b707a40ed0220c04ef5d8268b7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 18:14:33 +0000 Subject: [PATCH 03/24] Add simple Boulder integration test --- tests/boulder-integration.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100755 tests/boulder-integration.sh diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh new file mode 100755 index 000000000..ed6428618 --- /dev/null +++ b/tests/boulder-integration.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# Simple integration test, run as "./boulder-integration.sh auth" or +# adjust parameters: "./boulder-integration.sh --domain bang auth". + +root="$(mktemp -d)" + +# first three flags required, rest is handy defaults +letsencrypt \ + --server http://localhost:4000/acme/new-reg \ + --no-verify-ssl \ + --dvsni-port 5001 \ + --config-dir "$root/conf" \ + --work-dir "$root/work" \ + --text \ + --agree-tos \ + --email "" \ + --domains le.wtf \ + -vvvvvvv \ + "$@" + +# print at the end, so it's more visible +echo "\nRoot integration tests directory: $root" From 635e5852262ac1ea5c23351d0ad963032aef6f68 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 15 Jun 2015 10:17:17 +0000 Subject: [PATCH 04/24] Initial support for "auth --csr" (fixes: #370) --- letsencrypt/cli.py | 70 +++++++++++------- letsencrypt/client.py | 123 +++++++++++++++++-------------- letsencrypt/crypto_util.py | 21 ++++-- letsencrypt/tests/client_test.py | 57 ++++++++++++-- 4 files changed, 179 insertions(+), 92 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 3bdf2bfc6..ba5a0de9e 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -49,16 +49,20 @@ def _account_init(args, config): return None -def _common_run(args, config, acc, authenticator, installer): +def _find_domains(args, installer): if args.domains is None: - doms = display_ops.choose_names(installer) + domains = display_ops.choose_names(installer) else: - doms = args.domains + domains = args.domains - if not doms: + if not domains: sys.exit("Please specify --domains, or --installer that will " "help in domain names autodiscovery") + return domains + + +def _init_acme(config, acc, authenticator, installer): acme = client.Client(config, acc, authenticator, installer) # Validate the key and csr @@ -71,7 +75,7 @@ def _common_run(args, config, acc, authenticator, installer): except errors.LetsEncryptClientError: sys.exit("Unable to register an account with ACME server") - return acme, doms + return acme def run(args, config, plugins): @@ -99,18 +103,24 @@ def run(args, config, plugins): if installer is None or authenticator is None: return "Configurator could not be determined" - acme, doms = _common_run(args, config, acc, authenticator, installer) - # TODO: Handle errors from _common_run? - lineage = acme.obtain_and_enroll_certificate(doms, authenticator, - installer, plugins) + domains = _find_domains(args, installer) + # TODO: Handle errors from _init_acme? + acme = _init_acme(config, acc, authenticator, installer) + lineage = acme.obtain_and_enroll_certificate( + domains, authenticator, installer, plugins) if not lineage: return "Certificate could not be obtained" - acme.deploy_certificate(doms, lineage.privkey, lineage.cert, lineage.chain) - acme.enhance_config(doms, args.redirect) + acme.deploy_certificate(domains, lineage.privkey, lineage.cert, lineage.chain) + acme.enhance_config(domains, args.redirect) def auth(args, config, plugins): """Obtain a certificate (no install).""" + if args.domains is not None and args.csr is not None: + # TODO: --csr could have a priority, when --domains is + # supplied, check if CSR matches given domains? + return "--domains and --csr are mutually exclusive" + # XXX: Update for renewer / RenewableCert acc = _account_init(args, config) if acc is None: @@ -126,13 +136,18 @@ def auth(args, config, plugins): else: installer = None - # TODO: Handle errors from _common_run? - acme, doms = _common_run( - args, config, acc, authenticator=authenticator, installer=installer) - if not acme.obtain_and_enroll_certificate(doms, authenticator, installer, - plugins): - return "Certificate could not be obtained" + # TODO: Handle errors from _init_acme? + acme = _init_acme(config, acc, authenticator, installer) + if args.csr is not None: + certr, chain = acme.obtain_certificate_from_csr(le_util.CSR( + file=args.csr[0], data=args.csr[1], form="der")) + acme.save_certificate(certr, chain, args.cert_path, args.chain_path) + else: + domains = _find_domains(args, installer) + if not acme.obtain_and_enroll_certificate( + domains, authenticator, installer, plugins): + return "Certificate could not be obtained" def install(args, config, plugins): """Install (no auth).""" @@ -144,11 +159,11 @@ def install(args, config, plugins): installer = display_ops.pick_installer(config, args.installer, plugins) if installer is None: return "Installer could not be determined" - acme, doms = _common_run( - args, config, acc, authenticator=None, installer=installer) + domains = _find_domains(args, installer) + acme = _init_acme(config, acc, authenticator=None, installer=installer) assert args.cert_path is not None - acme.deploy_certificate(doms, acc.key.file, args.cert_path, args.chain_path) - acme.enhance_config(doms, args.redirect) + acme.deploy_certificate(domains, acc.key.file, args.cert_path, args.chain_path) + acme.enhance_config(domains, args.redirect) def revoke(args, unused_config, unused_plugins): @@ -203,10 +218,11 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDiplay rathern than print print str(available) -def read_file(filename): - """Returns the given file's contents with universal new line support. +def read_file(filename, mode="rb"): + """Returns the given file's contents. :param str filename: Filename + :param str mode: open mode (see `open`) :returns: A tuple of filename and its contents :rtype: tuple @@ -215,7 +231,7 @@ def read_file(filename): """ try: - return filename, open(filename, "rU").read() + return filename, open(filename, mode).read() except IOError as exc: raise argparse.ArgumentTypeError(exc.strerror) @@ -276,12 +292,16 @@ def create_parser(plugins): return subparser add_subparser("run", run) - add_subparser("auth", auth) + parser_auth = add_subparser("auth", auth) add_subparser("install", install) parser_revoke = add_subparser("revoke", revoke) parser_rollback = add_subparser("rollback", rollback) add_subparser("config_changes", config_changes) + parser_auth.add_argument( + "--csr", type=read_file, help="Path to a Certificate Signing " + "Request (CSR) in DER format.") + parser_plugins = add_subparser("plugins", plugins_cmd) parser_plugins.add_argument("--init", action="store_true") parser_plugins.add_argument("--prepare", action="store_true") diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 17bee6069..40fe899f3 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -4,6 +4,7 @@ import os import pkg_resources import M2Crypto +import OpenSSL.crypto import zope.component from acme import jose @@ -123,22 +124,17 @@ class Client(object): "{0}.".format(self.account.email)) reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True) - def obtain_certificate(self, domains, csr=None): - """Obtains a certificate from the ACME server. + def _obtain_certificate(self, domains, csr): + """Obtain certificate. - :meth:`.register` must be called before :meth:`.obtain_certificate` + Internal function with precondition that `domains` are + consistent with identifiers present in the `csr`. - .. todo:: This function does not currently handle CSR correctly. + :param .le_util.CSR csr: Certificate Signing Request must + contain requested domains, the key used to generate this CSR + can be different than self.authkey. - :param set domains: domains to get a certificate - - :param csr: CSR must contain requested domains, the key used to generate - this CSR can be different than self.authkey - :type csr: :class:`CSR` - - :returns: Certificate, private key, and certificate chain (all - PEM-encoded). - :rtype: `tuple` of `str` + :returns: Certificate Resource and certificate chain. """ if self.auth_handler is None: @@ -150,37 +146,47 @@ class Client(object): raise errors.LetsEncryptClientError( "Please register with the ACME server first.") - # Perform Challenges/Get Authorizations + logging.debug("CSR: %s, domains: %s", csr, domains) + authzr = self.auth_handler.get_authorizations(domains) - - # Create CSR from names - cert_key = crypto_util.init_save_key( - self.config.rsa_key_size, self.config.key_dir) - csr = crypto_util.init_save_csr( - cert_key, domains, self.config.cert_dir) - - # Retrieve certificate certr = self.network.request_issuance( jose.ComparableX509( M2Crypto.X509.load_request_der_string(csr.data)), authzr) + return certr, self.network.fetch_chain(certr) - cert_pem = certr.body.as_pem() - chain_pem = None - if certr.cert_chain_uri is not None: - chain_pem = self.network.fetch_chain(certr) + def obtain_certificate_from_csr(self, csr): + """Obtain certficiate from CSR. - if chain_pem is None: - # XXX: just to stop RenewableCert from complaining; this is - # probably not a good solution - chain_pem = "" - else: - chain_pem = chain_pem.as_pem() + :param .le_util.CSR csr: Certificate Signing Request. - return cert_pem, cert_key.pem, chain_pem + """ + return self._obtain_certificate( + # TODO: add CN to domains? + crypto_util.get_sans_from_csr( + csr.data, OpenSSL.crypto.FILETYPE_ASN1), csr) + + def obtain_certificate(self, domains): + """Obtains a certificate from the ACME server. + + :meth:`.register` must be called before :meth:`.obtain_certificate` + + :param set domains: domains to get a certificate + + :returns: Certificate, private key, and certificate chain (all + PEM-encoded). + :rtype: `tuple` of `str` + + """ + # Create CSR from names + key = crypto_util.init_save_key( + self.config.rsa_key_size, self.config.key_dir) + csr = crypto_util.init_save_csr(key, domains, self.config.cert_dir) + + return key, csr, self._obtain_certificate(domains, csr) def obtain_and_enroll_certificate( - self, domains, authenticator, installer, plugins, csr=None): + self, domains, authenticator, installer, plugins): """Obtain and enroll certificate. Get a new certificate for the specified domains using the specified @@ -196,14 +202,14 @@ class Client(object): :param plugins: A PluginsFactory object. - :param str csr: A preexisting CSR to use with this request. - :returns: A new :class:`letsencrypt.storage.RenewableCert` instance referred to the enrolled cert lineage, or False if the cert could not be obtained. """ - cert, privkey, chain = self.obtain_certificate(domains, csr) + key, _, (certr, chain) = self.obtain_certificate(domains) + + # TODO: remove this dirty hack self.config.namespace.authenticator = plugins.find_init( authenticator).name if installer is not None: @@ -218,8 +224,12 @@ class Client(object): params = vars(self.config.namespace) config = {"renewer_config_file": params["renewer_config_file"]} if "renewer_config_file" in params else None - renewable_cert = storage.RenewableCert.new_lineage(domains[0], cert, privkey, - chain, params, config) + + # XXX: just to stop RenewableCert from complaining; this is + # probably not a good solution + chain_pem = "" if chain is None else chain.as_pem() + renewable_cert = storage.RenewableCert.new_lineage( + domains[0], certr.body.as_pem(), key.pem, chain_pem, params, config) self._report_renewal_status(renewable_cert) return renewable_cert @@ -251,13 +261,15 @@ class Client(object): reporter = zope.component.getUtility(interfaces.IReporter) reporter.add_message(msg, reporter.LOW_PRIORITY, True) - def save_certificate(self, certr, cert_path, chain_path): + def save_certificate(self, certr, chain_cert, cert_path, chain_path): # pylint: disable=no-self-use """Saves the certificate received from the ACME server. :param certr: ACME "certificate" resource. :type certr: :class:`acme.messages.Certificate` + :param chain_cert: + :param str cert_path: Path to attempt to save the cert file :param str chain_path: Path to attempt to save the chain file @@ -267,6 +279,10 @@ class Client(object): :raises IOError: If unable to find room to write the cert files """ + for path in cert_path, chain_path: + le_util.make_or_verify_dir( + os.path.dirname(path), 0o755, os.geteuid()) + # try finally close cert_chain_abspath = None cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) @@ -279,22 +295,20 @@ class Client(object): logging.info("Server issued certificate; certificate written to %s", act_cert_path) - if certr.cert_chain_uri is not None: + if chain_cert is not None: + chain_file, act_chain_path = le_util.unique_file( + chain_path, 0o644) # TODO: Except - chain_cert = self.network.fetch_chain(certr) - if chain_cert is not None: - chain_file, act_chain_path = le_util.unique_file( - chain_path, 0o644) - chain_pem = chain_cert.as_pem() - try: - chain_file.write(chain_pem) - finally: - chain_file.close() + chain_pem = chain_cert.as_pem() + try: + chain_file.write(chain_pem) + finally: + chain_file.close() - logging.info("Cert chain written to %s", act_chain_path) + logging.info("Cert chain written to %s", act_chain_path) - # This expects a valid chain file - cert_chain_abspath = os.path.abspath(act_chain_path) + # This expects a valid chain file + cert_chain_abspath = os.path.abspath(act_chain_path) return os.path.abspath(act_cert_path), cert_chain_abspath @@ -383,8 +397,7 @@ def validate_key_csr(privkey, csr=None): :param privkey: Key associated with CSR :type privkey: :class:`letsencrypt.le_util.Key` - :param csr: CSR - :type csr: :class:`letsencrypt.le_util.CSR` + :param .le_util.CSR csr: CSR :raises letsencrypt.errors.LetsEncryptClientError: when validation fails diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 1eb565289..b2b5ecf58 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -90,6 +90,9 @@ def make_csr(key_str, domains): :param str key_str: RSA key. :param list domains: Domains included in the certificate. + .. todo:: Detect duplicates in `domains`? Using a set doesn't + preserve order... + :returns: new CSR in PEM and DER form containing all domains :rtype: tuple @@ -101,13 +104,17 @@ def make_csr(key_str, domains): csr = M2Crypto.X509.Request() csr.set_pubkey(pubkey) - name = csr.get_subject() - name.C = "US" - name.ST = "Michigan" - name.L = "Ann Arbor" - name.O = "EFF" - name.OU = "University of Michigan" - name.CN = domains[0] + # TODO: "The CSR MUST contain at least one extensionRequest + # attribute {{RFC2985}} requesting a subjectAltName extension, + # containing the requested identifiers." -> Subject (CN in + # particular) ignored? can be empty? + #name = csr.get_subject() + #name.C = "US" + #name.ST = "Michigan" + #name.L = "Ann Arbor" + #name.O = "EFF" + #name.OU = "University of Michigan" + #name.CN = domains[0] extstack = M2Crypto.X509.X509_Extension_Stack() ext = M2Crypto.X509.new_extension( diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 1fb9c2a03..511b2df60 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -6,8 +6,11 @@ import shutil import tempfile import configobj +import M2Crypto.X509 import mock +from acme import jose + from letsencrypt import account from letsencrypt import configuration from letsencrypt import le_util @@ -15,6 +18,8 @@ from letsencrypt import le_util KEY = pkg_resources.resource_string( __name__, os.path.join("testdata", "rsa512_key.pem")) +CSR_SAN = pkg_resources.resource_string( + __name__, os.path.join("testdata", "csr-san.der")) class ClientTest(unittest.TestCase): @@ -26,16 +31,58 @@ class ClientTest(unittest.TestCase): self.account = mock.MagicMock(**{"key.pem": KEY}) from letsencrypt.client import Client - with mock.patch("letsencrypt.client.network2") as network2: + with mock.patch("letsencrypt.client.network2.Network") as network: self.client = Client( - config=self.config, account_=self.account, dv_auth=None, - installer=None) - self.network2 = network2 + config=self.config, account_=self.account, + dv_auth=None, installer=None) + self.network = network def test_init_network_verify_ssl(self): - self.network2.Network.assert_called_once_with( + self.network.assert_called_once_with( mock.ANY, mock.ANY, verify_ssl=True) + def _mock_obtain_certificate(self): + self.client.auth_handler = mock.MagicMock() + self.network().request_issuance.return_value = mock.sentinel.certr + self.network().fetch_chain.return_value = mock.sentinel.chain + + def _check_obtain_certificate(self): + self.client.auth_handler.get_authorizations.assert_called_once_with( + ["example.com", "www.example.com"]) + self.network.request_issuance.assert_callend_once_with( + jose.ComparableX509( + M2Crypto.X509.load_request_der_string(CSR_SAN)), + self.client.auth_handler.get_authorizations()) + self.network().fetch_chain.assert_called_once_with(mock.sentinel.certr) + + def test_obtain_certificate_from_csr(self): + self._mock_obtain_certificate() + self.assertEqual( + (mock.sentinel.certr, mock.sentinel.chain), + self.client.obtain_certificate_from_csr(le_util.CSR( + form="der", file=None, data=CSR_SAN))) + self._check_obtain_certificate() + + @mock.patch("letsencrypt.client.crypto_util") + def test_obtain_certificate(self, mock_crypto_util): + self._mock_obtain_certificate() + + csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + mock_crypto_util.init_save_csr.return_value = csr + mock_crypto_util.init_save_key.return_value = mock.sentinel.key + domains = ["example.com", "www.example.com"] + + self.assertEqual( + self.client.obtain_certificate(domains), + (mock.sentinel.key, csr, ( + mock.sentinel.certr, mock.sentinel.chain))) + + mock_crypto_util.init_save_key.assert_called_once_with( + self.config.rsa_key_size, self.config.key_dir) + mock_crypto_util.init_save_csr.assert_called_once_with( + mock.sentinel.key, domains, self.config.cert_dir) + self._check_obtain_certificate() + @mock.patch("letsencrypt.client.zope.component.getUtility") def test_report_new_account(self, mock_zope): # pylint: disable=protected-access From 60cc02565845296ae0d68ab4a9991a207abe7c51 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 15 Jun 2015 11:03:11 +0000 Subject: [PATCH 05/24] Add generete-csr.sh script to examples. --- examples/.gitignore | 3 +++ examples/generate-csr.sh | 28 ++++++++++++++++++++++++++++ examples/openssl.cnf | 5 +++++ 3 files changed, 36 insertions(+) create mode 100644 examples/.gitignore create mode 100755 examples/generate-csr.sh create mode 100644 examples/openssl.cnf diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 000000000..abaf425d1 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,3 @@ +# generate-csr.sh: +/key.pem +/csr.der \ No newline at end of file diff --git a/examples/generate-csr.sh b/examples/generate-csr.sh new file mode 100755 index 000000000..c63f3c2d1 --- /dev/null +++ b/examples/generate-csr.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# This script generates a simple SAN CSR to be used with Let's Encrypt +# CA. Mostly intedened for "auth --csr" testing, but, since its easily +# auditable, feel free to adjust it and use on you production web +# server. + +if [ "$#" -lt 1 ] +then + echo "Usage: $0 domain [domain...]" >&2 + exit 1 +fi + +domains="DNS:$1" +shift +for x in "$@" +do + domains="$domains,DNS:$x" +done + +SAN="$domains" openssl req -config openssl.cnf \ + -new -nodes -subj '/' -reqexts san \ + -out csr.der \ + -keyout key.pem \ + -newkey rsa:2048 \ + -outform DER +# 512 or 1024 too low for Boulder, 2048 is smallest for tests + +echo "You can now run: letsencrypt auth --csr csr.der" diff --git a/examples/openssl.cnf b/examples/openssl.cnf new file mode 100644 index 000000000..a3e6f3895 --- /dev/null +++ b/examples/openssl.cnf @@ -0,0 +1,5 @@ +[ req ] +distinguished_name = req_distinguished_name +[ req_distinguished_name ] +[ san ] +subjectAltName=${ENV::SAN} From 8a9759bf88b9ac87b969c063b2aca3439b5bff2a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 16 Jun 2015 06:26:44 +0000 Subject: [PATCH 06/24] Update Client.obtain_* docs, simplify obtain_certificate() rtype. --- letsencrypt/client.py | 32 +++++++++++++++++++++----------- letsencrypt/tests/client_test.py | 3 +-- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 40fe899f3..da225690d 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -130,11 +130,14 @@ class Client(object): Internal function with precondition that `domains` are consistent with identifiers present in the `csr`. - :param .le_util.CSR csr: Certificate Signing Request must - contain requested domains, the key used to generate this CSR - can be different than self.authkey. + :param list domains: Domain names. + :param .le_util.CSR csr: DER-encoded Certificate Signing + Request. The key used to generate this CSR can be different + than `authkey`. - :returns: Certificate Resource and certificate chain. + :returns: `.CertificateResource` and certificate chain (as + returned by `.fetch_chain`). + :rtype: tuple """ if self.auth_handler is None: @@ -158,7 +161,12 @@ class Client(object): def obtain_certificate_from_csr(self, csr): """Obtain certficiate from CSR. - :param .le_util.CSR csr: Certificate Signing Request. + :param .le_util.CSR csr: DER-encoded Certificate Signing + Request. + + :returns: `.CertificateResource` and certificate chain (as + returned by `.fetch_chain`). + :rtype: tuple """ return self._obtain_certificate( @@ -169,13 +177,15 @@ class Client(object): def obtain_certificate(self, domains): """Obtains a certificate from the ACME server. - :meth:`.register` must be called before :meth:`.obtain_certificate` + `.register` must be called before `.obtain_certificate` :param set domains: domains to get a certificate - :returns: Certificate, private key, and certificate chain (all - PEM-encoded). - :rtype: `tuple` of `str` + :returns: `.CertificateResource`, certificate chain (as + returned by `.fetch_chain`), and newly generated private key + (`.le_util.Key`) and DER-encoded Certificate Signing Request + (`.le_util.CSR`). + :rtype: tuple """ # Create CSR from names @@ -183,7 +193,7 @@ class Client(object): self.config.rsa_key_size, self.config.key_dir) csr = crypto_util.init_save_csr(key, domains, self.config.cert_dir) - return key, csr, self._obtain_certificate(domains, csr) + return self._obtain_certificate(domains, csr) + (key, csr) def obtain_and_enroll_certificate( self, domains, authenticator, installer, plugins): @@ -207,7 +217,7 @@ class Client(object): not be obtained. """ - key, _, (certr, chain) = self.obtain_certificate(domains) + certr, chain, key, _ = self.obtain_certificate(domains) # TODO: remove this dirty hack self.config.namespace.authenticator = plugins.find_init( diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 511b2df60..7216acea7 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -74,8 +74,7 @@ class ClientTest(unittest.TestCase): self.assertEqual( self.client.obtain_certificate(domains), - (mock.sentinel.key, csr, ( - mock.sentinel.certr, mock.sentinel.chain))) + (mock.sentinel.certr, mock.sentinel.chain, mock.sentinel.key, csr)) mock_crypto_util.init_save_key.assert_called_once_with( self.config.rsa_key_size, self.config.key_dir) From 1cd47d4af3f0bd7a2b094ad2c2c38051f981511c Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 22 Jun 2015 23:23:57 -0700 Subject: [PATCH 07/24] first pass for ssl labs --- letsencrypt/display/ops.py | 17 +++++++++++++++-- letsencrypt/tests/display/ops_test.py | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index fa77e3566..0bb7f5977 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -205,8 +205,21 @@ def success_installation(domains): """ util(interfaces.IDisplay).notification( - "Congratulations! You have successfully enabled " - "%s!" % _gen_https_names(domains), pause=False) + "Congratulations! You have successfully enabled {0}! You should test your configuration:{1}" + "{2}".format( + _gen_https_names(domains), + os.linesep, + os.linesep.join(_gen_ssl_lab_urls(domains))), + pause=False) + + +def _gen_ssl_lab_urls(domains): + """Returns a list of urls. + + :param list domains: Each domain is a 'str' + + """ + return ["https://www.ssllabs.com/ssltest/analyze.html?d=%s", dom for dom in domains] def _gen_https_names(domains): diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 01daf0004..a1f9a65f6 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -182,6 +182,27 @@ class ChooseAccountTest(unittest.TestCase): self.assertTrue(self._call([self.acc1, self.acc2]) is None) +class GenSSLLabURLs(unittest.TestCase): + """Loose test of _gen_ssl_lab_urls. URL can change easily in the future""" + def setUp(self): + zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + + @classmethod + def _call(cls, domains): + from letsencrypt.display.ops import _gen_ssl_lab_urls + return _gen_ssl_lab_urls(domains) + + def test_zero(self): + self.assertEqual(self._call([]), []) + + def test_one(self): + self.assertTrue("eff.org" in self._call(["eff.org"])[0]) + + def test_two(self): + urls = self._call(["eff.org", "umich.edu"]) + self.assertTrue("eff.org" in urls[0]) + self.assertTrue("umich.edu" in urls[1]) + class GenHttpsNamesTest(unittest.TestCase): """Test _gen_https_names.""" def setUp(self): From f8384127c093b576d73840a529defe1751cb2685 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 24 Jun 2015 10:55:08 -0700 Subject: [PATCH 08/24] Format and fix ssl_labs printout --- letsencrypt/display/ops.py | 7 ++++--- letsencrypt/tests/display/ops_test.py | 5 +---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 0bb7f5977..48718f88c 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -205,11 +205,12 @@ def success_installation(domains): """ util(interfaces.IDisplay).notification( - "Congratulations! You have successfully enabled {0}! You should test your configuration:{1}" - "{2}".format( + "Congratulations! You have successfully enabled {0}!{1}{1}" + "You should test your configuration at:{1}{2}".format( _gen_https_names(domains), os.linesep, os.linesep.join(_gen_ssl_lab_urls(domains))), + height=(10 + len(domains)), pause=False) @@ -219,7 +220,7 @@ def _gen_ssl_lab_urls(domains): :param list domains: Each domain is a 'str' """ - return ["https://www.ssllabs.com/ssltest/analyze.html?d=%s", dom for dom in domains] + return ["https://www.ssllabs.com/ssltest/analyze.html?d=%s" % dom for dom in domains] def _gen_https_names(domains): diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index a1f9a65f6..48a51bf27 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -195,9 +195,6 @@ class GenSSLLabURLs(unittest.TestCase): def test_zero(self): self.assertEqual(self._call([]), []) - def test_one(self): - self.assertTrue("eff.org" in self._call(["eff.org"])[0]) - def test_two(self): urls = self._call(["eff.org", "umich.edu"]) self.assertTrue("eff.org" in urls[0]) @@ -328,7 +325,7 @@ class SuccessInstallationTest(unittest.TestCase): names = ["example.com", "abc.com"] self._call(names) - + self.assertEqual(mock_util().notification.call_count, 1) arg = mock_util().notification.call_args_list[0][0][0] From 512e02c837347f7ef821925ee61452645c97adaf Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Jun 2015 11:46:39 -0700 Subject: [PATCH 09/24] Added reporter messages for failed challenges. --- letsencrypt/auth_handler.py | 87 +++++++++++++++++++++++++++++++++++++ letsencrypt/reporter.py | 23 +++++++--- 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index d7d590878..502b0b76d 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -3,12 +3,15 @@ import itertools import logging import time +import zope.component + from acme import challenges from acme import messages from letsencrypt import achallenges from letsencrypt import constants from letsencrypt import errors +from letsencrypt import interfaces class AuthHandler(object): @@ -193,6 +196,7 @@ class AuthHandler(object): updated for _, updated in failed_achalls) if all_failed_achalls: + _report_failed_challs(all_failed_achalls) raise errors.FailedChallenges(all_failed_achalls) dom_to_check -= comp_domains @@ -480,3 +484,86 @@ def is_preferred(offered_challb, satisfied, different=True): return False return True + + +_ERROR_HELP_COMMON = ( + 'To fix these errors, please make sure that your domain name was entered ' + 'correctly and the DNS A/AAAA record(s) for that domain contains the ' + 'right IP address.' +) + + +_ERROR_HELP = { + 'connection' : + _ERROR_HELP_COMMON + ' Additionally, please check that your computer ' + 'has publicly routable IP address and no firewalls are preventing the ' + 'server from communicating with the client.', + 'dnssec' : + _ERROR_HELP_COMMON + ' Additionally, if you have DNSSEC enabled for ' + 'your domain, please ensure the signature is valid.', + 'malformed' : + 'To fix these errors, please make sure that you did not provide any ' + 'invalid information to the client and try running Let\'s Encrypt ' + 'again.', + 'serverInternal' : + 'Unfortunately, an error on the ACME server prevented you from completing ' + 'authorization. Please try again later.', + 'tls' : + _ERROR_HELP_COMMON + ' Additionally, please check that you have an up ' + 'to date TLS configuration that allows the server to communicate with ' + 'the Let\'s Encrypt client.', + 'unauthorized' : _ERROR_HELP_COMMON, + 'unknownHost' : + 'To fix these errors, please make sure that your domain name was ' + 'entered correctly.', +} + + +def _report_failed_challs(failed_achalls): + """Notifies the user about failed challenges. + + :param set failed_achalls: A set of failed + :class:`letsencrypt.achallenges.AnnotatedChallenge`. + + """ + problems = dict() + for achall in failed_achalls: + if achall.error: + problems.setdefault(achall.error.typ, []).append(achall) + + reporter = zope.component.getUtility(interfaces.IReporter) + for achalls in problems.itervalues(): + reporter.add_message(_generate_failed_chall_msg(achalls), 1, True) + + +def _generate_failed_chall_msg(failed_achalls): + """Creates a user friendly error message about failed challenges. + + :param list failed_achalls: A list of failed + :class:`letsencrypt.achallenges.AnnotatedChallenge` with the same error + type. + + :returns: A formatted error message for the client. + :rtype: str + + """ + typ = failed_achalls[0].error.typ + msg = [ + 'The following \'{0}\' errors were reported by the server:'.format(typ) + ] + + problems = dict() + for achall in failed_achalls: + problems.setdefault(achall.error.description, []).append(achall.domain) + for problem in problems: + domains = problems[problem] + domains.sort() + msg.append('\n\nDomains: ') + msg.append(', '.join(domains)) + msg.append('\nError: {0}'.format(problem)) + + if typ in _ERROR_HELP: + msg.append('\n\n') + msg.append(_ERROR_HELP[typ]) + + return "".join(msg) diff --git a/letsencrypt/reporter.py b/letsencrypt/reporter.py index 045c1befa..3045a7e19 100644 --- a/letsencrypt/reporter.py +++ b/letsencrypt/reporter.py @@ -46,9 +46,10 @@ class Reporter(object): printed if the program exits abnormally. """ - assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY - self.messages.put(self._msg_type(priority, msg, on_crash)) - logging.info("Reporting to user: %s", msg) + if msg: + assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY + self.messages.put(self._msg_type(priority, msg, on_crash)) + logging.info("Reporting to user: %s", msg) def atexit_print_messages(self, pid=os.getpid()): """Function to be registered with atexit to print messages. @@ -66,7 +67,8 @@ class Reporter(object): If there is an unhandled exception, only messages for which ``on_crash`` is ``True`` are printed. -""" + + """ bold_on = False if not self.messages.empty(): no_exception = sys.exc_info()[0] is None @@ -74,14 +76,21 @@ class Reporter(object): if bold_on: print self._BOLD print 'IMPORTANT NOTES:' - wrapper = textwrap.TextWrapper(initial_indent=' - ', - subsequent_indent=(' ' * 3)) + first_wrapper = textwrap.TextWrapper( + initial_indent=' - ', subsequent_indent=(' ' * 3)) + next_wrapper = textwrap.TextWrapper( + initial_indent=first_wrapper.subsequent_indent, + subsequent_indent=first_wrapper.subsequent_indent) while not self.messages.empty(): msg = self.messages.get() if no_exception or msg.on_crash: if bold_on and msg.priority > self.HIGH_PRIORITY: sys.stdout.write(self._RESET) bold_on = False - print wrapper.fill(msg.text) + lines = msg.text.splitlines() + print first_wrapper.fill(lines[0]) + if len(lines) > 1: + print "\n".join( + next_wrapper.fill(line) for line in lines[1:]) if bold_on: sys.stdout.write(self._RESET) From 8f760cf828c0b1038313ecdc3483014f2a0107a9 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Jun 2015 11:51:50 -0700 Subject: [PATCH 10/24] Cleaned up multiline statements --- letsencrypt/auth_handler.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 502b0b76d..4a685439f 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -489,8 +489,7 @@ def is_preferred(offered_challb, satisfied, _ERROR_HELP_COMMON = ( 'To fix these errors, please make sure that your domain name was entered ' 'correctly and the DNS A/AAAA record(s) for that domain contains the ' - 'right IP address.' -) + 'right IP address.') _ERROR_HELP = { @@ -515,8 +514,7 @@ _ERROR_HELP = { 'unauthorized' : _ERROR_HELP_COMMON, 'unknownHost' : 'To fix these errors, please make sure that your domain name was ' - 'entered correctly.', -} + 'entered correctly.',} def _report_failed_challs(failed_achalls): @@ -533,7 +531,8 @@ def _report_failed_challs(failed_achalls): reporter = zope.component.getUtility(interfaces.IReporter) for achalls in problems.itervalues(): - reporter.add_message(_generate_failed_chall_msg(achalls), 1, True) + reporter.add_message( + _generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY, True) def _generate_failed_chall_msg(failed_achalls): @@ -549,8 +548,7 @@ def _generate_failed_chall_msg(failed_achalls): """ typ = failed_achalls[0].error.typ msg = [ - 'The following \'{0}\' errors were reported by the server:'.format(typ) - ] + "The following '{0}' errors were reported by the server:".format(typ)] problems = dict() for achall in failed_achalls: From 67f67fea02446e53ccc0de78a7f566d04a2a9607 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 24 Jun 2015 14:53:28 -0700 Subject: [PATCH 11/24] Remove trailing whitespace --- letsencrypt/display/ops.py | 2 +- letsencrypt/tests/display/ops_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 48718f88c..1a1887b9a 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -216,7 +216,7 @@ def success_installation(domains): def _gen_ssl_lab_urls(domains): """Returns a list of urls. - + :param list domains: Each domain is a 'str' """ diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 48a51bf27..25be6bebc 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -183,7 +183,7 @@ class ChooseAccountTest(unittest.TestCase): class GenSSLLabURLs(unittest.TestCase): - """Loose test of _gen_ssl_lab_urls. URL can change easily in the future""" + """Loose test of _gen_ssl_lab_urls. URL can change easily in the future.""" def setUp(self): zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) @@ -325,7 +325,7 @@ class SuccessInstallationTest(unittest.TestCase): names = ["example.com", "abc.com"] self._call(names) - + self.assertEqual(mock_util().notification.call_count, 1) arg = mock_util().notification.call_args_list[0][0][0] From 9637142c4c5a19dad1722aa0274100a150a9d8a3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Jun 2015 15:27:34 -0700 Subject: [PATCH 12/24] Added auth_handler tests --- letsencrypt/auth_handler.py | 12 ++---- letsencrypt/tests/auth_handler_test.py | 51 +++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 4a685439f..019cb07dc 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -512,9 +512,7 @@ _ERROR_HELP = { 'to date TLS configuration that allows the server to communicate with ' 'the Let\'s Encrypt client.', 'unauthorized' : _ERROR_HELP_COMMON, - 'unknownHost' : - 'To fix these errors, please make sure that your domain name was ' - 'entered correctly.',} + 'unknownHost' : _ERROR_HELP_COMMON,} def _report_failed_challs(failed_achalls): @@ -552,16 +550,14 @@ def _generate_failed_chall_msg(failed_achalls): problems = dict() for achall in failed_achalls: - problems.setdefault(achall.error.description, []).append(achall.domain) + problems.setdefault(achall.error.description, set()).add(achall.domain) for problem in problems: - domains = problems[problem] - domains.sort() msg.append('\n\nDomains: ') - msg.append(', '.join(domains)) + msg.append(', '.join(sorted(problems[problem]))) msg.append('\nError: {0}'.format(problem)) if typ in _ERROR_HELP: msg.append('\n\n') msg.append(_ERROR_HELP[typ]) - return "".join(msg) + return ''.join(msg) diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 24bceb5f8..15ce6a490 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -216,7 +216,8 @@ class PollChallengesTest(unittest.TestCase): self.assertEqual(authzr.body.status, messages.STATUS_PENDING) @mock.patch("letsencrypt.auth_handler.time") - def test_poll_challenges_failure(self, unused_mock_time): + @mock.patch("letsencrypt.auth_handler.zope.component.getUtility") + def test_poll_challenges_failure(self, unused_mock_time, unused_mock_zope): self.mock_net.poll.side_effect = self._mock_poll_solve_one_invalid self.assertRaises( errors.AuthorizationError, self.handler._poll_challenges, @@ -420,6 +421,54 @@ class IsPreferredTest(unittest.TestCase): self._call(acme_util.DVSNI_P, frozenset([acme_util.DVSNI_P]))) +class ReportFailedChallsTest(unittest.TestCase): + """Tests for letsencrypt.auth_handler._report_failed_challs.""" + # pylint: disable=protected-access + + def setUp(self): + from letsencrypt import achallenges + + kwargs = { + "chall" : acme_util.SIMPLE_HTTP, + "uri": "uri", + "status": messages.STATUS_INVALID, + "error": messages.Error(typ="tls", detail="detail"), + } + + self.simple_http = achallenges.SimpleHTTP( + challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args + domain="example.com", + key=acme_util.KEY) + + kwargs["chall"] = acme_util.DVSNI + self.dvsni_same = achallenges.DVSNI( + challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args + domain="example.com", + key=acme_util.KEY) + + kwargs["error"] = messages.Error(typ="dnssec", detail="detail") + self.dvsni_diff = achallenges.DVSNI( + challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args + domain="foo.bar", + key=acme_util.KEY) + + @mock.patch("letsencrypt.auth_handler.zope.component.getUtility") + def test_same_error_and_domain(self, mock_zope): + from letsencrypt import auth_handler + + auth_handler._report_failed_challs([self.simple_http, self.dvsni_same]) + call_list = mock_zope().add_message.call_args_list + self.assertTrue(len(call_list) == 1) + self.assertIn("Domains: example.com\n", call_list[0][0][0]) + + @mock.patch("letsencrypt.auth_handler.zope.component.getUtility") + def test_different_errors_and_domains(self, mock_zope): + from letsencrypt import auth_handler + + auth_handler._report_failed_challs([self.simple_http, self.dvsni_diff]) + self.assertTrue(mock_zope().add_message.call_count == 2) + + def gen_auth_resp(chall_list): """Generate a dummy authorization response.""" return ["%s%s" % (chall.__class__.__name__, chall.domain) From d15a386f921bc4a75681c54c3a32c5b4cd20fad6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Jun 2015 18:24:54 -0700 Subject: [PATCH 13/24] Incorporated jdkasten's feedback --- letsencrypt/auth_handler.py | 56 +++++++++++++------------- letsencrypt/reporter.py | 7 ++-- letsencrypt/tests/auth_handler_test.py | 2 +- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 019cb07dc..43f7b9fd2 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -487,32 +487,32 @@ def is_preferred(offered_challb, satisfied, _ERROR_HELP_COMMON = ( - 'To fix these errors, please make sure that your domain name was entered ' - 'correctly and the DNS A/AAAA record(s) for that domain contains the ' - 'right IP address.') + "To fix these errors, please make sure that your domain name was entered " + "correctly and the DNS A/AAAA record(s) for that domain contains the " + "right IP address.") _ERROR_HELP = { - 'connection' : - _ERROR_HELP_COMMON + ' Additionally, please check that your computer ' - 'has publicly routable IP address and no firewalls are preventing the ' - 'server from communicating with the client.', - 'dnssec' : - _ERROR_HELP_COMMON + ' Additionally, if you have DNSSEC enabled for ' - 'your domain, please ensure the signature is valid.', - 'malformed' : - 'To fix these errors, please make sure that you did not provide any ' - 'invalid information to the client and try running Let\'s Encrypt ' - 'again.', - 'serverInternal' : - 'Unfortunately, an error on the ACME server prevented you from completing ' - 'authorization. Please try again later.', - 'tls' : - _ERROR_HELP_COMMON + ' Additionally, please check that you have an up ' - 'to date TLS configuration that allows the server to communicate with ' - 'the Let\'s Encrypt client.', - 'unauthorized' : _ERROR_HELP_COMMON, - 'unknownHost' : _ERROR_HELP_COMMON,} + "connection" : + _ERROR_HELP_COMMON + " Additionally, please check that your computer " + "has publicly routable IP address and no firewalls are preventing the " + "server from communicating with the client.", + "dnssec" : + _ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for " + "your domain, please ensure the signature is valid.", + "malformed" : + "To fix these errors, please make sure that you did not provide any " + "invalid information to the client and try running Let's Encrypt " + "again.", + "serverInternal" : + "Unfortunately, an error on the ACME server prevented you from completing " + "authorization. Please try again later.", + "tls" : + _ERROR_HELP_COMMON + " Additionally, please check that you have an up " + "to date TLS configuration that allows the server to communicate with " + "the Let's Encrypt client.", + "unauthorized" : _ERROR_HELP_COMMON, + "unknownHost" : _ERROR_HELP_COMMON,} def _report_failed_challs(failed_achalls): @@ -552,12 +552,12 @@ def _generate_failed_chall_msg(failed_achalls): for achall in failed_achalls: problems.setdefault(achall.error.description, set()).add(achall.domain) for problem in problems: - msg.append('\n\nDomains: ') - msg.append(', '.join(sorted(problems[problem]))) - msg.append('\nError: {0}'.format(problem)) + msg.append("\n\nDomains: ") + msg.append(", ".join(sorted(problems[problem]))) + msg.append("\nError: {0}".format(problem)) if typ in _ERROR_HELP: - msg.append('\n\n') + msg.append("\n\n") msg.append(_ERROR_HELP[typ]) - return ''.join(msg) + return "".join(msg) diff --git a/letsencrypt/reporter.py b/letsencrypt/reporter.py index 3045a7e19..dc3859535 100644 --- a/letsencrypt/reporter.py +++ b/letsencrypt/reporter.py @@ -46,10 +46,9 @@ class Reporter(object): printed if the program exits abnormally. """ - if msg: - assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY - self.messages.put(self._msg_type(priority, msg, on_crash)) - logging.info("Reporting to user: %s", msg) + assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY + self.messages.put(self._msg_type(priority, msg, on_crash)) + logging.info("Reporting to user: %s", msg) def atexit_print_messages(self, pid=os.getpid()): """Function to be registered with atexit to print messages. diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 15ce6a490..6a94baea7 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -459,7 +459,7 @@ class ReportFailedChallsTest(unittest.TestCase): auth_handler._report_failed_challs([self.simple_http, self.dvsni_same]) call_list = mock_zope().add_message.call_args_list self.assertTrue(len(call_list) == 1) - self.assertIn("Domains: example.com\n", call_list[0][0][0]) + self.assertTrue("Domains: example.com\n" in call_list[0][0][0]) @mock.patch("letsencrypt.auth_handler.zope.component.getUtility") def test_different_errors_and_domains(self, mock_zope): From b9f2823d6b2ceb4f037bae527f7ae6f83389b0c2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 05:35:15 +0000 Subject: [PATCH 14/24] renewer: _paths_parser --- letsencrypt/renewer.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 7ac22138d..57df57ffc 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -96,12 +96,21 @@ def renew(cert, old_version): # (where fewer than all names were renewed) +def _paths_parser(parser): + add = parser.add_argument_group("paths").add_argument + add("--config-dir", default=cli.flag_default("config_dir"), + help=cli.config_help("config_dir")) + add("--work-dir", default=cli.flag_default("work_dir"), + help=cli.config_help("work_dir")) + return parser + + def _create_parser(): parser = argparse.ArgumentParser() #parser.add_argument("--cron", action="store_true", help="Run as cronjob.") # pylint: disable=protected-access - cli._paths_parser(parser.add_argument_group("paths")) - return parser + return _paths_parser(parser) + def main(config=None, args=sys.argv[1:]): """Main function for autorenewer script.""" From 4de60f68ab034eb5401d1b1dc5f0319dfa367e76 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 05:35:49 +0000 Subject: [PATCH 15/24] Pass cli_config to RenewableCert --- letsencrypt/renewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 57df57ffc..fb3490198 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -148,7 +148,7 @@ def main(config=None, args=sys.argv[1:]): # RenewableCert object for this cert at all, which could # dramatically improve performance for large deployments # where autorenewal is widely turned off. - cert = storage.RenewableCert(rc_config) + cert = storage.RenewableCert(rc_config, cli_config=cli_config) except ValueError: # This indicates an invalid renewal configuration file, such # as one missing a required parameter (in the future, perhaps From 38b497ef73dd8688dddfcbd67f81b67dfe0f6334 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 05:36:02 +0000 Subject: [PATCH 16/24] renewer: fix bug where renewer.conf wasn't read. --- letsencrypt/renewer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index fb3490198..4a1e96a65 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -137,8 +137,9 @@ def main(config=None, args=sys.argv[1:]): print "Processing", i if not i.endswith(".conf"): continue - rc_config = configobj.ConfigObj( - os.path.join(cli_config.renewal_configs_dir, i)) + rc_config = configobj.ConfigObj(cli_config.renewer_config_file) + rc_config.merge(configobj.ConfigObj( + os.path.join(cli_config.renewal_configs_dir, i))) try: # TODO: Before trying to initialize the RenewableCert object, # we could check here whether the combination of the config From 77ddc68d8e12723a666fc63540ead43e76e73abc Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 05:53:39 +0000 Subject: [PATCH 17/24] Quickfix for wrong renewer target paths. IOError: [Errno 2] No such file or directory: '/tmp/le/config/archive/renewer/privkey2.pem' --- letsencrypt/renewer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 4a1e96a65..96f7c9863 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -140,6 +140,8 @@ def main(config=None, args=sys.argv[1:]): rc_config = configobj.ConfigObj(cli_config.renewer_config_file) rc_config.merge(configobj.ConfigObj( os.path.join(cli_config.renewal_configs_dir, i))) + # TODO: this is a dirty hack! + rc_config.filename = os.path.join(cli_config.renewal_configs_dir, i) try: # TODO: Before trying to initialize the RenewableCert object, # we could check here whether the combination of the config From 8bde250ff2835ec6294a3e1e1e1c99f59a05d7be Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 08:25:10 +0000 Subject: [PATCH 18/24] boulder-integration.sh: agree-tos -> agree-eula, force standalone. --- tests/boulder-integration.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index ed6428618..2172d7e35 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -12,9 +12,10 @@ letsencrypt \ --config-dir "$root/conf" \ --work-dir "$root/work" \ --text \ - --agree-tos \ + --agree-eula \ --email "" \ --domains le.wtf \ + --authenticator standalone \ -vvvvvvv \ "$@" From e0bc6aeb50e714e3c8372c50e4521b14e2ca51ff Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 08:26:02 +0000 Subject: [PATCH 19/24] Travis CI setup for integration testing (fixes #530). Candidate issues to be addressed: - race condition: start.sh might fail to start Boulder WFE before ./tests/boulder-integration.sh is run (unlikely, but possible, leading to flaky tests) - intertwined build logs (./start.sh boostraps in the background, integration test debug logs on server and client side) --- .travis.yml | 23 ++++++++++++++--------- tests/boulder-integration.sh | 13 ++++++------- tests/boulder-start.sh | 15 +++++++++++++++ 3 files changed, 35 insertions(+), 16 deletions(-) create mode 100755 tests/boulder-start.sh diff --git a/.travis.yml b/.travis.yml index 167d6ad74..6e29702ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,19 +3,24 @@ language: python # http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS before_install: travis_retry sudo ./bootstrap/ubuntu.sh -install: "travis_retry pip install tox coveralls" -script: "travis_retry tox" - -after_success: '[ "$TOXENV" == "cover" ] && coveralls' - # using separate envs with different TOXENVs creates 4x1 Travis build # matrix, which allows us to clearly distinguish which component under # test has failed env: - - TOXENV=py26 - - TOXENV=py27 - - TOXENV=lint - - TOXENV=cover + global: + - GOPATH=/tmp/go + matrix: + - TOXENV=py26 + - TOXENV=py27 + - TOXENV=lint + - TOXENV=cover + +install: "travis_retry pip install tox coveralls" +before_script: '[ "${TOXENV:0:2}" != "py" ] || ./tests/boulder-start.sh' +# TODO: eliminate substring slice bashism +script: 'travis_retry tox && ([ "${TOXENV:0:2}" != "py" ] || (source .tox/$TOXENV/bin/activate && ./tests/boulder-integration.sh))' + +after_success: '[ "$TOXENV" == "cover" ] && coveralls' notifications: email: false diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 2172d7e35..adb6ab528 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -1,8 +1,10 @@ -#!/bin/sh -# Simple integration test, run as "./boulder-integration.sh auth" or -# adjust parameters: "./boulder-integration.sh --domain bang auth". +#!/bin/sh -xe +# Simple integration test, make sure to activate virtualenv beforehand +# (source venv/bin/activate) and that you are running Boulder test +# instance (see ./boulder-start.sh). root="$(mktemp -d)" +echo "\nRoot integration tests directory: $root" # first three flags required, rest is handy defaults letsencrypt \ @@ -17,7 +19,4 @@ letsencrypt \ --domains le.wtf \ --authenticator standalone \ -vvvvvvv \ - "$@" - -# print at the end, so it's more visible -echo "\nRoot integration tests directory: $root" + auth diff --git a/tests/boulder-start.sh b/tests/boulder-start.sh new file mode 100755 index 000000000..49139ff3c --- /dev/null +++ b/tests/boulder-start.sh @@ -0,0 +1,15 @@ +#!/bin/sh -xe +# Download and run Boulder instance for integration testing + +export GOPATH="${GOPATH:-/tmp/go}" + +# $ go get github.com/letsencrypt/boulder +# package github.com/letsencrypt/boulder +# imports github.com/letsencrypt/boulder +# imports github.com/letsencrypt/boulder: no buildable Go source files in /tmp/go/src/github.com/letsencrypt/boulder + +go get -d github.com/letsencrypt/boulder/cmd/boulder +cd $GOPATH/src/github.com/letsencrypt/boulder +make -j4 # Travis has 2 cores per build instance. +./start.sh & +# Hopefully start.sh bootstraps before integration test is started... From 4057734c333f8b7e914f8029aed9230b26c406d8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 16:05:25 +0000 Subject: [PATCH 20/24] Add integrations tests for CSR. --- examples/generate-csr.sh | 6 +++--- tests/boulder-integration.sh | 38 +++++++++++++++++++++++------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/examples/generate-csr.sh b/examples/generate-csr.sh index c63f3c2d1..617319c3d 100755 --- a/examples/generate-csr.sh +++ b/examples/generate-csr.sh @@ -17,12 +17,12 @@ do domains="$domains,DNS:$x" done -SAN="$domains" openssl req -config openssl.cnf \ +SAN="$domains" openssl req -config "${OPENSSL_CNF:-openssl.cnf}" \ -new -nodes -subj '/' -reqexts san \ - -out csr.der \ + -out "${CSR_PATH:-csr.der}" \ -keyout key.pem \ -newkey rsa:2048 \ -outform DER # 512 or 1024 too low for Boulder, 2048 is smallest for tests -echo "You can now run: letsencrypt auth --csr csr.der" +echo "You can now run: letsencrypt auth --csr ${CSR_PATH:-csr.der}" diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index adb6ab528..fd4c27beb 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -6,17 +6,27 @@ root="$(mktemp -d)" echo "\nRoot integration tests directory: $root" -# first three flags required, rest is handy defaults -letsencrypt \ - --server http://localhost:4000/acme/new-reg \ - --no-verify-ssl \ - --dvsni-port 5001 \ - --config-dir "$root/conf" \ - --work-dir "$root/work" \ - --text \ - --agree-eula \ - --email "" \ - --domains le.wtf \ - --authenticator standalone \ - -vvvvvvv \ - auth +common() { + # first three flags required, rest is handy defaults + letsencrypt \ + --server http://localhost:4000/acme/new-reg \ + --no-verify-ssl \ + --dvsni-port 5001 \ + --config-dir "$root/conf" \ + --work-dir "$root/work" \ + --text \ + --agree-eula \ + --email "" \ + --authenticator standalone \ + -vvvvvvv "$@" +} + +common --domains le.wtf auth + +export CSR_PATH="${root}/csr.der" OPENSSL_CNF=examples/openssl.cnf +./examples/generate-csr.sh le.wtf +common auth --csr "$CSR_PATH" \ + --cert-path "${root}/csr/cert.pem" \ + --chain-path "${root}/csr/chain.pem" +openssl x509 -in "${root}/csr/0000_cert.pem" -text +openssl x509 -in "${root}/csr/0000_chain.pem" -text From d804853958c631074e577c597842f31e94c4410b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 16:08:52 +0000 Subject: [PATCH 21/24] Remove commented suject fields in make_csr --- letsencrypt/crypto_util.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index e6f0ab8bb..943fd27eb 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -104,17 +104,7 @@ def make_csr(key_str, domains): csr = M2Crypto.X509.Request() csr.set_pubkey(pubkey) - # TODO: "The CSR MUST contain at least one extensionRequest - # attribute {{RFC2985}} requesting a subjectAltName extension, - # containing the requested identifiers." -> Subject (CN in - # particular) ignored? can be empty? - #name = csr.get_subject() - #name.C = "US" - #name.ST = "Michigan" - #name.L = "Ann Arbor" - #name.O = "EFF" - #name.OU = "University of Michigan" - #name.CN = domains[0] + # TODO: what to put into csr.get_subject()? extstack = M2Crypto.X509.X509_Extension_Stack() ext = M2Crypto.X509.new_extension( From 040f434a61dad55afd2d08a667ec4d85f4124a0d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 17:16:22 +0000 Subject: [PATCH 22/24] Renewer integration tests for standalone. --- letsencrypt/renewer.py | 5 +++++ tests/boulder-integration.sh | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 96f7c9863..188eb72cf 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -12,6 +12,7 @@ import os import sys import configobj +import zope.component from letsencrypt import configuration from letsencrypt import cli @@ -20,6 +21,7 @@ from letsencrypt import crypto_util from letsencrypt import notify from letsencrypt import storage +from letsencrypt.display import util as display_util from letsencrypt.plugins import disco as plugins_disco @@ -64,6 +66,7 @@ def renew(cert, old_version): # XXX: this loses type data (for example, the fact that key_size # was an int, not a str) config.rsa_key_size = int(config.rsa_key_size) + config.dvsni_port = int(config.dvsni_port) try: authenticator = plugins[renewalparams["authenticator"]] except KeyError: @@ -120,6 +123,8 @@ def main(config=None, args=sys.argv[1:]): # turned it off. (The boolean parameter should probably be # called renewer_enabled.) + zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + cli_config = configuration.RenewerConfiguration( _create_parser().parse_args(args)) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index adb6ab528..596fbf748 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -5,14 +5,14 @@ root="$(mktemp -d)" echo "\nRoot integration tests directory: $root" +store_flags="--config-dir $root/conf --work-dir $root/work" # first three flags required, rest is handy defaults letsencrypt \ --server http://localhost:4000/acme/new-reg \ --no-verify-ssl \ --dvsni-port 5001 \ - --config-dir "$root/conf" \ - --work-dir "$root/work" \ + $store_flags \ --text \ --agree-eula \ --email "" \ @@ -20,3 +20,18 @@ letsencrypt \ --authenticator standalone \ -vvvvvvv \ auth + +# the following assumes that Boulder issues certificates for less than +# 10 years, otherwise renewal will not take place +cat < "$root/conf/renewer.conf" +renew_before_expiry = 10 years +deploy_before_expiry = 10 years +EOF +letsencrypt-renewer $store_flags +dir="$root/conf/archive/le.wtf" +for x in cert chain fullchain privkey; +do + latest="$(ls -1t $dir/ | grep -e "^${x}" | head -n1)" + live="$(readlink -f "$root/conf/live/le.wtf/${x}.pem")" + #[ "${dir}/${latest}" = "$live" ] # renewer fails this test +done From 3789922c0b00213f60f6d1c87b2ae24e7bfdb531 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 23 Jun 2015 11:16:36 +0000 Subject: [PATCH 23/24] Move AugeasConfigurator to letsencrypt_apache. https://github.com/letsencrypt/lets-encrypt-preview/pull/531#issuecomment-114285541 --- docs/api/augeas_configurator.rst | 5 ----- docs/pkgs/letsencrypt_apache.rst | 7 +++++++ {letsencrypt => letsencrypt_apache}/augeas_configurator.py | 0 letsencrypt_apache/configurator.py | 2 +- setup.py | 1 - 5 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 docs/api/augeas_configurator.rst rename {letsencrypt => letsencrypt_apache}/augeas_configurator.py (100%) diff --git a/docs/api/augeas_configurator.rst b/docs/api/augeas_configurator.rst deleted file mode 100644 index 402eee797..000000000 --- a/docs/api/augeas_configurator.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.augeas_configurator` --------------------------------------- - -.. automodule:: letsencrypt.augeas_configurator - :members: diff --git a/docs/pkgs/letsencrypt_apache.rst b/docs/pkgs/letsencrypt_apache.rst index 44966cca6..f78caa971 100644 --- a/docs/pkgs/letsencrypt_apache.rst +++ b/docs/pkgs/letsencrypt_apache.rst @@ -27,3 +27,10 @@ .. automodule:: letsencrypt_apache.parser :members: + + +:mod:`letsencrypt_apache.augeas_configurator` +============================================= + +.. automodule:: letsencrypt_apache.augeas_configurator + :members: diff --git a/letsencrypt/augeas_configurator.py b/letsencrypt_apache/augeas_configurator.py similarity index 100% rename from letsencrypt/augeas_configurator.py rename to letsencrypt_apache/augeas_configurator.py diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py index ee522d781..fd81be636 100644 --- a/letsencrypt_apache/configurator.py +++ b/letsencrypt_apache/configurator.py @@ -12,7 +12,6 @@ import zope.interface from acme import challenges from letsencrypt import achallenges -from letsencrypt import augeas_configurator from letsencrypt import constants as core_constants from letsencrypt import errors from letsencrypt import interfaces @@ -20,6 +19,7 @@ from letsencrypt import le_util from letsencrypt.plugins import common +from letsencrypt_apache import augeas_configurator from letsencrypt_apache import constants from letsencrypt_apache import dvsni from letsencrypt_apache import obj diff --git a/setup.py b/setup.py index ef819f50b..e2dd6f88e 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,6 @@ letsencrypt_install_requires = [ # https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions 'PyOpenSSL>=0.15', 'pyrfc3339', - 'python-augeas', 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 'pytz', 'requests', From f312bdac22d390bc2825ab47dfe49f6657942c45 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 18:36:06 +0000 Subject: [PATCH 24/24] Fix unittests for dvsni_port. --- letsencrypt/tests/renewer_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index d68078c18..a690ddfc2 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -574,6 +574,7 @@ class RenewableCertTests(unittest.TestCase): self.test_rc.configfile["renewalparams"]["rsa_key_size"] = "2048" self.test_rc.configfile["renewalparams"]["server"] = "acme.example.com" self.test_rc.configfile["renewalparams"]["authenticator"] = "fake" + self.test_rc.configfile["renewalparams"]["dvsni_port"] = "4430" mock_auth = mock.MagicMock() mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth} # Fails because "fake" != "apache"