diff --git a/README.rst b/README.rst index 874b0c450..050cde82b 100644 --- a/README.rst +++ b/README.rst @@ -18,16 +18,17 @@ The Let's Encrypt Client is a fully-featured, extensible client for the Let's Encrypt CA (or any other CA that speaks the `ACME `_ protocol) that can automate the tasks of obtaining certificates and -configuring webservers to use them. +configuring webservers to use them. This client runs on Unix-based operating +systems. Installation ------------ -If ``letsencrypt`` is packaged for your OS, you can install it from there, and -run it by typing ``letsencrypt``. Because not all operating systems have -packages yet, we provide a temporary solution via the ``letsencrypt-auto`` -wrapper script, which obtains some dependencies from your OS and puts others -in a python virtual environment:: +If ``letsencrypt`` is packaged for your Unix OS, you can install it from +there, and run it by typing ``letsencrypt``. Because not all operating +systems have packages yet, we provide a temporary solution via the +``letsencrypt-auto`` wrapper script, which obtains some dependencies +from your OS and puts others in a python virtual environment:: user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt user@webserver:~$ cd letsencrypt diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 285a1f3b7..658315597 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -52,9 +52,8 @@ class AuthHandler(object): :param bool best_effort: Whether or not all authorizations are required (this is useful in renewal) - :returns: tuple of lists of authorization resources. Takes the - form of (`completed`, `failed`) - :rtype: tuple + :returns: List of authorization resources + :rtype: list :raises .AuthorizationError: If unable to retrieve all authorizations @@ -76,9 +75,16 @@ class AuthHandler(object): # Just make sure all decisions are complete. self.verify_authzr_complete() + # Only return valid authorizations - return [authzr for authzr in self.authzr.values() - if authzr.body.status == messages.STATUS_VALID] + retVal = [authzr for authzr in self.authzr.values() + if authzr.body.status == messages.STATUS_VALID] + + if not retVal: + raise errors.AuthorizationError( + "Challenges failed for all domains") + + return retVal def _choose_challenges(self, domains): """Retrieve necessary challenges to satisfy server.""" @@ -175,9 +181,11 @@ class AuthHandler(object): chall_update[domain].remove(achall) # We failed some challenges... damage control else: - # Right now... just assume a loss and carry on... if best_effort: comp_domains.add(domain) + logger.warning( + "Challenge failed for domain %s", + domain) else: all_failed_achalls.update( updated for _, updated in failed_achalls) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 12c30819b..fccebe02f 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -714,6 +714,9 @@ class HelpfulArgumentParser(object): parsed_args.register_unsafely_without_email = True if parsed_args.csr: + if parsed_args.allow_subset_of_names: + raise errors.Error("--allow-subset-of-names " + "cannot be used with --csr") self.handle_csr(parsed_args) if self.detect_defaults: # plumbing @@ -1090,6 +1093,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "security", "--strict-permissions", action="store_true", help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") + helpful.add( + "automation", "--allow-subset-of-names", + action="store_true", + help="When performing domain validation, do not consider it a failure " + "if authorizations can not be obtained for a strict subset of " + "the requested domains. This option cannot be used with --csr.") helpful.add_group( "renew", description="The 'renew' subcommand will attempt to renew all" diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 3d611f856..42f087739 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -188,7 +188,7 @@ class Client(object): self.auth_handler = None def obtain_certificate_from_csr(self, domains, csr, - typ=OpenSSL.crypto.FILETYPE_ASN1): + typ=OpenSSL.crypto.FILETYPE_ASN1, authzr=None): """Obtain certificate. Internal function with precondition that `domains` are @@ -198,6 +198,8 @@ class Client(object): :param .le_util.CSR csr: DER-encoded Certificate Signing Request. The key used to generate this CSR can be different than `authkey`. + :param list authzr: List of + :class:`acme.messages.AuthorizationResource` :returns: `.CertificateResource` and certificate chain (as returned by `.fetch_chain`). @@ -214,14 +216,15 @@ class Client(object): logger.debug("CSR: %s, domains: %s", csr, domains) - authzr = self.auth_handler.get_authorizations(domains) + if authzr is None: + authzr = self.auth_handler.get_authorizations(domains) + certr = self.acme.request_issuance( jose.ComparableX509( OpenSSL.crypto.load_certificate_request(typ, csr.data)), - authzr) + authzr) return certr, self.acme.fetch_chain(certr) - def obtain_certificate(self, domains): """Obtains a certificate from the ACME server. @@ -236,12 +239,20 @@ class Client(object): :rtype: tuple """ + authzr = self.auth_handler.get_authorizations( + domains, + self.config.allow_subset_of_names) + + domains = [a.body.identifier.value.encode('ascii') + for a in authzr] + # Create CSR from names key = crypto_util.init_save_key( self.config.rsa_key_size, self.config.key_dir) csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) - return self.obtain_certificate_from_csr(domains, csr) + (key, csr) + return (self.obtain_certificate_from_csr(domains, csr, authzr=authzr) + + (key, csr)) def obtain_and_enroll_certificate(self, domains): """Obtain and enroll certificate. diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 426a269ac..b7ac04984 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -126,6 +126,7 @@ class GetAuthorizationsTest(unittest.TestCase): for achall in self.mock_auth.cleanup.call_args[0][0]: self.assertTrue(achall.typ in ["tls-sni-01", "http-01", "dns"]) + # Length of authorizations list self.assertEqual(len(authzr), 1) @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") @@ -162,6 +163,9 @@ class GetAuthorizationsTest(unittest.TestCase): self.assertRaises( errors.AuthorizationError, self.handler.get_authorizations, ["0"]) + def test_no_domains(self): + self.assertRaises(errors.AuthorizationError, self.handler.get_authorizations, []) + def _validate_all(self, unused_1, unused_2): for dom in self.handler.authzr.keys(): azr = self.handler.authzr[dom] diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 13a8a0dac..920bc2d99 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -364,6 +364,10 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._call, ['-d', '204.11.231.35']) + def test_csr_with_besteffort(self): + args = ["--csr", CSR, "--allow-subset-of-names"] + self.assertRaises(errors.Error, self._call, args) + def test_run_with_csr(self): # This is an error because you can only use --csr with certonly try: diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 5ecebe30a..16a744b9a 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -96,7 +96,7 @@ class ClientTest(unittest.TestCase): def setUp(self): self.config = mock.MagicMock( - no_verify_ssl=False, config_dir="/etc/letsencrypt") + no_verify_ssl=False, config_dir="/etc/letsencrypt", allow_subset_of_names=False) # pylint: disable=star-args self.account = mock.MagicMock(**{"key.pem": KEY}) self.eg_domains = ["example.com", "www.example.com"] @@ -115,15 +115,22 @@ class ClientTest(unittest.TestCase): def _mock_obtain_certificate(self): self.client.auth_handler = mock.MagicMock() + self.client.auth_handler.get_authorizations.return_value = [None] self.acme.request_issuance.return_value = mock.sentinel.certr 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( + self.eg_domains, + self.config.allow_subset_of_names) + + authzr = self.client.auth_handler.get_authorizations() + self.acme.request_issuance.assert_called_once_with( jose.ComparableX509(OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)), - self.client.auth_handler.get_authorizations()) + authzr) + self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr) # FIXME move parts of this to test_cli.py... @@ -151,12 +158,28 @@ class ClientTest(unittest.TestCase): self.assertRaises(errors.ConfigurationError, cli.HelpfulArgumentParser.handle_csr, mock_parser, mock_parsed_args) + authzr = self.client.auth_handler.get_authorizations(self.eg_domains, False) + self.assertEqual( (mock.sentinel.certr, mock.sentinel.chain), - self.client.obtain_certificate_from_csr(self.eg_domains, test_csr)) + self.client.obtain_certificate_from_csr( + self.eg_domains, + test_csr, + authzr=authzr)) # and that the cert was obtained correctly self._check_obtain_certificate() + # Test for authzr=None + self.assertEqual( + (mock.sentinel.certr, mock.sentinel.chain), + self.client.obtain_certificate_from_csr( + self.eg_domains, + test_csr, + authzr=None)) + + self.client.auth_handler.get_authorizations.assert_called_with( + self.eg_domains) + # Test for no auth_handler self.client.auth_handler = None self.assertRaises( @@ -175,6 +198,21 @@ class ClientTest(unittest.TestCase): mock_crypto_util.init_save_key.return_value = mock.sentinel.key domains = ["example.com", "www.example.com"] + # return_value is essentially set to (None, None) in + # _mock_obtain_certificate(), which breaks this test. + # Thus fixed by the next line. + + authzr = [] + + for domain in domains: + authzr.append( + mock.MagicMock( + body=mock.MagicMock( + identifier=mock.MagicMock( + value=domain)))) + + self.client.auth_handler.get_authorizations.return_value = authzr + self.assertEqual( self.client.obtain_certificate(domains), (mock.sentinel.certr, mock.sentinel.chain, mock.sentinel.key, csr))