mirror of
https://github.com/certbot/certbot.git
synced 2026-06-08 08:12:15 -04:00
Merge pull request #3466 from certbot/all-together-now
DNS challenge support in the manual plugin and general purpose --preferred-challenges flag
This commit is contained in:
commit
1584ee8ac6
15 changed files with 292 additions and 92 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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, "")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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),)
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 <https://certbot.eff.org/docs/using.html#command-line-options>`_.
|
||||
the same command line flags and arguments. For more information, see
|
||||
`Certbot command-line options <https://certbot.eff.org/docs/using.html#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
|
||||
------
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue