From 96847ba77982cad3af708656e7371920ed5cbe0c Mon Sep 17 00:00:00 2001 From: osirisinferi Date: Sun, 27 Feb 2022 21:25:49 +0100 Subject: [PATCH] Add extra challenge info to `--debug-challenges` (#9208) * Add challenge info to `--debug-challenges` * Expand/add tests * Add changelog entry * Make tests Python 3.6 and 3.7 compatible * Don't use `config.namespace` * And don't use `config.namespace` in tests too * Expand tests to check for token/thumbprint * Add test for the DNS-01 challenge Changed the Apache authenticator to the manual authenticator. Doesn't seem to make a difference to the tests, but makes more sense if the DNS-01 challenge is being used. * Reword changelog entry * Mention feature in --help output * Better variable assignment in test Co-authored-by: alexzorin * Better variable assignment in test Co-authored-by: alexzorin * Remove unnecessary `verbose_count` assignment Co-authored-by: alexzorin * Use terminology from RFC 8555 * Compress the two new tests into one * s/world wide web/internet * Move new code into separate function * Remove superfluous newline with mixed challs Co-authored-by: alexzorin --- certbot/CHANGELOG.md | 3 ++ certbot/certbot/_internal/auth_handler.py | 42 +++++++++++++++++++- certbot/certbot/_internal/cli/__init__.py | 4 +- certbot/tests/auth_handler_test.py | 47 +++++++++++++++++++++-- 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 9794e8426..73d2a5a7c 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -6,6 +6,9 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### Added +* When the `--debug-challenges` option is used in combination with `-v`, Certbot + now displays the challenge URLs (for `http-01` challenges) or FQDNs (for + `dns-01` challenges) and their expected return values. * ### Changed diff --git a/certbot/certbot/_internal/auth_handler.py b/certbot/certbot/_internal/auth_handler.py index 16b8dfdbe..e7e059656 100644 --- a/certbot/certbot/_internal/auth_handler.py +++ b/certbot/certbot/_internal/auth_handler.py @@ -88,8 +88,8 @@ class AuthHandler: # If debug is on, wait for user input before starting the verification process. if config.debug_challenges: display_util.notification( - 'Challenges loaded. Press continue to submit to CA. ' - 'Pass "-v" for more info about challenges.', pause=True) + 'Challenges loaded. Press continue to submit to CA.\n' + + self._debug_challenges_msg(achalls, config), pause=True) except errors.AuthorizationError as error: logger.critical('Failure in setting up challenges.') logger.info('Attempting to clean up outstanding challenges...') @@ -324,6 +324,44 @@ class AuthHandler: display_util.notify("".join(msg)) + def _debug_challenges_msg(self, achalls: List[achallenges.AnnotatedChallenge], + config: configuration.NamespaceConfig) -> str: + """Construct message for debug challenges prompt + + :param list achalls: A list of + :class:`certbot.achallenges.AnnotatedChallenge`. + :param certbot.configuration.NamespaceConfig config: current Certbot configuration + :returns: Message containing challenge debug info + :rtype: str + + """ + if config.verbose_count > 0: + msg = [] + http01_achalls = {} + dns01_achalls = {} + for achall in achalls: + if isinstance(achall.chall, challenges.HTTP01): + http01_achalls[achall.chall.uri(achall.domain)] = ( + achall.validation(achall.account_key) + "\n" + ) + if isinstance(achall.chall, challenges.DNS01): + dns01_achalls[achall.validation_domain_name(achall.domain)] = ( + achall.validation(achall.account_key) + "\n" + ) + if http01_achalls: + msg.append("The following URLs should be accessible from the " + "internet and return the value mentioned:\n") + for uri, key_authz in http01_achalls.items(): + msg.append(f"URL: {uri}\nExpected value: {key_authz}") + if dns01_achalls: + msg.append("The following FQDNs should return a TXT resource " + "record with the value mentioned:\n") + for fqdn, key_authz_hash in dns01_achalls.items(): + msg.append(f"FQDN: {fqdn}\nExpected value: {key_authz_hash}") + return "\n" + "\n".join(msg) + else: + return 'Pass "-v" for more info about challenges.' + def challb_to_achall(challb: messages.ChallengeBody, account_key: josepy.JWK, domain: str) -> achallenges.AnnotatedChallenge: diff --git a/certbot/certbot/_internal/cli/__init__.py b/certbot/certbot/_internal/cli/__init__.py index a69f96666..d11a454b1 100644 --- a/certbot/certbot/_internal/cli/__init__.py +++ b/certbot/certbot/_internal/cli/__init__.py @@ -264,7 +264,9 @@ def prepare_and_parse_args(plugins: plugins_disco.PluginsRegistry, args: List[st [None, "certonly", "run"], "--debug-challenges", action="store_true", default=flag_default("debug_challenges"), help="After setting up challenges, wait for user input before " - "submitting to CA") + "submitting to CA. When used in combination with the `-v` " + "option, the challenge URLs or FQDNs and their expected " + "return values are shown.") helpful.add( "testing", "--no-verify-ssl", action="store_true", help=config_help("no_verify_ssl"), diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index a94259a79..e13dfbfe5 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -3,6 +3,7 @@ import functools import logging import unittest +from josepy import b64encode try: import mock except ImportError: # pragma: no cover @@ -72,13 +73,13 @@ class HandleAuthorizationsTest(unittest.TestCase): with mock.patch("zope.component.provideUtility"): display_obj.set_display(self.mock_display) - self.mock_auth = mock.MagicMock(name="ApacheConfigurator") + self.mock_auth = mock.MagicMock(name="Authenticator") self.mock_auth.get_chall_pref.return_value = [challenges.HTTP01] self.mock_auth.perform.side_effect = gen_auth_resp - self.mock_account = mock.Mock(key=util.Key("file_path", "PEM")) + self.mock_account = mock.MagicMock() self.mock_net = mock.MagicMock(spec=acme_client.ClientV2) self.mock_net.acme_version = 1 self.mock_net.retry_after.side_effect = acme_client.ClientV2.retry_after @@ -190,16 +191,56 @@ class HandleAuthorizationsTest(unittest.TestCase): self._test_name3_http_01_3_common(combos=False) def test_debug_challenges(self): - config = mock.Mock(debug_challenges=True) + config = mock.Mock(debug_challenges=True, verbose_count=0) authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) + account_key_thumbprint = b"foobarbaz" + self.mock_account.key.thumbprint.return_value = account_key_thumbprint + self.mock_net.poll.side_effect = _gen_mock_on_poll() self.handler.handle_authorizations(mock_order, config) self.assertEqual(self.mock_net.answer_challenge.call_count, 1) self.assertEqual(self.mock_display.notification.call_count, 1) + self.assertIn('Pass "-v" for more info', + self.mock_display.notification.call_args[0][0]) + self.assertNotIn(f"http://{authzrs[0].body.identifier.value}/.well-known/acme-challenge/" + + b64encode(authzrs[0].body.challenges[0].chall.token).decode(), + self.mock_display.notification.call_args[0][0]) + self.assertNotIn(b64encode(account_key_thumbprint).decode(), + self.mock_display.notification.call_args[0][0]) + + def test_debug_challenges_verbose(self): + config = mock.Mock(debug_challenges=True, verbose_count=1) + authzrs = [gen_dom_authzr(domain="0", challs=[acme_util.HTTP01]), + gen_dom_authzr(domain="1", challs=[acme_util.DNS01])] + mock_order = mock.MagicMock(authorizations=authzrs) + + account_key_thumbprint = b"foobarbaz" + self.mock_account.key.thumbprint.return_value = account_key_thumbprint + + self.mock_net.poll.side_effect = _gen_mock_on_poll() + + self.mock_auth.get_chall_pref.return_value = [challenges.HTTP01, + challenges.DNS01] + + self.handler.handle_authorizations(mock_order, config) + + self.assertEqual(self.mock_net.answer_challenge.call_count, 2) + self.assertEqual(self.mock_display.notification.call_count, 1) + self.assertNotIn('Pass "-v" for more info', + self.mock_display.notification.call_args[0][0]) + self.assertIn(f"http://{authzrs[0].body.identifier.value}/.well-known/acme-challenge/" + + b64encode(authzrs[0].body.challenges[0].chall.token).decode(), + self.mock_display.notification.call_args[0][0]) + self.assertIn(b64encode(account_key_thumbprint).decode(), + self.mock_display.notification.call_args[0][0]) + self.assertIn(f"_acme-challenge.{authzrs[1].body.identifier.value}", + self.mock_display.notification.call_args[0][0]) + self.assertIn(authzrs[1].body.challenges[0].validation(self.mock_account.key), + self.mock_display.notification.call_args[0][0]) def test_perform_failure(self): authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)]