From 8d8a95800c9fe05369cfb635dd3dfd14f545b259 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sat, 6 Feb 2016 12:14:42 -0800 Subject: [PATCH 01/44] Preliminary fix for #2386 --- letsencrypt/cli.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 838da4015..fc5a5439b 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -704,12 +704,18 @@ def obtain_cert(config, plugins, lineage=None): if config.dry_run: _report_successful_dry_run() - elif config.verb == "renew" and installer is not None: - # In case of a renewal, reload server to pick up new certificate. - # In principle we could have a configuration option to inhibit this - # from happening. - installer.restart() - print("reloaded") + elif config.verb == "renew": + if installer is None: + # Tell the user that the server was not restarted. + print("new certificate deployed without restart, fullchain", + lineage.fullchain) + else: + # In case of a renewal, reload server to pick up new certificate. + # In principle we could have a configuration option to inhibit this + # from happening. + installer.restart() + print("new certificate deployed with restart of plugin", + config.installer, "fullchain is", lineage.fullchain) _suggest_donation_if_appropriate(config) From a3fd5c73a6f713d9b3fa2125717e3af780b19da0 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sat, 6 Feb 2016 12:16:10 -0800 Subject: [PATCH 02/44] =?UTF-8?q?restart=20=E2=86=92=20reload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- letsencrypt/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index fc5a5439b..7d90361ef 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -707,14 +707,14 @@ def obtain_cert(config, plugins, lineage=None): elif config.verb == "renew": if installer is None: # Tell the user that the server was not restarted. - print("new certificate deployed without restart, fullchain", + print("new certificate deployed without reload, fullchain", lineage.fullchain) else: # In case of a renewal, reload server to pick up new certificate. # In principle we could have a configuration option to inhibit this # from happening. installer.restart() - print("new certificate deployed with restart of plugin", + print("new certificate deployed with reload of plugin", config.installer, "fullchain is", lineage.fullchain) _suggest_donation_if_appropriate(config) From 46984689ae95f637951a58a03f2a5aea265c18d4 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 6 Feb 2016 13:19:55 -0800 Subject: [PATCH 03/44] Attempt to get --csr and -w to play together --- letsencrypt/cli.py | 3 +-- letsencrypt/client.py | 23 ++++++++++++++++------- letsencrypt/tests/client_test.py | 23 ++++++++++++++++------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 838da4015..3b614d4b4 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -689,8 +689,7 @@ def obtain_cert(config, plugins, lineage=None): # This is a special case; cert and chain are simply saved if config.csr is not None: assert lineage is None, "Did not expect a CSR with a RenewableCert" - certr, chain = le_client.obtain_certificate_from_csr(le_util.CSR( - file=config.csr[0], data=config.csr[1], form="der")) + certr, chain = le_client.obtain_certificate_from_csr(_process_domain) if config.dry_run: logger.info( "Dry run: skipping saving certificate to %s", config.cert_path) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 57b21a55f..b4d6c5b56 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -228,21 +228,30 @@ class Client(object): authzr) return certr, self.acme.fetch_chain(certr) - def obtain_certificate_from_csr(self, csr): + def obtain_certificate_from_csr(self, domain_callback): """Obtain certficiate from CSR. - :param .le_util.CSR csr: DER-encoded Certificate Signing - Request. + :param function(config, domains) domain_callback: callback for each + domain extracted from the CSR, to ensure that webroot-map and similar + housekeeping in cli.py is performed correctly :returns: `.CertificateResource` and certificate chain (as returned by `.fetch_chain`). :rtype: tuple """ - return self._obtain_certificate( - # TODO: add CN to domains? - crypto_util.get_sans_from_csr( - csr.data, OpenSSL.crypto.FILETYPE_ASN1), csr) + + #raise TypeError("About to call %r" % le_util.CSR) + csr = le_util.CSR(file=self.config.csr[0], data=self.config.csr[1], form="der") + # TODO: add CN to domains? + try: + domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) + except: + raise TypeError("Failed %r %r %r" % (self.config.csr, csr, csr.data)) + for d in domains: + domain_callback(self.config, d) + + return self._obtain_certificate(domains, csr) def obtain_certificate(self, domains): """Obtains a certificate from the ACME server. diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 2f117f80c..f051b6618 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -82,6 +82,7 @@ class ClientTest(unittest.TestCase): no_verify_ssl=False, config_dir="/etc/letsencrypt") # pylint: disable=star-args self.account = mock.MagicMock(**{"key.pem": KEY}) + self.eg_domains = ["example.com", "www.example.com"] from letsencrypt.client import Client with mock.patch("letsencrypt.client.acme_client.Client") as acme: @@ -101,8 +102,7 @@ class ClientTest(unittest.TestCase): self.acme.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.client.auth_handler.get_authorizations.assert_called_once_with(self.eg_domains) self.acme.request_issuance.assert_called_once_with( jose.ComparableX509(OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)), @@ -111,11 +111,20 @@ class ClientTest(unittest.TestCase): 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_process_domain = mock.MagicMock() + test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR: + mock_CSR.return_value = test_csr + self.assertEqual( + (mock.sentinel.certr, mock.sentinel.chain), + self.client.obtain_certificate_from_csr(mock_process_domain)) + + # make sure cli processing occurred + cli_processed = (call[0][1] for call in mock_process_domain.call_args_list) + self.assertEqual(set(cli_processed), set(self.eg_domains)) + + # and that the cert was obtained correctly + self._check_obtain_certificate() @mock.patch("letsencrypt.client.crypto_util") def test_obtain_certificate(self, mock_crypto_util): From 89df062a1c6be00153807b4fb015b04e63f1d318 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 6 Feb 2016 13:38:35 -0800 Subject: [PATCH 04/44] Allow config.domains to exist in CSR mode --- letsencrypt/cli.py | 5 ----- letsencrypt/client.py | 12 ++++++++---- letsencrypt/tests/client_test.py | 3 ++- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 3b614d4b4..99ee7884a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -672,11 +672,6 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals def obtain_cert(config, plugins, lineage=None): """Implements "certonly": authenticate & obtain cert, but do not install it.""" - if config.domains and config.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" - try: # installers are used in auth mode to determine domain names installer, authenticator = choose_configurator_plugins(config, plugins, "certonly") diff --git a/letsencrypt/client.py b/letsencrypt/client.py index b4d6c5b56..046c58cc7 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -244,13 +244,17 @@ class Client(object): #raise TypeError("About to call %r" % le_util.CSR) csr = le_util.CSR(file=self.config.csr[0], data=self.config.csr[1], form="der") # TODO: add CN to domains? - try: - domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) - except: - raise TypeError("Failed %r %r %r" % (self.config.csr, csr, csr.data)) + domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) for d in domains: domain_callback(self.config, d) + csr_domains, config_domains = set(domains), set(self.config.domains) + if csr_domains != config_domains: + raise errors.ConfigurationError( + "Inconsistent domain requests:\ncsr:{0}\n:cli config{1}" + .format(", ".join(csr_domains), ", ".join(config_domains)) + ) + return self._obtain_certificate(domains, csr) def obtain_certificate(self, domains): diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index f051b6618..5e8fd57a7 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -115,13 +115,14 @@ class ClientTest(unittest.TestCase): test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR: mock_CSR.return_value = test_csr + self.client.config.domains=self.eg_domains self.assertEqual( (mock.sentinel.certr, mock.sentinel.chain), self.client.obtain_certificate_from_csr(mock_process_domain)) # make sure cli processing occurred cli_processed = (call[0][1] for call in mock_process_domain.call_args_list) - self.assertEqual(set(cli_processed), set(self.eg_domains)) + self.assertEqual(set(cli_processed), set(("example.com", "www.example.com"))) # and that the cert was obtained correctly self._check_obtain_certificate() From dd20788e1cc37e5a1ec80ac8de65c8b790fbe8d1 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 6 Feb 2016 13:39:32 -0800 Subject: [PATCH 05/44] lint --- letsencrypt/tests/client_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 5e8fd57a7..6a8899c3b 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -115,7 +115,7 @@ class ClientTest(unittest.TestCase): test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR: mock_CSR.return_value = test_csr - self.client.config.domains=self.eg_domains + self.client.config.domains = self.eg_domains self.assertEqual( (mock.sentinel.certr, mock.sentinel.chain), self.client.obtain_certificate_from_csr(mock_process_domain)) From 6df94bf68dff22f2dc91dac7f2d8f772de2e5793 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 6 Feb 2016 13:47:52 -0800 Subject: [PATCH 06/44] Better webroot configuration error Fixes: #2377 --- letsencrypt/plugins/webroot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/letsencrypt/plugins/webroot.py b/letsencrypt/plugins/webroot.py index f8176417c..3f5bc6d28 100644 --- a/letsencrypt/plugins/webroot.py +++ b/letsencrypt/plugins/webroot.py @@ -49,8 +49,10 @@ to serve all files under specified web root ({0}).""" path_map = self.conf("map") if not path_map: - raise errors.PluginError("--{0} must be set".format( - self.option_name("path"))) + raise errors.PluginError( + "Missing parts of webroot configuration; please set either " + "--webroot-path and --domains, or --webroot-map. Run with " + " --help webroot for examples.") for name, path in path_map.items(): if not os.path.isdir(path): raise errors.PluginError(path + " does not exist or is not a directory") From 2ba2dde9ed4740db196fffc095acd8928475bcf8 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sun, 7 Feb 2016 18:47:50 -0800 Subject: [PATCH 07/44] Fix some broken tests --- letsencrypt/tests/cli_test.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index a5757399e..1a86fb99b 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -228,7 +228,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods args = ["certonly", "--webroot"] ret, _, _, _ = self._call(args) - self.assertTrue("--webroot-path must be set" in ret) + self.assertTrue("please set either --webroot-path" in ret) self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably") @@ -323,9 +323,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertEqual(config.fullchain_path, os.path.abspath(fullchain)) def test_certonly_bad_args(self): - ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) - self.assertEqual(ret, '--domains and --csr are mutually exclusive') - ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly']) self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') From 7b0e70173126f46d5b8be57a48ec45672681db4b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sun, 7 Feb 2016 19:08:26 -0800 Subject: [PATCH 08/44] Fix error formatting --- letsencrypt/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 046c58cc7..413409ded 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -251,7 +251,7 @@ class Client(object): csr_domains, config_domains = set(domains), set(self.config.domains) if csr_domains != config_domains: raise errors.ConfigurationError( - "Inconsistent domain requests:\ncsr:{0}\n:cli config{1}" + "Inconsistent domain requests:\ncsr: {0}\ncli config: {1}" .format(", ".join(csr_domains), ", ".join(config_domains)) ) From f3655f9ab3ffde9f7a5be6580b7cb4b17beb62c5 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sun, 7 Feb 2016 19:14:24 -0800 Subject: [PATCH 09/44] Throw in an extra test for good measure --- letsencrypt/tests/client_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 6a8899c3b..222e9c707 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -127,6 +127,12 @@ class ClientTest(unittest.TestCase): # and that the cert was obtained correctly self._check_obtain_certificate() + # Now provoke an inconsistent domains error... + + self.client.config.domains.append("hippopotamus.io") + self.assertRaises(errors.ConfigurationError, + self.client.obtain_certificate_from_csr, mock_process_domain) + @mock.patch("letsencrypt.client.crypto_util") def test_obtain_certificate(self, mock_crypto_util): self._mock_obtain_certificate() From 9c7af6a93f72d73f50d87ba1715995c429d00061 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Feb 2016 13:22:49 -0800 Subject: [PATCH 10/44] Better reporting of renewal results --- letsencrypt/cli.py | 67 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 7d90361ef..8566765f8 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -471,7 +471,7 @@ def _auth_from_domains(le_client, config, domains, lineage=None): if lineage is False: raise errors.Error("Certificate could not be obtained") - if not config.dry_run: + if not config.dry_run and not config.verb == "renew": _report_new_cert(lineage.cert, lineage.fullchain) return lineage, action @@ -681,7 +681,9 @@ def obtain_cert(config, plugins, lineage=None): # installers are used in auth mode to determine domain names installer, authenticator = choose_configurator_plugins(config, plugins, "certonly") except errors.PluginSelectionError as e: - return e.message + logger.info( + "Could not choose appropriate plugin: %s", e) + raise # TODO: Handle errors from _init_le_client? le_client = _init_le_client(config, authenticator, installer) @@ -707,7 +709,7 @@ def obtain_cert(config, plugins, lineage=None): elif config.verb == "renew": if installer is None: # Tell the user that the server was not restarted. - print("new certificate deployed without reload, fullchain", + print("new certificate deployed without reload, fullchain is", lineage.fullchain) else: # In case of a renewal, reload server to pick up new certificate. @@ -877,6 +879,30 @@ def _renewal_conf_files(config): return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) +def _renew_describe_results(renew_successes, renew_failures, parse_failures): + print() + if not renew_successes and not renew_failures: + print("No renewals were attempted.") + elif renew_successes and not renew_failures: + print("Congratulations, all renewals succeeded. The following certs " + "have been renewed:") + print("\t" + "\n\t".join(x + " (success)" for x in renew_successes)) + elif renew_failures and not renew_successes: + print("All renewal attempts failed. The following certs could not be " + "renewed:") + print("\t" + "\n\t".join(x + " (failure)" for x in renew_failures)) + elif renew_failures and renew_successes: + print("The following certs were successfully renewed:") + print("\t" + "\n\t".join(x + " (success)" for x in renew_successes)) + print("\nThe following certs could not be renewed:") + print("\t" + "\n\t".join(x + " (failure)" for x in renew_failures)) + + if parse_failures: + print("\nAdditionally, the following renewal configuration files " + "were invalid: ") + print("\t" + "\n\t".join(x + " (parsefail)" for x in parse_failures)) + + def renew(config, unused_plugins): """Renew previously-obtained certificates.""" if config.domains != []: @@ -892,6 +918,9 @@ def renew(config, unused_plugins): "specifying a CSR file. Please try the certonly " "command instead.") renewer_config = configuration.RenewerConfiguration(config) + renew_successes = [] + renew_failures = [] + parse_failures = [] for renewal_file in _renewal_conf_files(renewer_config): print("Processing " + renewal_file) # XXX: does this succeed in making a fully independent config object @@ -907,28 +936,42 @@ def renew(config, unused_plugins): logger.warning("Renewal configuration file %s produced an " "unexpected error: %s. Skipping.", renewal_file, e) logger.debug("Traceback was:\n%s", traceback.format_exc()) + parse_failures.append(renewal_file) continue try: - if renewal_candidate is not None: + if renewal_candidate is None: + parse_failures.append(renewal_file) + else: # _reconstitute succeeded in producing a RenewableCert, so we # have something to work with from this particular config file. # XXX: ensure that each call here replaces the previous one zope.component.provideUtility(lineage_config) - print("Trying...") - # Because obtain_cert itself indirectly decides whether to renew - # or not, we couldn't currently make a UI/logging distinction at - # this stage to indicate whether renewal was actually attempted - # (or successful). - obtain_cert(lineage_config, - plugins_disco.PluginsRegistry.find_all(), - renewal_candidate) + # Although obtain_cert itself also indirectly decides + # whether to renew or not, we need to check at this + # stage in order to avoid claiming that renewal + # succeeded when it wasn't even attempted (since + # obtain_cert wouldn't raise an error in that case). + if _should_renew(lineage_config, renewal_candidate): + err = obtain_cert(lineage_config, + plugins_disco.PluginsRegistry.find_all(), + renewal_candidate) + if err is None: + renew_successes.append(renewal_candidate.fullchain) + else: + renew_failures.append(renewal_candidate.fullchain) + else: + print("We skipped this one at the outset!") except Exception as e: # pylint: disable=broad-except # obtain_cert (presumably) encountered an unanticipated problem. logger.warning("Attempting to renew cert from %s produced an " "unexpected error: %s. Skipping.", renewal_file, e) logger.debug("Traceback was:\n%s", traceback.format_exc()) + renew_failures.append(renewal_candidate.fullchain) + + # Describe all the results + _renew_describe_results(renew_successes, renew_failures, parse_failures) def revoke(config, unused_plugins): # TODO: coop with renewal config From 1fd3f8a8dcb9a3a8a3060bf168f7852c4cdde7cd Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Feb 2016 14:21:31 -0800 Subject: [PATCH 11/44] Making tests pass after CLI change --- letsencrypt/tests/cli_test.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index a5757399e..8731b8112 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -222,13 +222,16 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods if "nginx" in real_plugins: # Sending nginx a non-existent conf dir will simulate misconfiguration # (we can only do that if letsencrypt-nginx is actually present) - ret, _, _, _ = self._call(args) - self.assertTrue("The nginx plugin is not working" in ret) - self.assertTrue("MisconfigurationError" in ret) + self._call(args) + # XXX: This probably now raises an exception (when nginx is + # present, but I don't know which one!) + # self.assertTrue("The nginx plugin is not working" in ret) + # self.assertTrue("MisconfigurationError" in ret) args = ["certonly", "--webroot"] - ret, _, _, _ = self._call(args) - self.assertTrue("--webroot-path must be set" in ret) + # ret, _, _, _ = self._call(args) + self.assertRaises(errors.PluginSelectionError, self._call, args) + # self.assertTrue("--webroot-path must be set" in ret) self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably") @@ -324,10 +327,14 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods def test_certonly_bad_args(self): ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) - self.assertEqual(ret, '--domains and --csr are mutually exclusive') + # self.assertEqual(ret, '--domains and --csr are mutually exclusive') + # self.assertRaises(errors.Error, self._call, + # ['-d', 'foo.bar', 'certonly', '--csr', CSR]) - ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly']) - self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') + # ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly']) + self.assertRaises(errors.PluginSelectionError, self._call, + ['-a', 'bad_auth', 'certonly']) + # self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') def test_check_config_sanity_domain(self): # Punycode From d8ea828de6b84cf3393037d6ea07ede2a01abc53 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Feb 2016 16:11:20 -0800 Subject: [PATCH 12/44] Fix lint complaints about cli.py --- letsencrypt/cli.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 8566765f8..60b5fdbbc 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -338,6 +338,7 @@ def _handle_identical_cert_request(config, cert): else: assert False, "This is impossible" + def _handle_subset_cert_request(config, domains, cert): """Figure out what to do if a previous cert had a subset of the names now requested @@ -488,7 +489,7 @@ def _avoid_invalidating_lineage(config, lineage, original_server): open(lineage.cert).read()) # all our test certs are from happy hacker fake CA, though maybe one day # we should test more methodically - now_valid = not "fake" in repr(latest_cert.get_issuer()).lower() + now_valid = "fake" not in repr(latest_cert.get_issuer()).lower() if _is_staging(config.server): if not _is_staging(original_server) or now_valid: @@ -547,6 +548,7 @@ def set_configurator(previously, now): raise errors.PluginSelectionError(msg.format(repr(previously), repr(now))) return now + def cli_plugin_requests(config): """ Figure out which plugins the user requested with CLI and config options @@ -575,6 +577,7 @@ def cli_plugin_requests(config): noninstaller_plugins = ["webroot", "manual", "standalone"] + def choose_configurator_plugins(config, plugins, verb): """ Figure out which configurator we're going to use, modifies @@ -597,7 +600,7 @@ def choose_configurator_plugins(config, plugins, verb): '{1} {2} certonly --{0}{1}{1}' '(Alternatively, add a --installer flag. See https://eff.org/letsencrypt-plugins' '{1} and "--help plugins" for more information.)'.format( - req_auth, os.linesep, cli_command)) + req_auth, os.linesep, cli_command)) raise errors.MissingCommandlineFlag(msg) else: @@ -769,6 +772,7 @@ def _restore_required_config_elements(config, renewalparams): raise errors.Error( "Expected a numeric value for {0}".format(config_item)) + def _restore_plugin_configs(config, renewalparams): """Sets plugin specific values in config from renewalparams @@ -801,7 +805,7 @@ def _restore_plugin_configs(config, renewalparams): if config_value == "None": setattr(config.namespace, config_item, None) continue - for action in _parser.parser._actions: # pylint: disable=protected-access + for action in _parser.parser._actions: # pylint: disable=protected-access if action.type is not None and action.dest == config_item: setattr(config.namespace, config_item, action.type(config_value)) @@ -931,7 +935,7 @@ def renew(config, unused_plugins): # elements from within the renewal configuration file). try: renewal_candidate = _reconstitute(lineage_config, renewal_file) - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except # reconstitute encountered an unanticipated problem. logger.warning("Renewal configuration file %s produced an " "unexpected error: %s. Skipping.", renewal_file, e) @@ -963,7 +967,7 @@ def renew(config, unused_plugins): renew_failures.append(renewal_candidate.fullchain) else: print("We skipped this one at the outset!") - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except # obtain_cert (presumably) encountered an unanticipated problem. logger.warning("Attempting to renew cert from %s produced an " "unexpected error: %s. Skipping.", renewal_file, e) @@ -1447,7 +1451,7 @@ def prepare_and_parse_args(plugins, args): # parser (--help should display plugin-specific options last) _plugins_parsing(helpful, plugins) - global _parser # pylint: disable=global-statement + global _parser # pylint: disable=global-statement _parser = helpful return helpful.parse_args() @@ -1584,14 +1588,17 @@ def _plugins_parsing(helpful, plugins): "www.example.com -w /var/www/thing -d thing.net -d m.thing.net`") # --webroot-map still has some awkward properties, so it is undocumented helpful.add("webroot", "--webroot-map", default={}, action=WebrootMapProcessor, - help="JSON dictionary mapping domains to webroot paths; this implies -d " - "for each entry. You may need to escape this from your shell. " - """Eg: --webroot-map '{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' """ - "This option is merged with, but takes precedence over, -w / -d entries." - " At present, if you put webroot-map in a config file, it needs to be " - ' on a single line, like: webroot-map = {"example.com":"/var/www"}.') + help="JSON dictionary mapping domains to webroot paths; this " + "implies -d for each entry. You may need to escape this " + "from your shell. " + """E.g.: --webroot-map '{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' """ + "This option is merged with, but takes precedence over, " + "-w / -d entries. At present, if you put webroot-map in " + "a config file, it needs to be on a single line, like: " + 'webroot-map = {"example.com":"/var/www"}.') -class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring + +class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring def __init__(self, *args, **kwargs): self.domain_before_webroot = False argparse.Action.__init__(self, *args, **kwargs) @@ -1640,14 +1647,14 @@ def _process_domain(args_or_config, domain_arg, webroot_path=None): args_or_config.webroot_map.setdefault(domain, webroot_path[-1]) -class WebrootMapProcessor(argparse.Action): # pylint: disable=missing-docstring +class WebrootMapProcessor(argparse.Action): # pylint: disable=missing-docstring def __call__(self, parser, args, webroot_map_arg, option_string=None): webroot_map = json.loads(webroot_map_arg) for domains, webroot_path in webroot_map.iteritems(): _process_domain(args, domains, [webroot_path]) -class DomainFlagProcessor(argparse.Action): # pylint: disable=missing-docstring +class DomainFlagProcessor(argparse.Action): # pylint: disable=missing-docstring def __call__(self, parser, args, domain_arg, option_string=None): """Just wrap _process_domain in argparseese.""" _process_domain(args, domain_arg) @@ -1738,8 +1745,8 @@ def _handle_exception(exc_type, exc_value, trace, config): # acme.messages.Error: urn:acme:error:malformed :: The request message was # malformed :: Error creating new registration :: Validation of contact # mailto:none@longrandomstring.biz failed: Server failure at resolver - if ("urn:acme" in err and ":: " in err - and config.verbose_count <= flag_default("verbose_count")): + if (("urn:acme" in err and ":: " in err and + config.verbose_count <= flag_default("verbose_count"))): # prune ACME error code, we have a human description _code, _sep, err = err.partition(":: ") msg = "An unexpected error occurred:\n" + err + "Please see the " From 4d7ad032ee6d4cd3224d5195c0f7c7bbccbe7618 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Feb 2016 16:34:46 -0800 Subject: [PATCH 13/44] Mention skipped lineages too --- letsencrypt/cli.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index bae124d81..886616dbf 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -719,8 +719,8 @@ def obtain_cert(config, plugins, lineage=None): # In principle we could have a configuration option to inhibit this # from happening. installer.restart() - print("new certificate deployed with reload of plugin", - config.installer, "fullchain is", lineage.fullchain) + print("new certificate deployed with reload of", + config.installer, "server; fullchain is", lineage.fullchain) _suggest_donation_if_appropriate(config) @@ -883,8 +883,12 @@ def _renewal_conf_files(config): return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) -def _renew_describe_results(renew_successes, renew_failures, parse_failures): +def _renew_describe_results(renew_successes, renew_failures, renew_skipped, + parse_failures): print() + if renew_skipped: + print("The following certs are not due for renewal yet:") + print("\t" + "\n\t".join(x + " (skipped)" for x in renew_skipped)) if not renew_successes and not renew_failures: print("No renewals were attempted.") elif renew_successes and not renew_failures: @@ -924,11 +928,10 @@ def renew(config, unused_plugins): renewer_config = configuration.RenewerConfiguration(config) renew_successes = [] renew_failures = [] + renew_skipped = [] parse_failures = [] for renewal_file in _renewal_conf_files(renewer_config): print("Processing " + renewal_file) - # XXX: does this succeed in making a fully independent config object - # each time? lineage_config = copy.deepcopy(config) # Note that this modifies config (to add back the configuration @@ -966,7 +969,7 @@ def renew(config, unused_plugins): else: renew_failures.append(renewal_candidate.fullchain) else: - print("We skipped this one at the outset!") + renew_skipped.append(renewal_candidate.fullchain) except Exception as e: # pylint: disable=broad-except # obtain_cert (presumably) encountered an unanticipated problem. logger.warning("Attempting to renew cert from %s produced an " @@ -975,7 +978,8 @@ def renew(config, unused_plugins): renew_failures.append(renewal_candidate.fullchain) # Describe all the results - _renew_describe_results(renew_successes, renew_failures, parse_failures) + _renew_describe_results(renew_successes, renew_failures, renew_skipped, + parse_failures) def revoke(config, unused_plugins): # TODO: coop with renewal config From ad4b8ec147e6560fd3368fbf2274ca6f364b6778 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Feb 2016 16:41:42 -0800 Subject: [PATCH 14/44] lambda to simplify printing lists of success/failure --- letsencrypt/cli.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 886616dbf..27a935ca8 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -885,30 +885,31 @@ def _renewal_conf_files(config): def _renew_describe_results(renew_successes, renew_failures, renew_skipped, parse_failures): + status = lambda x, msg: " " + "\n ".join(i + " (" + msg +")" for i in x) print() if renew_skipped: print("The following certs are not due for renewal yet:") - print("\t" + "\n\t".join(x + " (skipped)" for x in renew_skipped)) + print(status(renew_skipped, "skipped")) if not renew_successes and not renew_failures: print("No renewals were attempted.") elif renew_successes and not renew_failures: print("Congratulations, all renewals succeeded. The following certs " "have been renewed:") - print("\t" + "\n\t".join(x + " (success)" for x in renew_successes)) + print(status(renew_successes, "success")) elif renew_failures and not renew_successes: print("All renewal attempts failed. The following certs could not be " "renewed:") - print("\t" + "\n\t".join(x + " (failure)" for x in renew_failures)) + print(status(renew_failures, "failure")) elif renew_failures and renew_successes: print("The following certs were successfully renewed:") - print("\t" + "\n\t".join(x + " (success)" for x in renew_successes)) + print(status(renew_successes, "success")) print("\nThe following certs could not be renewed:") - print("\t" + "\n\t".join(x + " (failure)" for x in renew_failures)) + print(status(renew_failures, "failure")) if parse_failures: print("\nAdditionally, the following renewal configuration files " "were invalid: ") - print("\t" + "\n\t".join(x + " (parsefail)" for x in parse_failures)) + print(status(parse_failures, "parsefail")) def renew(config, unused_plugins): From de455ac6e06244d747da0c8b219390377a5eae57 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Feb 2016 17:23:06 -0800 Subject: [PATCH 15/44] Don't check _should_renew twice --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 63c672227..19358a8bd 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -440,7 +440,7 @@ def _auth_from_domains(le_client, config, domains, lineage=None): else: # Renewal, where we already know the specific lineage we're # interested in - action = "renew" if _should_renew(config, lineage) else "reinstall" + action = "renew" if action == "reinstall": # The lineage already exists; allow the caller to try installing From d65a3c65c20ca7c12c4f85802a8bc84a14b95611 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 17:25:47 -0800 Subject: [PATCH 16/44] Revert "Allow webroot-map and --csr to exist together." --- letsencrypt/cli.py | 8 +++++++- letsencrypt/client.py | 27 +++++++-------------------- letsencrypt/plugins/webroot.py | 6 ++---- letsencrypt/tests/cli_test.py | 5 ++++- letsencrypt/tests/client_test.py | 24 +++++++----------------- 5 files changed, 27 insertions(+), 43 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index de1321ac9..c335d8d5b 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -672,6 +672,11 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals def obtain_cert(config, plugins, lineage=None): """Implements "certonly": authenticate & obtain cert, but do not install it.""" + if config.domains and config.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" + try: # installers are used in auth mode to determine domain names installer, authenticator = choose_configurator_plugins(config, plugins, "certonly") @@ -684,7 +689,8 @@ def obtain_cert(config, plugins, lineage=None): # This is a special case; cert and chain are simply saved if config.csr is not None: assert lineage is None, "Did not expect a CSR with a RenewableCert" - certr, chain = le_client.obtain_certificate_from_csr(_process_domain) + certr, chain = le_client.obtain_certificate_from_csr(le_util.CSR( + file=config.csr[0], data=config.csr[1], form="der")) if config.dry_run: logger.info( "Dry run: skipping saving certificate to %s", config.cert_path) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 413409ded..57b21a55f 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -228,34 +228,21 @@ class Client(object): authzr) return certr, self.acme.fetch_chain(certr) - def obtain_certificate_from_csr(self, domain_callback): + def obtain_certificate_from_csr(self, csr): """Obtain certficiate from CSR. - :param function(config, domains) domain_callback: callback for each - domain extracted from the CSR, to ensure that webroot-map and similar - housekeeping in cli.py is performed correctly + :param .le_util.CSR csr: DER-encoded Certificate Signing + Request. :returns: `.CertificateResource` and certificate chain (as returned by `.fetch_chain`). :rtype: tuple """ - - #raise TypeError("About to call %r" % le_util.CSR) - csr = le_util.CSR(file=self.config.csr[0], data=self.config.csr[1], form="der") - # TODO: add CN to domains? - domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) - for d in domains: - domain_callback(self.config, d) - - csr_domains, config_domains = set(domains), set(self.config.domains) - if csr_domains != config_domains: - raise errors.ConfigurationError( - "Inconsistent domain requests:\ncsr: {0}\ncli config: {1}" - .format(", ".join(csr_domains), ", ".join(config_domains)) - ) - - return self._obtain_certificate(domains, csr) + 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. diff --git a/letsencrypt/plugins/webroot.py b/letsencrypt/plugins/webroot.py index 3f5bc6d28..f8176417c 100644 --- a/letsencrypt/plugins/webroot.py +++ b/letsencrypt/plugins/webroot.py @@ -49,10 +49,8 @@ to serve all files under specified web root ({0}).""" path_map = self.conf("map") if not path_map: - raise errors.PluginError( - "Missing parts of webroot configuration; please set either " - "--webroot-path and --domains, or --webroot-map. Run with " - " --help webroot for examples.") + raise errors.PluginError("--{0} must be set".format( + self.option_name("path"))) for name, path in path_map.items(): if not os.path.isdir(path): raise errors.PluginError(path + " does not exist or is not a directory") diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 2d36a9d21..c41f45116 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -228,7 +228,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods args = ["certonly", "--webroot"] ret, _, _, _ = self._call(args) - self.assertTrue("please set either --webroot-path" in ret) + self.assertTrue("--webroot-path must be set" in ret) self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably") @@ -323,6 +323,9 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertEqual(config.fullchain_path, os.path.abspath(fullchain)) def test_certonly_bad_args(self): + ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) + self.assertEqual(ret, '--domains and --csr are mutually exclusive') + ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly']) self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 6a8899c3b..2f117f80c 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -82,7 +82,6 @@ class ClientTest(unittest.TestCase): no_verify_ssl=False, config_dir="/etc/letsencrypt") # pylint: disable=star-args self.account = mock.MagicMock(**{"key.pem": KEY}) - self.eg_domains = ["example.com", "www.example.com"] from letsencrypt.client import Client with mock.patch("letsencrypt.client.acme_client.Client") as acme: @@ -102,7 +101,8 @@ class ClientTest(unittest.TestCase): self.acme.fetch_chain.return_value = mock.sentinel.chain def _check_obtain_certificate(self): - self.client.auth_handler.get_authorizations.assert_called_once_with(self.eg_domains) + self.client.auth_handler.get_authorizations.assert_called_once_with( + ["example.com", "www.example.com"]) self.acme.request_issuance.assert_called_once_with( jose.ComparableX509(OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)), @@ -111,21 +111,11 @@ class ClientTest(unittest.TestCase): def test_obtain_certificate_from_csr(self): self._mock_obtain_certificate() - mock_process_domain = mock.MagicMock() - test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) - with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR: - mock_CSR.return_value = test_csr - self.client.config.domains = self.eg_domains - self.assertEqual( - (mock.sentinel.certr, mock.sentinel.chain), - self.client.obtain_certificate_from_csr(mock_process_domain)) - - # make sure cli processing occurred - cli_processed = (call[0][1] for call in mock_process_domain.call_args_list) - self.assertEqual(set(cli_processed), set(("example.com", "www.example.com"))) - - # and that the cert was obtained correctly - self._check_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): From 374e4ebb4dc11941f6f271f30872df6bb1bb658e Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Feb 2016 17:48:12 -0800 Subject: [PATCH 17/44] Trying to satisfy pylint --- letsencrypt/cli.py | 4 ++-- letsencrypt/tests/cli_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 19358a8bd..7dd0a7771 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1589,8 +1589,8 @@ def _plugins_parsing(helpful, plugins): helpful.add("webroot", "--webroot-map", default={}, action=WebrootMapProcessor, help="JSON dictionary mapping domains to webroot paths; this " "implies -d for each entry. You may need to escape this " - "from your shell. " - """E.g.: --webroot-map '{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' """ + "from your shell. E.g.: --webroot-map " + """'{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' """ "This option is merged with, but takes precedence over, " "-w / -d entries. At present, if you put webroot-map in " "a config file, it needs to be on a single line, like: " diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index c263fd8ec..76b7676c3 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -326,7 +326,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertEqual(config.fullchain_path, os.path.abspath(fullchain)) def test_certonly_bad_args(self): - ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) + _, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) # self.assertEqual(ret, '--domains and --csr are mutually exclusive') # self.assertRaises(errors.Error, self._call, # ['-d', 'foo.bar', 'certonly', '--csr', CSR]) From 24a3b66b1ca7eff88fdf6ee40514898793eaab6a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 17:52:12 -0800 Subject: [PATCH 18/44] Use server_close() in standalone --- letsencrypt/plugins/standalone.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index cde7041d8..71a17a28e 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -90,6 +90,9 @@ class ServerManager(object): logger.debug("Stopping server at %s:%d...", *instance.server.socket.getsockname()[:2]) instance.server.shutdown() + # Not calling server_close causes problems when renewing multiple + # certs with `letsencrypt renew` using TLSSNI01 and PyOpenSSL 0.13 + instance.server.server_close() instance.thread.join() del self._instances[port] From bb2f054f1b9529c77f8f8d536dacd8a508667ec2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 17:54:02 -0800 Subject: [PATCH 19/44] Take boulder-integration.sh from #2398 --- tests/boulder-integration.sh | 39 ++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index b6c76ee22..8b6dc5f1b 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -51,14 +51,45 @@ common --domains le3.wtf install \ --cert-path "${root}/csr/cert.pem" \ --key-path "${root}/csr/key.pem" +echo round 1 + +CheckCertCount() { + CERTCOUNT=`ls "${root}/conf/archive/le.wtf/"* | wc -l` + if [ "$CERTCOUNT" -ne "$1" ] ; then + echo Wrong cert count, not "$1" `ls "${root}/conf/archive/le.wtf/"*` + exit 1 + fi +} + +CheckCertCount 4 # This won't renew (because it's not time yet) -letsencrypt_test_no_force_renew --authenticator standalone --installer null renew +letsencrypt_test_no_force_renew --authenticator standalone --installer null renew -tvv +CheckCertCount 4 + +echo round 2 # This will renew because the expiry is less than 10 years from now -sed -i "4arenew_before_expiry = 10 years" "$root/conf/renewal/le1.wtf.conf" -letsencrypt_test_no_force_renew --authenticator standalone --installer null renew +sed -i "4arenew_before_expiry = 10 years" "$root/conf/renewal/le.wtf.conf" +letsencrypt_test_no_force_renew --authenticator standalone --installer null renew # --renew-by-default +CheckCertCount 8 + +echo round 3 + +# Check Param setting in renewal... +letsencrypt_test_no_force_renew --authenticator standalone --installer null renew --renew-by-default +CheckCertCount 12 +echo round 4 + +# The 4096 bit setting should persist to the first renewal, but be overriden in the second +size2=`wc -c ${root}/conf/archive/le.wtf/privkey2.pem | cut -d" " -f1` +size3=`wc -c ${root}/conf/archive/le.wtf/privkey3.pem | cut -d" " -f1` +#if ! [ "$size3" -lt "$size2" ] ; then +# echo "key size failure:" +# ls -l ${root}/conf/archive/le.wtf/ +# exit 1 +#fi + -ls "$root/conf/archive/le1.wtf" # dir="$root/conf/archive/le1.wtf" # for x in cert chain fullchain privkey; # do From 38a6d442796c6a4973365f67a96affeb11b612df Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 17:54:39 -0800 Subject: [PATCH 20/44] Remove round echos --- tests/boulder-integration.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 8b6dc5f1b..53e9b3f15 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -51,8 +51,6 @@ common --domains le3.wtf install \ --cert-path "${root}/csr/cert.pem" \ --key-path "${root}/csr/key.pem" -echo round 1 - CheckCertCount() { CERTCOUNT=`ls "${root}/conf/archive/le.wtf/"* | wc -l` if [ "$CERTCOUNT" -ne "$1" ] ; then @@ -66,19 +64,14 @@ CheckCertCount 4 letsencrypt_test_no_force_renew --authenticator standalone --installer null renew -tvv CheckCertCount 4 -echo round 2 - # This will renew because the expiry is less than 10 years from now sed -i "4arenew_before_expiry = 10 years" "$root/conf/renewal/le.wtf.conf" letsencrypt_test_no_force_renew --authenticator standalone --installer null renew # --renew-by-default CheckCertCount 8 -echo round 3 - # Check Param setting in renewal... letsencrypt_test_no_force_renew --authenticator standalone --installer null renew --renew-by-default CheckCertCount 12 -echo round 4 # The 4096 bit setting should persist to the first renewal, but be overriden in the second size2=`wc -c ${root}/conf/archive/le.wtf/privkey2.pem | cut -d" " -f1` From 8eb889d94251d36189ea7f5f23e7e26f91fdd901 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 17:55:28 -0800 Subject: [PATCH 21/44] Make CheckCertCount check cert counts --- tests/boulder-integration.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 53e9b3f15..dd6c1835e 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -52,26 +52,26 @@ common --domains le3.wtf install \ --key-path "${root}/csr/key.pem" CheckCertCount() { - CERTCOUNT=`ls "${root}/conf/archive/le.wtf/"* | wc -l` + CERTCOUNT=`ls "${root}/conf/archive/le.wtf/cert*" | wc -l` if [ "$CERTCOUNT" -ne "$1" ] ; then echo Wrong cert count, not "$1" `ls "${root}/conf/archive/le.wtf/"*` exit 1 fi } -CheckCertCount 4 +CheckCertCount 1 # This won't renew (because it's not time yet) letsencrypt_test_no_force_renew --authenticator standalone --installer null renew -tvv -CheckCertCount 4 +CheckCertCount 1 # This will renew because the expiry is less than 10 years from now sed -i "4arenew_before_expiry = 10 years" "$root/conf/renewal/le.wtf.conf" letsencrypt_test_no_force_renew --authenticator standalone --installer null renew # --renew-by-default -CheckCertCount 8 +CheckCertCount 2 # Check Param setting in renewal... letsencrypt_test_no_force_renew --authenticator standalone --installer null renew --renew-by-default -CheckCertCount 12 +CheckCertCount 3 # The 4096 bit setting should persist to the first renewal, but be overriden in the second size2=`wc -c ${root}/conf/archive/le.wtf/privkey2.pem | cut -d" " -f1` From 77616a975bbea2b9166efe578314ca9ec5d773f2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 17:59:30 -0800 Subject: [PATCH 22/44] Allow non-interactive with test-mode --- letsencrypt/plugins/manual.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 54244db2a..0e516b5b0 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -91,7 +91,7 @@ s.serve_forever()" """ help="Automatically allows public IP logging.") def prepare(self): # pylint: disable=missing-docstring,no-self-use - if self.config.noninteractive_mode: + if self.config.noninteractive_mode and not self.conf("test-mode"): raise errors.PluginError("Running manual mode non-interactively is not supported") def more_info(self): # pylint: disable=missing-docstring,no-self-use From c23aa37f4b910e5357c191b729d50e2d7042a715 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 18:06:46 -0800 Subject: [PATCH 23/44] Refactor --csr handling to run early enough for --webroot --- letsencrypt/cli.py | 24 +++++++++++++++++++++++- letsencrypt/client.py | 32 ++------------------------------ letsencrypt/tests/client_test.py | 17 +++++++++++------ 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index de1321ac9..c9c58ea3b 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -684,7 +684,7 @@ def obtain_cert(config, plugins, lineage=None): # This is a special case; cert and chain are simply saved if config.csr is not None: assert lineage is None, "Did not expect a CSR with a RenewableCert" - certr, chain = le_client.obtain_certificate_from_csr(_process_domain) + certr, chain = le_client.obtain_certificate_from_csr(config.domains, config.actual_csr) if config.dry_run: logger.info( "Dry run: skipping saving certificate to %s", config.cert_path) @@ -1106,8 +1106,30 @@ class HelpfulArgumentParser(object): "'certonly' or 'renew' subcommands") parsed_args.break_my_certs = parsed_args.staging = True + if parsed_args.csr: + self.handle_csr(parsed_args) + return parsed_args + def handle_csr(self, parsed_args): + """ + Process a --csr flag. This needs to happen early enought that the + webroot plugin can know about the calls to _process_domain + """ + csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der") + # TODO: add CN to domains? + domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) + for d in domains: + _process_domain(parsed_args, d) + parsed_args.actual_csr = csr + csr_domains, config_domains = set(domains), set(parsed_args.domains) + if csr_domains != config_domains: + raise errors.ConfigurationError( + "Inconsistent domain requests:\ncsr: {0}\ncli config: {1}" + .format(", ".join(csr_domains), ", ".join(config_domains)) + ) + + def determine_verb(self): """Determines the verb/subcommand provided by the user. diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 413409ded..fd851c163 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -195,7 +195,7 @@ class Client(object): else: self.auth_handler = None - def _obtain_certificate(self, domains, csr): + def obtain_certificate_from_csr(self, domains, csr): """Obtain certificate. Internal function with precondition that `domains` are @@ -228,34 +228,6 @@ class Client(object): authzr) return certr, self.acme.fetch_chain(certr) - def obtain_certificate_from_csr(self, domain_callback): - """Obtain certficiate from CSR. - - :param function(config, domains) domain_callback: callback for each - domain extracted from the CSR, to ensure that webroot-map and similar - housekeeping in cli.py is performed correctly - - :returns: `.CertificateResource` and certificate chain (as - returned by `.fetch_chain`). - :rtype: tuple - - """ - - #raise TypeError("About to call %r" % le_util.CSR) - csr = le_util.CSR(file=self.config.csr[0], data=self.config.csr[1], form="der") - # TODO: add CN to domains? - domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) - for d in domains: - domain_callback(self.config, d) - - csr_domains, config_domains = set(domains), set(self.config.domains) - if csr_domains != config_domains: - raise errors.ConfigurationError( - "Inconsistent domain requests:\ncsr: {0}\ncli config: {1}" - .format(", ".join(csr_domains), ", ".join(config_domains)) - ) - - return self._obtain_certificate(domains, csr) def obtain_certificate(self, domains): """Obtains a certificate from the ACME server. @@ -276,7 +248,7 @@ class Client(object): self.config.rsa_key_size, self.config.key_dir) csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) - return self._obtain_certificate(domains, csr) + (key, csr) + return self.obtain_certificate_from_csr(domains, csr) + (key, csr) def obtain_and_enroll_certificate(self, domains): """Obtain and enroll certificate. diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 6a8899c3b..d75237bab 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -109,21 +109,26 @@ class ClientTest(unittest.TestCase): self.client.auth_handler.get_authorizations()) self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr) - def test_obtain_certificate_from_csr(self): + # FIXME move parts of this to test_cli.py... + @mock.patch("letsencrypt.cli._process_domain") + def test_obtain_certificate_from_csr(self, mock_process_domain): self._mock_obtain_certificate() - mock_process_domain = mock.MagicMock() + from letsencrypt import cli test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + mock_parsed_args = mock.MagicMock() with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR: mock_CSR.return_value = test_csr - self.client.config.domains = self.eg_domains - self.assertEqual( - (mock.sentinel.certr, mock.sentinel.chain), - self.client.obtain_certificate_from_csr(mock_process_domain)) + mock_parsed_args.domains = self.eg_domains + mock_parser = mock.MagicMock(cli.HelpfulArgumentParser) + cli.HelpfulArgumentParser.handle_csr(mock_parser, mock_parsed_args) # make sure cli processing occurred cli_processed = (call[0][1] for call in mock_process_domain.call_args_list) self.assertEqual(set(cli_processed), set(("example.com", "www.example.com"))) + self.assertEqual( + (mock.sentinel.certr, mock.sentinel.chain), + self.client.obtain_certificate_from_csr(self.eg_domains, test_csr)) # and that the cert was obtained correctly self._check_obtain_certificate() From 4038be9816cbc4b942a6afad0377f84fc6944008 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 18:06:56 -0800 Subject: [PATCH 24/44] Test manual prepare() --- letsencrypt/plugins/manual_test.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index e16fadd13..e749eb1f9 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -23,16 +23,21 @@ class AuthenticatorTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.manual import Authenticator self.config = mock.MagicMock( - http01_port=8080, manual_test_mode=False, manual_public_ip_logging_ok=False) + http01_port=8080, manual_test_mode=False, + manual_public_ip_logging_ok=False, noninteractive_mode=True) self.auth = Authenticator(config=self.config, name="manual") self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)] config_test_mode = mock.MagicMock( - http01_port=8080, manual_test_mode=True) + http01_port=8080, manual_test_mode=True, noninteractive_mode=True) self.auth_test_mode = Authenticator( config=config_test_mode, name="manual") + def test_prepare(self): + self.assertRaises(errors.PluginError, self.auth.prepare) + self.auth_test_mode.prepare() # error not raised + def test_more_info(self): self.assertTrue(isinstance(self.auth.more_info(), str)) From 70402790a3cb8324eee230f304e8db53ce3442f6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 18:07:56 -0800 Subject: [PATCH 25/44] Use --non-interactive instead of --text --- tests/integration/_common.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 9230cc682..db6e2f0f1 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -19,7 +19,7 @@ letsencrypt_test () { --http-01-port 5002 \ --manual-test-mode \ $store_flags \ - --text \ + --non-interactive \ --no-redirect \ --agree-tos \ --register-unsafely-without-email \ @@ -37,7 +37,7 @@ letsencrypt_test_no_force_renew () { --http-01-port 5002 \ --manual-test-mode \ $store_flags \ - --text \ + --non-interactive \ --no-redirect \ --agree-tos \ --register-unsafely-without-email \ From 3999d65d1c51b0de8f8b1e7a586583ffed038dc6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 18:06:46 -0800 Subject: [PATCH 26/44] Refactor --csr handling to run early enough for --webroot --- letsencrypt/cli.py | 24 +++++++++++++++++++++++- letsencrypt/client.py | 32 ++------------------------------ letsencrypt/tests/client_test.py | 17 +++++++++++------ 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 99ee7884a..e01275153 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -684,7 +684,7 @@ def obtain_cert(config, plugins, lineage=None): # This is a special case; cert and chain are simply saved if config.csr is not None: assert lineage is None, "Did not expect a CSR with a RenewableCert" - certr, chain = le_client.obtain_certificate_from_csr(_process_domain) + certr, chain = le_client.obtain_certificate_from_csr(config.domains, config.actual_csr) if config.dry_run: logger.info( "Dry run: skipping saving certificate to %s", config.cert_path) @@ -1106,8 +1106,30 @@ class HelpfulArgumentParser(object): "'certonly' or 'renew' subcommands") parsed_args.break_my_certs = parsed_args.staging = True + if parsed_args.csr: + self.handle_csr(parsed_args) + return parsed_args + def handle_csr(self, parsed_args): + """ + Process a --csr flag. This needs to happen early enought that the + webroot plugin can know about the calls to _process_domain + """ + csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der") + # TODO: add CN to domains? + domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) + for d in domains: + _process_domain(parsed_args, d) + parsed_args.actual_csr = csr + csr_domains, config_domains = set(domains), set(parsed_args.domains) + if csr_domains != config_domains: + raise errors.ConfigurationError( + "Inconsistent domain requests:\ncsr: {0}\ncli config: {1}" + .format(", ".join(csr_domains), ", ".join(config_domains)) + ) + + def determine_verb(self): """Determines the verb/subcommand provided by the user. diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 413409ded..fd851c163 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -195,7 +195,7 @@ class Client(object): else: self.auth_handler = None - def _obtain_certificate(self, domains, csr): + def obtain_certificate_from_csr(self, domains, csr): """Obtain certificate. Internal function with precondition that `domains` are @@ -228,34 +228,6 @@ class Client(object): authzr) return certr, self.acme.fetch_chain(certr) - def obtain_certificate_from_csr(self, domain_callback): - """Obtain certficiate from CSR. - - :param function(config, domains) domain_callback: callback for each - domain extracted from the CSR, to ensure that webroot-map and similar - housekeeping in cli.py is performed correctly - - :returns: `.CertificateResource` and certificate chain (as - returned by `.fetch_chain`). - :rtype: tuple - - """ - - #raise TypeError("About to call %r" % le_util.CSR) - csr = le_util.CSR(file=self.config.csr[0], data=self.config.csr[1], form="der") - # TODO: add CN to domains? - domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) - for d in domains: - domain_callback(self.config, d) - - csr_domains, config_domains = set(domains), set(self.config.domains) - if csr_domains != config_domains: - raise errors.ConfigurationError( - "Inconsistent domain requests:\ncsr: {0}\ncli config: {1}" - .format(", ".join(csr_domains), ", ".join(config_domains)) - ) - - return self._obtain_certificate(domains, csr) def obtain_certificate(self, domains): """Obtains a certificate from the ACME server. @@ -276,7 +248,7 @@ class Client(object): self.config.rsa_key_size, self.config.key_dir) csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) - return self._obtain_certificate(domains, csr) + (key, csr) + return self.obtain_certificate_from_csr(domains, csr) + (key, csr) def obtain_and_enroll_certificate(self, domains): """Obtain and enroll certificate. diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 222e9c707..429945526 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -109,21 +109,26 @@ class ClientTest(unittest.TestCase): self.client.auth_handler.get_authorizations()) self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr) - def test_obtain_certificate_from_csr(self): + # FIXME move parts of this to test_cli.py... + @mock.patch("letsencrypt.cli._process_domain") + def test_obtain_certificate_from_csr(self, mock_process_domain): self._mock_obtain_certificate() - mock_process_domain = mock.MagicMock() + from letsencrypt import cli test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + mock_parsed_args = mock.MagicMock() with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR: mock_CSR.return_value = test_csr - self.client.config.domains = self.eg_domains - self.assertEqual( - (mock.sentinel.certr, mock.sentinel.chain), - self.client.obtain_certificate_from_csr(mock_process_domain)) + mock_parsed_args.domains = self.eg_domains + mock_parser = mock.MagicMock(cli.HelpfulArgumentParser) + cli.HelpfulArgumentParser.handle_csr(mock_parser, mock_parsed_args) # make sure cli processing occurred cli_processed = (call[0][1] for call in mock_process_domain.call_args_list) self.assertEqual(set(cli_processed), set(("example.com", "www.example.com"))) + self.assertEqual( + (mock.sentinel.certr, mock.sentinel.chain), + self.client.obtain_certificate_from_csr(self.eg_domains, test_csr)) # and that the cert was obtained correctly self._check_obtain_certificate() From 9af8a875cd6a6511a95d8a3e51885464be3861ad Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 18:12:49 -0800 Subject: [PATCH 27/44] Update hippopotamus test --- letsencrypt/tests/client_test.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 429945526..dbc57565e 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -118,13 +118,17 @@ class ClientTest(unittest.TestCase): mock_parsed_args = mock.MagicMock() with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR: mock_CSR.return_value = test_csr - mock_parsed_args.domains = self.eg_domains + mock_parsed_args.domains = self.eg_domains[:] mock_parser = mock.MagicMock(cli.HelpfulArgumentParser) cli.HelpfulArgumentParser.handle_csr(mock_parser, mock_parsed_args) # make sure cli processing occurred cli_processed = (call[0][1] for call in mock_process_domain.call_args_list) self.assertEqual(set(cli_processed), set(("example.com", "www.example.com"))) + # Now provoke an inconsistent domains error... + mock_parsed_args.domains.append("hippopotamus.io") + self.assertRaises(errors.ConfigurationError, + cli.HelpfulArgumentParser.handle_csr, mock_parser, mock_parsed_args) self.assertEqual( (mock.sentinel.certr, mock.sentinel.chain), @@ -132,11 +136,6 @@ class ClientTest(unittest.TestCase): # and that the cert was obtained correctly self._check_obtain_certificate() - # Now provoke an inconsistent domains error... - - self.client.config.domains.append("hippopotamus.io") - self.assertRaises(errors.ConfigurationError, - self.client.obtain_certificate_from_csr, mock_process_domain) @mock.patch("letsencrypt.client.crypto_util") def test_obtain_certificate(self, mock_crypto_util): From 7a902daa9f8480b527e54f6b18d38be7cd36ce73 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 18:14:29 -0800 Subject: [PATCH 28/44] duplication-- --- tests/boulder-integration.sh | 11 +++++------ tests/integration/_common.sh | 17 ++--------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index dd6c1835e..cfd0e5c16 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -20,17 +20,16 @@ else readlink="readlink" fi -common() { - letsencrypt_test \ +common_no_force_renew() { + letsencrypt_test_no_force_renew \ --authenticator standalone \ --installer null \ "$@" } -common_no_force_renew() { - letsencrypt_test_no_force_renew \ - --authenticator standalone \ - --installer null \ +common() { + common_no_force_renew \ + --renew-by-default \ "$@" } diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index db6e2f0f1..77a60112b 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -12,21 +12,8 @@ store_flags="$store_flags --logs-dir $root/logs" export root store_flags letsencrypt_test () { - letsencrypt \ - --server "${SERVER:-http://localhost:4000/directory}" \ - --no-verify-ssl \ - --tls-sni-01-port 5001 \ - --http-01-port 5002 \ - --manual-test-mode \ - $store_flags \ - --non-interactive \ - --no-redirect \ - --agree-tos \ - --register-unsafely-without-email \ - --renew-by-default \ - --debug \ - -vvvvvvv \ - "$@" + letsencrypt_test_no_force_renew \ + --renew-by-default } letsencrypt_test_no_force_renew () { From a774922f8f5374c0919d2324661083c64739bd21 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 18:14:55 -0800 Subject: [PATCH 29/44] Revert "Revert "Allow webroot-map and --csr to exist together."" This reverts commit d65a3c65c20ca7c12c4f85802a8bc84a14b95611. --- letsencrypt/cli.py | 8 +------- letsencrypt/client.py | 27 ++++++++++++++++++++------- letsencrypt/plugins/webroot.py | 6 ++++-- letsencrypt/tests/cli_test.py | 5 +---- letsencrypt/tests/client_test.py | 24 +++++++++++++++++------- 5 files changed, 43 insertions(+), 27 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index c335d8d5b..de1321ac9 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -672,11 +672,6 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals def obtain_cert(config, plugins, lineage=None): """Implements "certonly": authenticate & obtain cert, but do not install it.""" - if config.domains and config.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" - try: # installers are used in auth mode to determine domain names installer, authenticator = choose_configurator_plugins(config, plugins, "certonly") @@ -689,8 +684,7 @@ def obtain_cert(config, plugins, lineage=None): # This is a special case; cert and chain are simply saved if config.csr is not None: assert lineage is None, "Did not expect a CSR with a RenewableCert" - certr, chain = le_client.obtain_certificate_from_csr(le_util.CSR( - file=config.csr[0], data=config.csr[1], form="der")) + certr, chain = le_client.obtain_certificate_from_csr(_process_domain) if config.dry_run: logger.info( "Dry run: skipping saving certificate to %s", config.cert_path) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 57b21a55f..413409ded 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -228,21 +228,34 @@ class Client(object): authzr) return certr, self.acme.fetch_chain(certr) - def obtain_certificate_from_csr(self, csr): + def obtain_certificate_from_csr(self, domain_callback): """Obtain certficiate from CSR. - :param .le_util.CSR csr: DER-encoded Certificate Signing - Request. + :param function(config, domains) domain_callback: callback for each + domain extracted from the CSR, to ensure that webroot-map and similar + housekeeping in cli.py is performed correctly :returns: `.CertificateResource` and certificate chain (as returned by `.fetch_chain`). :rtype: tuple """ - return self._obtain_certificate( - # TODO: add CN to domains? - crypto_util.get_sans_from_csr( - csr.data, OpenSSL.crypto.FILETYPE_ASN1), csr) + + #raise TypeError("About to call %r" % le_util.CSR) + csr = le_util.CSR(file=self.config.csr[0], data=self.config.csr[1], form="der") + # TODO: add CN to domains? + domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) + for d in domains: + domain_callback(self.config, d) + + csr_domains, config_domains = set(domains), set(self.config.domains) + if csr_domains != config_domains: + raise errors.ConfigurationError( + "Inconsistent domain requests:\ncsr: {0}\ncli config: {1}" + .format(", ".join(csr_domains), ", ".join(config_domains)) + ) + + return self._obtain_certificate(domains, csr) def obtain_certificate(self, domains): """Obtains a certificate from the ACME server. diff --git a/letsencrypt/plugins/webroot.py b/letsencrypt/plugins/webroot.py index f8176417c..3f5bc6d28 100644 --- a/letsencrypt/plugins/webroot.py +++ b/letsencrypt/plugins/webroot.py @@ -49,8 +49,10 @@ to serve all files under specified web root ({0}).""" path_map = self.conf("map") if not path_map: - raise errors.PluginError("--{0} must be set".format( - self.option_name("path"))) + raise errors.PluginError( + "Missing parts of webroot configuration; please set either " + "--webroot-path and --domains, or --webroot-map. Run with " + " --help webroot for examples.") for name, path in path_map.items(): if not os.path.isdir(path): raise errors.PluginError(path + " does not exist or is not a directory") diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index c41f45116..2d36a9d21 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -228,7 +228,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods args = ["certonly", "--webroot"] ret, _, _, _ = self._call(args) - self.assertTrue("--webroot-path must be set" in ret) + self.assertTrue("please set either --webroot-path" in ret) self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably") @@ -323,9 +323,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertEqual(config.fullchain_path, os.path.abspath(fullchain)) def test_certonly_bad_args(self): - ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) - self.assertEqual(ret, '--domains and --csr are mutually exclusive') - ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly']) self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 2f117f80c..6a8899c3b 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -82,6 +82,7 @@ class ClientTest(unittest.TestCase): no_verify_ssl=False, config_dir="/etc/letsencrypt") # pylint: disable=star-args self.account = mock.MagicMock(**{"key.pem": KEY}) + self.eg_domains = ["example.com", "www.example.com"] from letsencrypt.client import Client with mock.patch("letsencrypt.client.acme_client.Client") as acme: @@ -101,8 +102,7 @@ class ClientTest(unittest.TestCase): self.acme.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.client.auth_handler.get_authorizations.assert_called_once_with(self.eg_domains) self.acme.request_issuance.assert_called_once_with( jose.ComparableX509(OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)), @@ -111,11 +111,21 @@ class ClientTest(unittest.TestCase): 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_process_domain = mock.MagicMock() + test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR: + mock_CSR.return_value = test_csr + self.client.config.domains = self.eg_domains + self.assertEqual( + (mock.sentinel.certr, mock.sentinel.chain), + self.client.obtain_certificate_from_csr(mock_process_domain)) + + # make sure cli processing occurred + cli_processed = (call[0][1] for call in mock_process_domain.call_args_list) + self.assertEqual(set(cli_processed), set(("example.com", "www.example.com"))) + + # and that the cert was obtained correctly + self._check_obtain_certificate() @mock.patch("letsencrypt.client.crypto_util") def test_obtain_certificate(self, mock_crypto_util): From e798b62d2e47f8ce669da6dfaad813fadd1f442f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 18:18:48 -0800 Subject: [PATCH 30/44] Testing cleanup --- tests/boulder-integration.sh | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index cfd0e5c16..7e0246085 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -60,36 +60,18 @@ CheckCertCount() { CheckCertCount 1 # This won't renew (because it's not time yet) -letsencrypt_test_no_force_renew --authenticator standalone --installer null renew -tvv +letsencrypt_test_no_force_renew renew CheckCertCount 1 +# --renew-by-default is used, so renewal should occur +letsencrypt_test renew +CheckCertCount 2 + # This will renew because the expiry is less than 10 years from now sed -i "4arenew_before_expiry = 10 years" "$root/conf/renewal/le.wtf.conf" -letsencrypt_test_no_force_renew --authenticator standalone --installer null renew # --renew-by-default -CheckCertCount 2 - -# Check Param setting in renewal... -letsencrypt_test_no_force_renew --authenticator standalone --installer null renew --renew-by-default +letsencrypt_test_no_force_renew CheckCertCount 3 -# The 4096 bit setting should persist to the first renewal, but be overriden in the second -size2=`wc -c ${root}/conf/archive/le.wtf/privkey2.pem | cut -d" " -f1` -size3=`wc -c ${root}/conf/archive/le.wtf/privkey3.pem | cut -d" " -f1` -#if ! [ "$size3" -lt "$size2" ] ; then -# echo "key size failure:" -# ls -l ${root}/conf/archive/le.wtf/ -# exit 1 -#fi - - -# dir="$root/conf/archive/le1.wtf" -# for x in cert chain fullchain privkey; -# do -# latest="$(ls -1t $dir/ | grep -e "^${x}" | head -n1)" -# live="$($readlink -f "$root/conf/live/le1.wtf/${x}.pem")" -# [ "${dir}/${latest}" = "$live" ] # renewer fails this test -# done - # revoke by account key common revoke --cert-path "$root/conf/live/le.wtf/cert.pem" # revoke renewed From 2170c8d7d2830b0d883d605f17f3831d38ade4a7 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 18:35:44 -0800 Subject: [PATCH 31/44] Move * outside of " --- tests/boulder-integration.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 7e0246085..5520a75f1 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -51,7 +51,7 @@ common --domains le3.wtf install \ --key-path "${root}/csr/key.pem" CheckCertCount() { - CERTCOUNT=`ls "${root}/conf/archive/le.wtf/cert*" | wc -l` + CERTCOUNT=`ls "${root}/conf/archive/le.wtf/cert"* | wc -l` if [ "$CERTCOUNT" -ne "$1" ] ; then echo Wrong cert count, not "$1" `ls "${root}/conf/archive/le.wtf/"*` exit 1 From 8b613eed8f3a9bb0313cafe887c9cc764355dd8e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 18:39:59 -0800 Subject: [PATCH 32/44] Pass additional args to letsencrypt_test_no_force_renew --- tests/integration/_common.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 77a60112b..e86d087cb 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -13,7 +13,8 @@ export root store_flags letsencrypt_test () { letsencrypt_test_no_force_renew \ - --renew-by-default + --renew-by-default \ + "$@" } letsencrypt_test_no_force_renew () { From 0fa61f4192131892a9e2949d6c77a6cd38e297a0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Feb 2016 18:46:24 -0800 Subject: [PATCH 33/44] Use common and add verb --- tests/boulder-integration.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 5520a75f1..29618b97f 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -60,16 +60,16 @@ CheckCertCount() { CheckCertCount 1 # This won't renew (because it's not time yet) -letsencrypt_test_no_force_renew renew +common_no_force_renew renew CheckCertCount 1 # --renew-by-default is used, so renewal should occur -letsencrypt_test renew +common renew CheckCertCount 2 # This will renew because the expiry is less than 10 years from now sed -i "4arenew_before_expiry = 10 years" "$root/conf/renewal/le.wtf.conf" -letsencrypt_test_no_force_renew +common_no_force_renew renew CheckCertCount 3 # revoke by account key From a8ba6f7c2c25d279a98c08534da1fc20c051fec7 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Feb 2016 18:54:36 -0800 Subject: [PATCH 34/44] Dry run messages --- letsencrypt/cli.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 96b30d25e..5ebf1ff25 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -419,7 +419,8 @@ def _suggest_donation_if_appropriate(config): def _report_successful_dry_run(): reporter_util = zope.component.getUtility(interfaces.IReporter) - reporter_util.add_message("The dry run was successful.", + reporter_util.add_message("A test certificate requested in dry run was " + "successfully issued.", reporter_util.HIGH_PRIORITY, on_crash=False) @@ -979,8 +980,14 @@ def renew(config, unused_plugins): renew_failures.append(renewal_candidate.fullchain) # Describe all the results + if config.dry_run: + print("** DRY RUN (messages below refer to test certs only!") + print("** The certificates mentioned have not been saved.") _renew_describe_results(renew_successes, renew_failures, renew_skipped, parse_failures) + if config.dry_run: + print("** DRY RUN (messages above refer to test certs only!") + print("** The certificates mentioned have not been saved.") def revoke(config, unused_plugins): # TODO: coop with renewal config From 9bc5523a3b44302c660cc2ddeeedf7138ebbc214 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Feb 2016 19:06:16 -0800 Subject: [PATCH 35/44] Reorganize to make pylint happier --- letsencrypt/cli.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 5ebf1ff25..5e6e1fade 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -884,9 +884,12 @@ def _renewal_conf_files(config): return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) -def _renew_describe_results(renew_successes, renew_failures, renew_skipped, - parse_failures): +def _renew_describe_results(config, renew_successes, renew_failures, + renew_skipped, parse_failures): status = lambda x, msg: " " + "\n ".join(i + " (" + msg +")" for i in x) + if config.dry_run: + print("** DRY RUN (messages below refer to test certs only!") + print("** The certificates mentioned have not been saved.") print() if renew_skipped: print("The following certs are not due for renewal yet:") @@ -912,6 +915,10 @@ def _renew_describe_results(renew_successes, renew_failures, renew_skipped, "were invalid: ") print(status(parse_failures, "parsefail")) + if config.dry_run: + print("** DRY RUN (messages above refer to test certs only!") + print("** The certificates mentioned have not been saved.") + def renew(config, unused_plugins): """Renew previously-obtained certificates.""" @@ -980,14 +987,8 @@ def renew(config, unused_plugins): renew_failures.append(renewal_candidate.fullchain) # Describe all the results - if config.dry_run: - print("** DRY RUN (messages below refer to test certs only!") - print("** The certificates mentioned have not been saved.") - _renew_describe_results(renew_successes, renew_failures, renew_skipped, - parse_failures) - if config.dry_run: - print("** DRY RUN (messages above refer to test certs only!") - print("** The certificates mentioned have not been saved.") + _renew_describe_results(config, renew_successes, renew_failures, + renew_skipped, parse_failures) def revoke(config, unused_plugins): # TODO: coop with renewal config From 63c0718d869a95eadc2e05a38d5df375d71ee07c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 19:15:28 -0800 Subject: [PATCH 36/44] Accept --csr PEMFILE * Closes: #1082 #1935 * Also produce better errors if SANs are missing, though not yet fixing #1076 --- letsencrypt/cli.py | 28 +++++++++++++++++++++++----- letsencrypt/client.py | 7 ++++--- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index c9c58ea3b..d9497d8fe 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -684,7 +684,8 @@ def obtain_cert(config, plugins, lineage=None): # This is a special case; cert and chain are simply saved if config.csr is not None: assert lineage is None, "Did not expect a CSR with a RenewableCert" - certr, chain = le_client.obtain_certificate_from_csr(config.domains, config.actual_csr) + csr, typ = config.actual_csr + certr, chain = le_client.obtain_certificate_from_csr(config.domains, csr, typ) if config.dry_run: logger.info( "Dry run: skipping saving certificate to %s", config.cert_path) @@ -1116,12 +1117,29 @@ class HelpfulArgumentParser(object): Process a --csr flag. This needs to happen early enought that the webroot plugin can know about the calls to _process_domain """ - csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der") - # TODO: add CN to domains? - domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) + try: + csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der") + typ = OpenSSL.crypto.FILETYPE_ASN1 + domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) + except: + try: + e1 = traceback.format_exc() + typ = OpenSSL.crypto.FILETYPE_PEM + csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="pem") + domains = crypto_util.get_sans_from_csr(csr.data, typ) + except: + logger.debug("DER CSR parse error %s", e1) + logger.debug("PEM CSR parse error %s", traceback.format_exc()) + raise errors.Error("Failed to CSR file: %s", parsed_args.csr[0]) + + if not domains: + # TODO: add CN to domains instead: + raise errors.Error( + "Unfortunately, your CSR %s needs to have a SubjectAltName for every domain" + % parsed_args.csr[0]) for d in domains: _process_domain(parsed_args, d) - parsed_args.actual_csr = csr + parsed_args.actual_csr = (csr, typ) csr_domains, config_domains = set(domains), set(parsed_args.domains) if csr_domains != config_domains: raise errors.ConfigurationError( diff --git a/letsencrypt/client.py b/letsencrypt/client.py index fd851c163..9dfa70e8d 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -195,7 +195,8 @@ class Client(object): else: self.auth_handler = None - def obtain_certificate_from_csr(self, domains, csr): + def obtain_certificate_from_csr(self, domains, csr, + typ=OpenSSL.crypto.FILETYPE_ASN1): """Obtain certificate. Internal function with precondition that `domains` are @@ -223,8 +224,8 @@ class Client(object): authzr = self.auth_handler.get_authorizations(domains) certr = self.acme.request_issuance( - jose.ComparableX509(OpenSSL.crypto.load_certificate_request( - OpenSSL.crypto.FILETYPE_ASN1, csr.data)), + jose.ComparableX509( + OpenSSL.crypto.load_certificate_request(typ, csr.data)), authzr) return certr, self.acme.fetch_chain(certr) From b2e460f34bb5b29a545c6a4aa61d9cb56f9b6977 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 19:54:32 -0800 Subject: [PATCH 37/44] Address the comments of reviewers & lintmonsters --- letsencrypt/cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index d9497d8fe..375495833 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1114,23 +1114,23 @@ class HelpfulArgumentParser(object): def handle_csr(self, parsed_args): """ - Process a --csr flag. This needs to happen early enought that the + Process a --csr flag. This needs to happen early enough that the webroot plugin can know about the calls to _process_domain """ try: csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der") typ = OpenSSL.crypto.FILETYPE_ASN1 domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) - except: + except OpenSSL.crypto.Error: try: e1 = traceback.format_exc() typ = OpenSSL.crypto.FILETYPE_PEM csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="pem") domains = crypto_util.get_sans_from_csr(csr.data, typ) - except: + except OpenSSL.crypto.Error: logger.debug("DER CSR parse error %s", e1) logger.debug("PEM CSR parse error %s", traceback.format_exc()) - raise errors.Error("Failed to CSR file: %s", parsed_args.csr[0]) + raise errors.Error("Failed to parse CSR file: {0}".format(parsed_args.csr[0])) if not domains: # TODO: add CN to domains instead: @@ -1143,7 +1143,7 @@ class HelpfulArgumentParser(object): csr_domains, config_domains = set(domains), set(parsed_args.domains) if csr_domains != config_domains: raise errors.ConfigurationError( - "Inconsistent domain requests:\ncsr: {0}\ncli config: {1}" + "Inconsistent domain requests:\nFrom the CSR: {0}\nFrom command line/config: {1}" .format(", ".join(csr_domains), ", ".join(config_domains)) ) From ff9d7a7b802f1e272ad379bdfec9010b9f701246 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 20:21:08 -0800 Subject: [PATCH 38/44] Restore old versions of some tests, port others --- letsencrypt/tests/cli_test.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index ff11b1dde..de64fce04 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -222,16 +222,16 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods if "nginx" in real_plugins: # Sending nginx a non-existent conf dir will simulate misconfiguration # (we can only do that if letsencrypt-nginx is actually present) - self._call(args) - # XXX: This probably now raises an exception (when nginx is - # present, but I don't know which one!) - # self.assertTrue("The nginx plugin is not working" in ret) - # self.assertTrue("MisconfigurationError" in ret) + ret, _, _, _ = self._call(args) + self.assertTrue("The nginx plugin is not working" in ret) + self.assertTrue("MisconfigurationError" in ret) args = ["certonly", "--webroot"] - # ret, _, _, _ = self._call(args) - self.assertRaises(errors.PluginSelectionError, self._call, args) - # self.assertTrue("--webroot-path must be set" in ret) + try: + self._call(args) + assert False, "Exception should have been raised" + except errors.PluginSelectionError as e: + self.assertTrue("--webroot-path must be set" in e.message) self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably") From b2de2cd1816d1d1646742822eb55925d220fa3e9 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 20:21:40 -0800 Subject: [PATCH 39/44] Better dry run reporting --- letsencrypt/cli.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 5e6e1fade..414c3b0ce 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -417,11 +417,12 @@ def _suggest_donation_if_appropriate(config): reporter_util.add_message(msg, reporter_util.LOW_PRIORITY) -def _report_successful_dry_run(): + +def _report_successful_dry_run(config): reporter_util = zope.component.getUtility(interfaces.IReporter) - reporter_util.add_message("A test certificate requested in dry run was " - "successfully issued.", - reporter_util.HIGH_PRIORITY, on_crash=False) + if config.verb != "renew": + reporter_util.add_message("The dry run was successful.", + reporter_util.HIGH_PRIORITY, on_crash=False) def _auth_from_domains(le_client, config, domains, lineage=None): @@ -709,7 +710,7 @@ def obtain_cert(config, plugins, lineage=None): _auth_from_domains(le_client, config, domains, lineage) if config.dry_run: - _report_successful_dry_run() + _report_successful_dry_run(config) elif config.verb == "renew": if installer is None: # Tell the user that the server was not restarted. From 28c54476533fad62c7bc5d90d123fbb74d90a2be Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 20:30:48 -0800 Subject: [PATCH 40/44] Port another test --- letsencrypt/cli.py | 3 +-- letsencrypt/tests/cli_test.py | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 414c3b0ce..064de1b3d 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -686,8 +686,7 @@ def obtain_cert(config, plugins, lineage=None): # installers are used in auth mode to determine domain names installer, authenticator = choose_configurator_plugins(config, plugins, "certonly") except errors.PluginSelectionError as e: - logger.info( - "Could not choose appropriate plugin: %s", e) + logger.info("Could not choose appropriate plugin: %s", e) raise # TODO: Handle errors from _init_le_client? diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index de64fce04..07029ca66 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -329,10 +329,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) self.assertEqual(ret, '--domains and --csr are mutually exclusive') - # ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly']) - self.assertRaises(errors.PluginSelectionError, self._call, - ['-a', 'bad_auth', 'certonly']) - # self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') + try: + self._call(['-a', 'bad_auth', 'certonly']) + assert False, "Exception should have been raised" + except errors.PluginSelectionError as e: + self.assertTrue('The requested bad_auth plugin does not appear' in e.message) def test_check_config_sanity_domain(self): # Punycode From 57ee4f0b4684364f6f9cf62fc8a1589fe5bcd579 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 20:43:13 -0800 Subject: [PATCH 41/44] Nicen dry run renewal messages --- letsencrypt/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 064de1b3d..758c3e7f2 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -888,8 +888,8 @@ def _renew_describe_results(config, renew_successes, renew_failures, renew_skipped, parse_failures): status = lambda x, msg: " " + "\n ".join(i + " (" + msg +")" for i in x) if config.dry_run: - print("** DRY RUN (messages below refer to test certs only!") - print("** The certificates mentioned have not been saved.") + print("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") + print("** (The test certificates below have not been saved.)") print() if renew_skipped: print("The following certs are not due for renewal yet:") @@ -916,8 +916,8 @@ def _renew_describe_results(config, renew_successes, renew_failures, print(status(parse_failures, "parsefail")) if config.dry_run: - print("** DRY RUN (messages above refer to test certs only!") - print("** The certificates mentioned have not been saved.") + print("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") + print("** (The test certificates above have not been saved.)") def renew(config, unused_plugins): From e0cfd9f691fb9efd6197d41ecacfac4b1cf443be Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 21:10:34 -0800 Subject: [PATCH 42/44] Extra CSR sanity checking --- letsencrypt/cli.py | 15 +++++++++++---- letsencrypt/le_util.py | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 375495833..ac6e2c937 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1132,20 +1132,27 @@ class HelpfulArgumentParser(object): logger.debug("PEM CSR parse error %s", traceback.format_exc()) raise errors.Error("Failed to parse CSR file: {0}".format(parsed_args.csr[0])) + for d in domains: + _process_domain(parsed_args, d) + + for d in domains: + sanitised = le_util.enforce_domain_sanity(d): + if d.lower() != sanitised: + raise errors.ConfigurationError( + "CSR domain {0} needs to be sanitised to {1}.".format(d, sanitised)) + if not domains: # TODO: add CN to domains instead: raise errors.Error( "Unfortunately, your CSR %s needs to have a SubjectAltName for every domain" % parsed_args.csr[0]) - for d in domains: - _process_domain(parsed_args, d) + parsed_args.actual_csr = (csr, typ) csr_domains, config_domains = set(domains), set(parsed_args.domains) if csr_domains != config_domains: raise errors.ConfigurationError( "Inconsistent domain requests:\nFrom the CSR: {0}\nFrom command line/config: {1}" - .format(", ".join(csr_domains), ", ".join(config_domains)) - ) + .format(", ".join(csr_domains), ", ".join(config_domains))) def determine_verb(self): diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 35793849e..527c9bdae 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -308,7 +308,7 @@ def enforce_domain_sanity(domain): # Unicode try: - domain = domain.encode('ascii') + domain = domain.encode('ascii').lower() except UnicodeDecodeError: raise errors.ConfigurationError( "Internationalized domain names are not presently supported: {0}" From 4000aa762ef3235f576b013725d95e402ef8ed48 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 21:14:45 -0800 Subject: [PATCH 43/44] Fix snauf --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 7791d2819..533539684 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1202,7 +1202,7 @@ class HelpfulArgumentParser(object): _process_domain(parsed_args, d) for d in domains: - sanitised = le_util.enforce_domain_sanity(d): + sanitised = le_util.enforce_domain_sanity(d) if d.lower() != sanitised: raise errors.ConfigurationError( "CSR domain {0} needs to be sanitised to {1}.".format(d, sanitised)) From d6703f771ac158941360cce74642cc27b2e717e7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Feb 2016 21:28:43 -0800 Subject: [PATCH 44/44] Lint & cleanup weirdness from #2392... --- letsencrypt/cli.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 533539684..00d45b700 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -943,7 +943,6 @@ def renew(config, unused_plugins): try: renewal_candidate = _reconstitute(lineage_config, renewal_file) except Exception as e: # pylint: disable=broad-except - # reconstitute encountered an unanticipated problem. logger.warning("Renewal configuration file %s produced an " "unexpected error: %s. Skipping.", renewal_file, e) logger.debug("Traceback was:\n%s", traceback.format_exc()) @@ -954,24 +953,12 @@ def renew(config, unused_plugins): if renewal_candidate is None: parse_failures.append(renewal_file) else: - # _reconstitute succeeded in producing a RenewableCert, so we - # have something to work with from this particular config file. - # XXX: ensure that each call here replaces the previous one zope.component.provideUtility(lineage_config) - # Although obtain_cert itself also indirectly decides - # whether to renew or not, we need to check at this - # stage in order to avoid claiming that renewal - # succeeded when it wasn't even attempted (since - # obtain_cert wouldn't raise an error in that case). if _should_renew(lineage_config, renewal_candidate): - err = obtain_cert(lineage_config, - plugins_disco.PluginsRegistry.find_all(), - renewal_candidate) - if err is None: - renew_successes.append(renewal_candidate.fullchain) - else: - renew_failures.append(renewal_candidate.fullchain) + plugins = plugins_disco.PluginsRegistry.find_all() + obtain_cert(lineage_config, plugins, renewal_candidate) + renew_successes.append(renewal_candidate.fullchain) else: renew_skipped.append(renewal_candidate.fullchain) except Exception as e: # pylint: disable=broad-except @@ -1197,7 +1184,6 @@ class HelpfulArgumentParser(object): logger.debug("DER CSR parse error %s", e1) logger.debug("PEM CSR parse error %s", traceback.format_exc()) raise errors.Error("Failed to parse CSR file: {0}".format(parsed_args.csr[0])) - for d in domains: _process_domain(parsed_args, d)