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 <alex@zor.io>

* Better variable assignment in test

Co-authored-by: alexzorin <alex@zor.io>

* Remove unnecessary `verbose_count` assignment

Co-authored-by: alexzorin <alex@zor.io>

* 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 <alex@zor.io>
This commit is contained in:
osirisinferi 2022-02-27 21:25:49 +01:00 committed by GitHub
parent d9dd3134f0
commit 96847ba779
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 6 deletions

View file

@ -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

View file

@ -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:

View file

@ -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"),

View file

@ -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)]