diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 6242c376c..4ebd37bf9 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -234,8 +234,8 @@ class DNS01Response(KeyAuthorizationChallengeResponse): try: from acme import dns_resolver except ImportError: # pragma: no cover - raise errors.Error("Local validation for 'dns-01' challenges " - "requires 'dnspython'") + raise errors.DependencyError("Local validation for 'dns-01' " + "challenges requires 'dnspython'") txt_records = dns_resolver.txt_records_for_name(validation_domain_name) exists = validation in txt_records if not exists: diff --git a/acme/acme/errors.py b/acme/acme/errors.py index 70894a808..7446b60fc 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -6,6 +6,10 @@ class Error(Exception): """Generic ACME error.""" +class DependencyError(Error): + """Dependency error""" + + class SchemaValidationError(jose_errors.DeserializationError): """JSON schema ACME object validation error.""" diff --git a/certbot/auth_handler.py b/certbot/auth_handler.py index a94734572..cc8beb463 100644 --- a/certbot/auth_handler.py +++ b/certbot/auth_handler.py @@ -33,14 +33,18 @@ class AuthHandler(object): and values are :class:`acme.messages.AuthorizationResource` :ivar list achalls: DV challenges in the form of :class:`certbot.achallenges.AnnotatedChallenge` + :ivar list pref_challs: sorted user specified preferred challenges + in the form of subclasses of :class:`acme.challenges.Challenge` + with the most preferred challenge listed first """ - def __init__(self, auth, acme, account): + def __init__(self, auth, acme, account, pref_challs): self.auth = auth self.acme = acme self.account = account self.authzr = dict() + self.pref_challs = pref_challs # List must be used to keep responses straight. self.achalls = [] @@ -244,9 +248,18 @@ class AuthHandler(object): :param str domain: domain for which you are requesting preferences """ - # Make sure to make a copy... chall_prefs = [] - chall_prefs.extend(self.auth.get_chall_pref(domain)) + # Make sure to make a copy... + plugin_pref = self.auth.get_chall_pref(domain) + if self.pref_challs: + chall_prefs.extend(pref for pref in self.pref_challs + if pref in plugin_pref) + if chall_prefs: + return chall_prefs + raise errors.AuthorizationError( + "None of the preferred challenges " + "are supported by the selected plugin") + chall_prefs.extend(plugin_pref) return chall_prefs def _cleanup_challenges(self, achall_list=None): diff --git a/certbot/cli.py b/certbot/cli.py index 46ff74cd0..83697d8da 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -11,6 +11,8 @@ import sys import configargparse import six +from acme import challenges + import certbot from certbot import constants @@ -788,11 +790,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) helpful.add( - "testing", "--tls-sni-01-port", type=int, + ["certonly", "renew", "run"], "--tls-sni-01-port", type=int, default=flag_default("tls_sni_01_port"), help=config_help("tls_sni_01_port")) helpful.add( - "testing", "--http-01-port", type=int, dest="http01_port", + ["certonly", "renew", "run", "manual"], "--http-01-port", type=int, + dest="http01_port", default=flag_default("http01_port"), help=config_help("http01_port")) helpful.add( "testing", "--break-my-certs", action="store_true", @@ -844,6 +847,18 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "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( + ["manual", "standalone", "certonly", "renew", "run"], + "--preferred-challenges", dest="pref_challs", + action=_PrefChallAction, default=[], + help='A sorted, comma delimited list of the preferred challenge to ' + 'use during authorization with the most preferred challenge ' + 'listed first (Eg, "dns" or "tls-sni-01,http,dns"). ' + 'Not all plugins support all challenges. See ' + 'https://certbot.eff.org/docs/using.html#plugins for details. ' + 'ACME Challenges are versioned, but if you pick "http" rather ' + 'than "http-01", Certbot will select the latest version ' + 'automatically.') helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates." @@ -1032,3 +1047,18 @@ def add_domains(args_or_config, domains): args_or_config.domains.append(domain) return validated_domains + +class _PrefChallAction(argparse.Action): + """Action class for parsing preferred challenges.""" + + def __call__(self, parser, namespace, pref_challs, option_string=None): + aliases = {"dns": "dns-01", "http": "http-01", "tls-sni": "tls-sni-01"} + challs = [c.strip() for c in pref_challs.split(",")] + challs = [aliases[c] if c in aliases else c for c in challs] + unrecognized = ", ".join(name for name in challs + if name not in challenges.Challenge.TYPES) + if unrecognized: + raise argparse.ArgumentTypeError( + "Unrecognized challenges: {0}".format(unrecognized)) + namespace.pref_challs.extend(challenges.Challenge.TYPES[name] + for name in challs) diff --git a/certbot/client.py b/certbot/client.py index a4f2248a6..0c5d5ec59 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -192,7 +192,7 @@ class Client(object): if auth is not None: self.auth_handler = auth_handler.AuthHandler( - auth, self.acme, self.account) + auth, self.acme, self.account, self.config.pref_challs) else: self.auth_handler = None diff --git a/certbot/interfaces.py b/certbot/interfaces.py index d4b391378..42a952f10 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -229,11 +229,14 @@ class IConfig(zope.interface.Interface): no_verify_ssl = zope.interface.Attribute( "Disable verification of the ACME server's certificate.") tls_sni_01_port = zope.interface.Attribute( - "Port number to perform tls-sni-01 challenge. " - "Boulder in testing mode defaults to 5001.") + "Port used during tls-sni-01 challenge. " + "This only affects the port Certbot listens on. " + "A conforming ACME server will still attempt to connect on port 443.") http01_port = zope.interface.Attribute( - "Port used in the SimpleHttp challenge.") + "Port used in the http-01 challenge." + "This only affects the port Certbot listens on. " + "A conforming ACME server will still attempt to connect on port 80.") class IInstaller(IPlugin): diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 6c7b822ab..2ef49d7f4 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -15,6 +15,7 @@ import zope.component import zope.interface from acme import challenges +from acme import errors as acme_errors from certbot import errors from certbot import interfaces @@ -41,7 +42,16 @@ class Authenticator(common.Plugin): description = "Manually configure an HTTP server" - MESSAGE_TEMPLATE = """\ + MESSAGE_TEMPLATE = { + "dns-01": """\ +Please deploy a DNS TXT record under the name +{domain} with the following value: + +{validation} + +Once this is deployed, +""", + "http-01": """\ Make sure your web server displays the following content at {uri} before continuing: @@ -51,7 +61,7 @@ If you don't have HTTP server configured, you can run the following command on the target server (as root): {command} -""" +"""} # a disclaimer about your current IP being transmitted to Let's Encrypt's servers. IP_DISCLAIMER = """\ @@ -97,21 +107,29 @@ s.serve_forever()" """ def more_info(self): # pylint: disable=missing-docstring,no-self-use return ("This plugin requires user's manual intervention in setting " - "up an HTTP server for solving http-01 challenges and thus " - "does not need to be run as a privileged process. " - "Alternatively shows instructions on how to use Python's " - "built-in HTTP server.") + "up challenges to prove control of a domain and does not need " + "to be run as a privileged process. When solving " + "http-01 challenges, the user is responsible for setting up " + "an HTTP server. Alternatively, instructions are shown on how " + "to use Python's built-in HTTP server. The user is " + "responsible for configuration of a domain's DNS when solving " + "dns-01 challenges. The type of challenges used can be " + "controlled through the --preferred-challenges flag.") def get_chall_pref(self, domain): # pylint: disable=missing-docstring,no-self-use,unused-argument - return [challenges.HTTP01] + return [challenges.HTTP01, challenges.DNS01] - def perform(self, achalls): # pylint: disable=missing-docstring + def perform(self, achalls): + # pylint: disable=missing-docstring + self._get_ip_logging_permission() + mapping = {"http-01": self._perform_http01_challenge, + "dns-01": self._perform_dns01_challenge} responses = [] # TODO: group achalls by the same socket.gethostbyname(_ex) # and prompt only once per server (one "echo -n" per domain) for achall in achalls: - responses.append(self._perform_single(achall)) + responses.append(mapping[achall.typ](achall)) return responses @classmethod @@ -128,7 +146,13 @@ s.serve_forever()" """ finally: sock.close() - def _perform_single(self, achall): + def cleanup(self, achalls): + # pylint: disable=missing-docstring + for achall in achalls: + if isinstance(achall.chall, challenges.HTTP01): + self._cleanup_http01_challenge(achall) + + def _perform_http01_challenge(self, achall): # same path for each challenge response would be easier for # users, but will not work if multiple domains point at the # same server: default command doesn't support virtual hosts @@ -162,19 +186,16 @@ s.serve_forever()" """ # give it some time to bootstrap, before we try to verify # (cert generation in case of simpleHttpS might take time) self._test_mode_busy_wait(port) + if self._httpd.poll() is not None: raise errors.Error("Couldn't execute manual command") else: - if not self.conf("public-ip-logging-ok"): - if not zope.component.getUtility(interfaces.IDisplay).yesno( - self.IP_DISCLAIMER, "Yes", "No", - cli_flag="--manual-public-ip-logging-ok"): - raise errors.PluginError("Must agree to IP logging to proceed") - - self._notify_and_wait(self.MESSAGE_TEMPLATE.format( - validation=validation, response=response, - uri=achall.chall.uri(achall.domain), - command=command)) + self._notify_and_wait( + self._get_message(achall).format( + validation=validation, + response=response, + uri=achall.chall.uri(achall.domain), + command=command)) if not response.simple_verify( achall.chall, achall.domain, @@ -183,15 +204,30 @@ s.serve_forever()" """ return response - def _notify_and_wait(self, message): # pylint: disable=no-self-use - # TODO: IDisplay wraps messages, breaking the command - #answer = zope.component.getUtility(interfaces.IDisplay).notification( - # message=message, height=25, pause=True) - sys.stdout.write(message) - six.moves.input("Press ENTER to continue") + def _perform_dns01_challenge(self, achall): + response, validation = achall.response_and_validation() + if not self.conf("test-mode"): + self._notify_and_wait( + self._get_message(achall).format( + validation=validation, + domain=achall.validation_domain_name(achall.domain), + response=response)) - def cleanup(self, achalls): - # pylint: disable=missing-docstring,no-self-use,unused-argument + try: + verification_status = response.simple_verify( + achall.chall, achall.domain, + achall.account_key.public_key()) + except acme_errors.DependencyError: + logger.warning("Self verification requires optional " + "dependency `dnspython` to be installed.") + else: + if not verification_status: + logger.warning("Self-verify of challenge failed.") + + return response + + def _cleanup_http01_challenge(self, achall): + # pylint: disable=missing-docstring,unused-argument if self.conf("test-mode"): assert self._httpd is not None, ( "cleanup() must be called after perform()") @@ -202,3 +238,25 @@ s.serve_forever()" """ logger.debug("Manual command process already terminated " "with %s code", self._httpd.returncode) shutil.rmtree(self._root) + + def _notify_and_wait(self, message): + # pylint: disable=no-self-use + # TODO: IDisplay wraps messages, breaking the command + #answer = zope.component.getUtility(interfaces.IDisplay).notification( + # message=message, height=25, pause=True) + sys.stdout.write(message) + six.moves.input("Press ENTER to continue") + + def _get_ip_logging_permission(self): + # pylint: disable=missing-docstring + if not (self.conf("test-mode") or self.conf("public-ip-logging-ok")): + if not zope.component.getUtility(interfaces.IDisplay).yesno( + self.IP_DISCLAIMER, "Yes", "No", + cli_flag="--manual-public-ip-logging-ok"): + raise errors.PluginError("Must agree to IP logging to proceed") + else: + self.config.namespace.manual_public_ip_logging_ok = True + + def _get_message(self, achall): + # pylint: disable=missing-docstring,no-self-use,unused-argument + return self.MESSAGE_TEMPLATE.get(achall.chall.typ, "") diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index dd0905049..25107e4b4 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -5,6 +5,7 @@ import unittest import mock from acme import challenges +from acme import errors as acme_errors from acme import jose from certbot import achallenges @@ -26,8 +27,13 @@ class AuthenticatorTest(unittest.TestCase): 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)] + + self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY) + self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY) + + self.achalls = [self.http01, self.dns01] config_test_mode = mock.MagicMock( http01_port=8080, manual_test_mode=True, noninteractive_mode=True) @@ -45,7 +51,9 @@ class AuthenticatorTest(unittest.TestCase): self.assertTrue(all(issubclass(pref, challenges.Challenge) for pref in self.auth.get_chall_pref("foo.com"))) - def test_perform_empty(self): + @mock.patch("certbot.plugins.manual.zope.component.getUtility") + def test_perform_empty(self, mock_interaction): + mock_interaction().yesno.return_value = True self.assertEqual([], self.auth.perform([])) @mock.patch("certbot.plugins.manual.zope.component.getUtility") @@ -56,19 +64,34 @@ class AuthenticatorTest(unittest.TestCase): mock_verify.return_value = True mock_interaction().yesno.return_value = True - resp = self.achalls[0].response(KEY) - self.assertEqual([resp], self.auth.perform(self.achalls)) - self.assertEqual(1, mock_raw_input.call_count) + resp_http = self.http01.response(KEY) + resp_dns = self.dns01.response(KEY) + + self.assertEqual([resp_http, resp_dns], self.auth.perform(self.achalls)) + self.assertEqual(2, mock_raw_input.call_count) mock_verify.assert_called_with( - self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 8080) + self.http01.challb.chall, "foo.com", KEY.public_key(), 8080) message = mock_stdout.write.mock_calls[0][1][0] - self.assertTrue(self.achalls[0].chall.encode("token") in message) + self.assertTrue(self.http01.chall.encode("token") in message) mock_verify.return_value = False with mock.patch("certbot.plugins.manual.logger") as mock_logger: self.auth.perform(self.achalls) - mock_logger.warning.assert_called_once_with(mock.ANY) + self.assertEqual(2, mock_logger.warning.call_count) + + @mock.patch("certbot.plugins.manual.zope.component.getUtility") + @mock.patch("acme.challenges.DNS01Response.simple_verify") + @mock.patch("six.moves.input") + def test_perform_missing_dependency(self, mock_raw_input, mock_verify, mock_interaction): + mock_interaction().yesno.return_value = True + mock_verify.side_effect = acme_errors.DependencyError() + + with mock.patch("certbot.plugins.manual.logger") as mock_logger: + self.auth.perform([self.dns01]) + self.assertEqual(1, mock_logger.warning.call_count) + + mock_raw_input.assert_called_once_with("Press ENTER to continue") @mock.patch("certbot.plugins.manual.zope.component.getUtility") @mock.patch("certbot.plugins.manual.Authenticator._notify_and_wait") @@ -82,7 +105,7 @@ class AuthenticatorTest(unittest.TestCase): @mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True) def test_perform_test_command_oserror(self, mock_popen): mock_popen.side_effect = OSError - self.assertEqual([False], self.auth_test_mode.perform(self.achalls)) + self.assertEqual([False], self.auth_test_mode.perform([self.http01])) @mock.patch("certbot.plugins.manual.socket.socket") @mock.patch("certbot.plugins.manual.time.sleep", autospec=True) diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index 97aca351a..0195b2726 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -3,6 +3,7 @@ import argparse import collections import logging import socket +import sys import threading import OpenSSL @@ -12,6 +13,7 @@ import zope.interface from acme import challenges from acme import standalone as acme_standalone +from certbot import cli from certbot import errors from certbot import interfaces @@ -119,6 +121,11 @@ def supported_challenges_validator(data): It should be passed as `type` argument to `add_argument`. """ + if cli.set_by_cli("standalone_supported_challenges"): + sys.stderr.write( + "WARNING: The standalone specific " + "supported challenges flag is deprecated.\n" + "Please use the --preferred-challenges flag instead.\n") challs = data.split(",") # tls-sni-01 was dvsni during private beta @@ -177,7 +184,7 @@ class Authenticator(common.Plugin): @classmethod def add_parser_arguments(cls, add): add("supported-challenges", - help="Supported challenges. Preferred in the order they are listed.", + help=argparse.SUPPRESS, type=supported_challenges_validator, default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES)) diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index eb6631732..1dfa3950a 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -67,10 +67,25 @@ class ServerManagerTest(unittest.TestCase): class SupportedChallengesValidatorTest(unittest.TestCase): """Tests for plugins.standalone.supported_challenges_validator.""" + def setUp(self): + self.set_by_cli_patch = mock.patch( + "certbot.plugins.standalone.cli.set_by_cli") + self.stderr_patch = mock.patch("certbot.plugins.standalone.sys.stderr") + + self.set_by_cli_patch.start().return_value = True + self.stderr = self.stderr_patch.start() + + def tearDown(self): + self.set_by_cli_patch.stop() + self.stderr_patch.stop() + def _call(self, data): from certbot.plugins.standalone import ( supported_challenges_validator) - return supported_challenges_validator(data) + return_value = supported_challenges_validator(data) + self.assertTrue(self.stderr.write.called) # pylint: disable=no-member + self.stderr.write.reset_mock() # pylint: disable=no-member + return return_value def test_correct(self): self.assertEqual("tls-sni-01", self._call("tls-sni-01")) diff --git a/certbot/tests/acme_util.py b/certbot/tests/acme_util.py index 4f6e86cc7..de64dfef9 100644 --- a/certbot/tests/acme_util.py +++ b/certbot/tests/acme_util.py @@ -17,9 +17,9 @@ HTTP01 = challenges.HTTP01( token=b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") TLSSNI01 = challenges.TLSSNI01( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) -DNS = challenges.DNS(token=b"17817c66b60ce2e4012dfad92657527a") +DNS01 = challenges.DNS01(token=b"17817c66b60ce2e4012dfad92657527a") -CHALLENGES = [HTTP01, TLSSNI01, DNS] +CHALLENGES = [HTTP01, TLSSNI01, DNS01] def gen_combos(challbs): @@ -45,9 +45,9 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING) HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING) -DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING) +DNS01_P = chall_to_challb(DNS01, messages.STATUS_PENDING) -CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS_P] +CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS01_P] def gen_authzr(authz_status, domain, challs, statuses, combos=True): diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index fce130f7c..84c3e16fa 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -24,7 +24,7 @@ class ChallengeFactoryTest(unittest.TestCase): from certbot.auth_handler import AuthHandler # Account is mocked... - self.handler = AuthHandler(None, None, mock.Mock(key="mock_key")) + self.handler = AuthHandler(None, None, mock.Mock(key="mock_key"), []) self.dom = "test" self.handler.authzr[self.dom] = acme_util.gen_authzr( @@ -74,7 +74,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_net = mock.MagicMock(spec=acme_client.Client) self.handler = AuthHandler( - self.mock_auth, self.mock_net, self.mock_account) + self.mock_auth, self.mock_net, self.mock_account, []) logging.disable(logging.CRITICAL) @@ -111,7 +111,7 @@ class GetAuthorizationsTest(unittest.TestCase): mock_poll.side_effect = self._validate_all self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01) - self.mock_auth.get_chall_pref.return_value.append(challenges.DNS) + self.mock_auth.get_chall_pref.return_value.append(challenges.DNS01) authzr = self.handler.get_authorizations(["0"]) @@ -125,7 +125,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.assertEqual(self.mock_auth.cleanup.call_count, 1) # Test if list first element is TLSSNI01, use typ because it is an achall for achall in self.mock_auth.cleanup.call_args[0][0]: - self.assertTrue(achall.typ in ["tls-sni-01", "http-01", "dns"]) + self.assertTrue(achall.typ in ["tls-sni-01", "http-01", "dns-01"]) # Length of authorizations list self.assertEqual(len(authzr), 1) @@ -167,6 +167,29 @@ class GetAuthorizationsTest(unittest.TestCase): def test_no_domains(self): self.assertRaises(errors.AuthorizationError, self.handler.get_authorizations, []) + @mock.patch("certbot.auth_handler.AuthHandler._poll_challenges") + def test_preferred_challenge_choice(self, mock_poll): + self.mock_net.request_domain_challenges.side_effect = functools.partial( + gen_dom_authzr, challs=acme_util.CHALLENGES) + + mock_poll.side_effect = self._validate_all + self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01) + + self.handler.pref_challs.extend((challenges.HTTP01, challenges.DNS01,)) + + self.handler.get_authorizations(["0"]) + + self.assertEqual(self.mock_auth.cleanup.call_count, 1) + self.assertEqual( + self.mock_auth.cleanup.call_args[0][0][0].typ, "http-01") + + def test_preferred_challenges_not_supported(self): + self.mock_net.request_domain_challenges.side_effect = functools.partial( + gen_dom_authzr, challs=acme_util.CHALLENGES) + self.handler.pref_challs.append(challenges.HTTP01) + self.assertRaises( + errors.AuthorizationError, self.handler.get_authorizations, ["0"]) + def _validate_all(self, unused_1, unused_2): for dom in six.iterkeys(self.handler.authzr): azr = self.handler.authzr[dom] @@ -189,7 +212,7 @@ class PollChallengesTest(unittest.TestCase): # Account and network are mocked... self.mock_net = mock.MagicMock() self.handler = AuthHandler( - None, self.mock_net, mock.Mock(key="mock_key")) + None, self.mock_net, mock.Mock(key="mock_key"), []) self.doms = ["0", "1", "2"] self.handler.authzr[self.doms[0]] = acme_util.gen_authzr( @@ -240,7 +263,7 @@ class PollChallengesTest(unittest.TestCase): from certbot.auth_handler import challb_to_achall self.mock_net.poll.side_effect = self._mock_poll_solve_one_valid self.chall_update[self.doms[0]].append( - challb_to_achall(acme_util.DNS_P, "key", self.doms[0])) + challb_to_achall(acme_util.DNS01_P, "key", self.doms[0])) self.assertRaises( errors.AuthorizationError, self.handler._poll_challenges, self.chall_update, False) @@ -342,7 +365,7 @@ class GenChallengePathTest(unittest.TestCase): self.assertTrue(self._call(challbs[::-1], prefs, None)) def test_not_supported(self): - challbs = (acme_util.DNS_P, acme_util.TLSSNI01_P) + challbs = (acme_util.DNS01_P, acme_util.TLSSNI01_P) prefs = [challenges.TLSSNI01] combos = ((0, 1),) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 2c6e32705..fdfb9dcc8 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -423,6 +423,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods namespace = parse(long_args) self.assertEqual(namespace.domains, ['example.com', 'another.net']) + def test_preferred_challenges(self): + from acme.challenges import HTTP01, TLSSNI01, DNS01 + parse = self._get_argument_parser() + + short_args = ['--preferred-challenges', 'http, tls-sni-01, dns'] + namespace = parse(short_args) + + self.assertEqual(namespace.pref_challs, [HTTP01, TLSSNI01, DNS01]) + + short_args = ['--preferred-challenges', 'jumping-over-the-moon'] + self.assertRaises(argparse.ArgumentTypeError, parse, short_args) + def test_server_flag(self): parse = self._get_argument_parser() namespace = parse('--server example.com'.split()) diff --git a/certbot/tests/errors_test.py b/certbot/tests/errors_test.py index 67611ed45..f35a5ea08 100644 --- a/certbot/tests/errors_test.py +++ b/certbot/tests/errors_test.py @@ -16,12 +16,12 @@ class FaiiledChallengesTest(unittest.TestCase): from certbot.errors import FailedChallenges self.error = FailedChallenges(set([achallenges.DNS( domain="example.com", challb=messages.ChallengeBody( - chall=acme_util.DNS, uri=None, + chall=acme_util.DNS01, uri=None, error=messages.Error(typ="tls", detail="detail")))])) def test_str(self): self.assertTrue(str(self.error).startswith( - "Failed authorization procedure. example.com (dns): tls")) + "Failed authorization procedure. example.com (dns-01): tls")) class StandaloneBindErrorTest(unittest.TestCase): diff --git a/docs/using.rst b/docs/using.rst index 20b6cc5c7..18dca071a 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -60,7 +60,7 @@ an alternate method fo install ``certbot``. Certbot-Auto ^^^^^^^^^^^^ -The ``certbot-auto`` wrapper script installs Certbot, obtaining some dependencies +The ``certbot-auto`` wrapper script installs Certbot, obtaining some dependencies from your web server OS and putting others in a python virtual environment. You can download and run it as follows:: @@ -77,8 +77,8 @@ download and run it as follows:: The ``certbot-auto`` command updates to the latest client release automatically. Since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly -the same command line flags and arguments. For more information, see -`Certbot command-line options `_. +the same command line flags and arguments. For more information, see +`Certbot command-line options `_. Running with Docker ^^^^^^^^^^^^^^^^^^^ @@ -88,8 +88,8 @@ certificate. However, this mode of operation is unable to install certificates or configure your webserver, because our installer plugins cannot reach your webserver from inside the Docker container. -Most users should use the operating system packages (see instructions at -certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only +Most users should use the operating system packages (see instructions at +certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only use Docker if you are sure you know what you are doing and have a good reason to do so. @@ -113,12 +113,12 @@ to, `install Docker`_, then issue the following command: quay.io/letsencrypt/letsencrypt:latest certonly Running Certbot with the ``certonly`` command will obtain a certificate and place it in the directory -``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from +``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from within Docker, you must install the certificate manually according to the procedure recommended by the provider of your webserver. -For more information about the layout -of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. +For more information about the layout +of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. .. _Docker: https://docker.com .. _`install Docker`: https://docs.docker.com/userguide/ @@ -242,8 +242,8 @@ whole process is described in the :doc:`contributing`. .. _plugins: -Getting certificates -==================== +Getting certificates (and chosing plugins) +========================================== The Certbot client supports a number of different "plugins" that can be used to obtain and/or install certificates. @@ -252,38 +252,50 @@ Plugins that can obtain a cert are called "authenticators" and can be used with the "certonly" command. This will carry out the steps needed to validate that you control the domain(s) you are requesting a cert for, obtain a cert for the specified domain(s), and place it in the ``/etc/letsencrypt`` directory on your -machine - without editing any of your server's configuration files to serve the +machine - without editing any of your server's configuration files to serve the obtained certificate. If you specify multiple domains to authenticate, they will all be listed in a single certificate. To obtain multiple seperate certificates you will need to run Certbot multiple times. -Plugins that can install a cert are called "installers" and can be used with the +Plugins that can install a cert are called "installers" and can be used with the "install" command. These plugins can modify your webserver's configuration to -serve your website over HTTPS using certificates obtained by certbot. +serve your website over HTTPS using certificates obtained by certbot. Plugins that do both can be used with the "certbot run" command, which is the default when no command is specified. The "run" subcommand can also be used to specify a combination of distinct authenticator and installer plugins. -=========== ==== ==== =============================================================== -Plugin Auth Inst Notes -=========== ==== ==== =============================================================== -apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on - Debian-based distributions with ``libaugeas0`` 1.0+. -webroot_ Y N Obtains a cert by writing to the webroot directory of an - already running webserver. -standalone_ Y N Uses a "standalone" webserver to obtain a cert. Requires - port 80 or 443 to be available. This is useful on systems - with no webserver, or when direct integration with the local - webserver is not supported or not desired. -manual_ Y N Helps you obtain a cert by giving you instructions to perform - domain validation yourself. -nginx_ Y Y Very experimental and not included in certbot-auto_. -=========== ==== ==== =============================================================== +=========== ==== ==== =============================================================== ============================= +Plugin Auth Inst Notes Challenge types (and port) +=========== ==== ==== =============================================================== ============================= +apache_ Y Y | Automates obtaining and installing a cert with Apache 2.4 on tls-sni-01_ (443) + | Debian-based distributions with ``libaugeas0`` 1.0+. +webroot_ Y N | Obtains a cert by writing to the webroot directory of an http-01_ (80) + | already running webserver. +standalone_ Y N | Uses a "standalone" webserver to obtain a cert. Requires http-01_ (80) or + | port 80 or 443 to be available. This is useful on systems tls-sni-01_ (443) + | with no webserver, or when direct integration with the local + | webserver is not supported or not desired. +manual_ Y N | Helps you obtain a cert by giving you instructions to perform http-01_ (80) or + | domain validation yourself. dns-01_ (53) +nginx_ Y Y | Very experimental and not included in certbot-auto_. tls-sni-01_ (443) +=========== ==== ==== =============================================================== ============================= + +Under the hood, plugins use one of several ACME protocol "Challenges_" to +prove you control a domain. The options are http-01_ (which uses port 80), +tls-sni-01_ (port 443) and dns-01_ (requring configuration of a DNS server on +port 53, thought that's often not the same machine as your webserver). A few +plugins support more than one challenge type, in which case you can choose one +with ``--preferred-challenges``. There are also many third-party-plugins_ available. Below we describe in more detail the circumstances in which each plugin can be used, and how to use it. +.. _Challenges: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7 +.. _tls-sni-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3 +.. _http-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.2 +.. _dns-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.4 + Apache ------