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:
Peter Eckersley
2016-09-22 14:15:25 -07:00
committed by GitHub
15 changed files with 292 additions and 92 deletions
+2 -2
View File
@@ -234,8 +234,8 @@ class DNS01Response(KeyAuthorizationChallengeResponse):
try: try:
from acme import dns_resolver from acme import dns_resolver
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
raise errors.Error("Local validation for 'dns-01' challenges " raise errors.DependencyError("Local validation for 'dns-01' "
"requires 'dnspython'") "challenges requires 'dnspython'")
txt_records = dns_resolver.txt_records_for_name(validation_domain_name) txt_records = dns_resolver.txt_records_for_name(validation_domain_name)
exists = validation in txt_records exists = validation in txt_records
if not exists: if not exists:
+4
View File
@@ -6,6 +6,10 @@ class Error(Exception):
"""Generic ACME error.""" """Generic ACME error."""
class DependencyError(Error):
"""Dependency error"""
class SchemaValidationError(jose_errors.DeserializationError): class SchemaValidationError(jose_errors.DeserializationError):
"""JSON schema ACME object validation error.""" """JSON schema ACME object validation error."""
+16 -3
View File
@@ -33,14 +33,18 @@ class AuthHandler(object):
and values are :class:`acme.messages.AuthorizationResource` and values are :class:`acme.messages.AuthorizationResource`
:ivar list achalls: DV challenges in the form of :ivar list achalls: DV challenges in the form of
:class:`certbot.achallenges.AnnotatedChallenge` :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.auth = auth
self.acme = acme self.acme = acme
self.account = account self.account = account
self.authzr = dict() self.authzr = dict()
self.pref_challs = pref_challs
# List must be used to keep responses straight. # List must be used to keep responses straight.
self.achalls = [] self.achalls = []
@@ -244,9 +248,18 @@ class AuthHandler(object):
:param str domain: domain for which you are requesting preferences :param str domain: domain for which you are requesting preferences
""" """
# Make sure to make a copy...
chall_prefs = [] 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 return chall_prefs
def _cleanup_challenges(self, achall_list=None): def _cleanup_challenges(self, achall_list=None):
+32 -2
View File
@@ -11,6 +11,8 @@ import sys
import configargparse import configargparse
import six import six
from acme import challenges
import certbot import certbot
from certbot import constants 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"), help=config_help("no_verify_ssl"),
default=flag_default("no_verify_ssl")) default=flag_default("no_verify_ssl"))
helpful.add( 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"), default=flag_default("tls_sni_01_port"),
help=config_help("tls_sni_01_port")) help=config_help("tls_sni_01_port"))
helpful.add( 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")) default=flag_default("http01_port"), help=config_help("http01_port"))
helpful.add( helpful.add(
"testing", "--break-my-certs", action="store_true", "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", "security", "--strict-permissions", action="store_true",
help="Require that all configuration files are owned by the current " help="Require that all configuration files are owned by the current "
"user; only needed if your config is somewhere unsafe like /tmp/") "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( helpful.add(
"renew", "--pre-hook", "renew", "--pre-hook",
help="Command to be run in a shell before obtaining any certificates." 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) args_or_config.domains.append(domain)
return validated_domains 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)
+1 -1
View File
@@ -192,7 +192,7 @@ class Client(object):
if auth is not None: if auth is not None:
self.auth_handler = auth_handler.AuthHandler( self.auth_handler = auth_handler.AuthHandler(
auth, self.acme, self.account) auth, self.acme, self.account, self.config.pref_challs)
else: else:
self.auth_handler = None self.auth_handler = None
+6 -3
View File
@@ -229,11 +229,14 @@ class IConfig(zope.interface.Interface):
no_verify_ssl = zope.interface.Attribute( no_verify_ssl = zope.interface.Attribute(
"Disable verification of the ACME server's certificate.") "Disable verification of the ACME server's certificate.")
tls_sni_01_port = zope.interface.Attribute( tls_sni_01_port = zope.interface.Attribute(
"Port number to perform tls-sni-01 challenge. " "Port used during tls-sni-01 challenge. "
"Boulder in testing mode defaults to 5001.") "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( 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): class IInstaller(IPlugin):
+86 -28
View File
@@ -15,6 +15,7 @@ import zope.component
import zope.interface import zope.interface
from acme import challenges from acme import challenges
from acme import errors as acme_errors
from certbot import errors from certbot import errors
from certbot import interfaces from certbot import interfaces
@@ -41,7 +42,16 @@ class Authenticator(common.Plugin):
description = "Manually configure an HTTP server" 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 Make sure your web server displays the following content at
{uri} before continuing: {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 on the target server (as root):
{command} {command}
""" """}
# a disclaimer about your current IP being transmitted to Let's Encrypt's servers. # a disclaimer about your current IP being transmitted to Let's Encrypt's servers.
IP_DISCLAIMER = """\ IP_DISCLAIMER = """\
@@ -97,21 +107,29 @@ s.serve_forever()" """
def more_info(self): # pylint: disable=missing-docstring,no-self-use def more_info(self): # pylint: disable=missing-docstring,no-self-use
return ("This plugin requires user's manual intervention in setting " return ("This plugin requires user's manual intervention in setting "
"up an HTTP server for solving http-01 challenges and thus " "up challenges to prove control of a domain and does not need "
"does not need to be run as a privileged process. " "to be run as a privileged process. When solving "
"Alternatively shows instructions on how to use Python's " "http-01 challenges, the user is responsible for setting up "
"built-in HTTP server.") "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): def get_chall_pref(self, domain):
# pylint: disable=missing-docstring,no-self-use,unused-argument # 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 = [] responses = []
# TODO: group achalls by the same socket.gethostbyname(_ex) # TODO: group achalls by the same socket.gethostbyname(_ex)
# and prompt only once per server (one "echo -n" per domain) # and prompt only once per server (one "echo -n" per domain)
for achall in achalls: for achall in achalls:
responses.append(self._perform_single(achall)) responses.append(mapping[achall.typ](achall))
return responses return responses
@classmethod @classmethod
@@ -128,7 +146,13 @@ s.serve_forever()" """
finally: finally:
sock.close() 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 # same path for each challenge response would be easier for
# users, but will not work if multiple domains point at the # users, but will not work if multiple domains point at the
# same server: default command doesn't support virtual hosts # 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 # give it some time to bootstrap, before we try to verify
# (cert generation in case of simpleHttpS might take time) # (cert generation in case of simpleHttpS might take time)
self._test_mode_busy_wait(port) self._test_mode_busy_wait(port)
if self._httpd.poll() is not None: if self._httpd.poll() is not None:
raise errors.Error("Couldn't execute manual command") raise errors.Error("Couldn't execute manual command")
else: else:
if not self.conf("public-ip-logging-ok"): self._notify_and_wait(
if not zope.component.getUtility(interfaces.IDisplay).yesno( self._get_message(achall).format(
self.IP_DISCLAIMER, "Yes", "No", validation=validation,
cli_flag="--manual-public-ip-logging-ok"): response=response,
raise errors.PluginError("Must agree to IP logging to proceed") uri=achall.chall.uri(achall.domain),
command=command))
self._notify_and_wait(self.MESSAGE_TEMPLATE.format(
validation=validation, response=response,
uri=achall.chall.uri(achall.domain),
command=command))
if not response.simple_verify( if not response.simple_verify(
achall.chall, achall.domain, achall.chall, achall.domain,
@@ -183,15 +204,30 @@ s.serve_forever()" """
return response return response
def _notify_and_wait(self, message): # pylint: disable=no-self-use def _perform_dns01_challenge(self, achall):
# TODO: IDisplay wraps messages, breaking the command response, validation = achall.response_and_validation()
#answer = zope.component.getUtility(interfaces.IDisplay).notification( if not self.conf("test-mode"):
# message=message, height=25, pause=True) self._notify_and_wait(
sys.stdout.write(message) self._get_message(achall).format(
six.moves.input("Press ENTER to continue") validation=validation,
domain=achall.validation_domain_name(achall.domain),
response=response))
def cleanup(self, achalls): try:
# pylint: disable=missing-docstring,no-self-use,unused-argument 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"): if self.conf("test-mode"):
assert self._httpd is not None, ( assert self._httpd is not None, (
"cleanup() must be called after perform()") "cleanup() must be called after perform()")
@@ -202,3 +238,25 @@ s.serve_forever()" """
logger.debug("Manual command process already terminated " logger.debug("Manual command process already terminated "
"with %s code", self._httpd.returncode) "with %s code", self._httpd.returncode)
shutil.rmtree(self._root) 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, "")
+33 -10
View File
@@ -5,6 +5,7 @@ import unittest
import mock import mock
from acme import challenges from acme import challenges
from acme import errors as acme_errors
from acme import jose from acme import jose
from certbot import achallenges from certbot import achallenges
@@ -26,8 +27,13 @@ class AuthenticatorTest(unittest.TestCase):
http01_port=8080, manual_test_mode=False, http01_port=8080, manual_test_mode=False,
manual_public_ip_logging_ok=False, noninteractive_mode=True) manual_public_ip_logging_ok=False, noninteractive_mode=True)
self.auth = Authenticator(config=self.config, name="manual") 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( config_test_mode = mock.MagicMock(
http01_port=8080, manual_test_mode=True, noninteractive_mode=True) 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) self.assertTrue(all(issubclass(pref, challenges.Challenge)
for pref in self.auth.get_chall_pref("foo.com"))) 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([])) self.assertEqual([], self.auth.perform([]))
@mock.patch("certbot.plugins.manual.zope.component.getUtility") @mock.patch("certbot.plugins.manual.zope.component.getUtility")
@@ -56,19 +64,34 @@ class AuthenticatorTest(unittest.TestCase):
mock_verify.return_value = True mock_verify.return_value = True
mock_interaction().yesno.return_value = True mock_interaction().yesno.return_value = True
resp = self.achalls[0].response(KEY) resp_http = self.http01.response(KEY)
self.assertEqual([resp], self.auth.perform(self.achalls)) resp_dns = self.dns01.response(KEY)
self.assertEqual(1, mock_raw_input.call_count)
self.assertEqual([resp_http, resp_dns], self.auth.perform(self.achalls))
self.assertEqual(2, mock_raw_input.call_count)
mock_verify.assert_called_with( 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] 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 mock_verify.return_value = False
with mock.patch("certbot.plugins.manual.logger") as mock_logger: with mock.patch("certbot.plugins.manual.logger") as mock_logger:
self.auth.perform(self.achalls) 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.zope.component.getUtility")
@mock.patch("certbot.plugins.manual.Authenticator._notify_and_wait") @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) @mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True)
def test_perform_test_command_oserror(self, mock_popen): def test_perform_test_command_oserror(self, mock_popen):
mock_popen.side_effect = OSError 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.socket.socket")
@mock.patch("certbot.plugins.manual.time.sleep", autospec=True) @mock.patch("certbot.plugins.manual.time.sleep", autospec=True)
+8 -1
View File
@@ -3,6 +3,7 @@ import argparse
import collections import collections
import logging import logging
import socket import socket
import sys
import threading import threading
import OpenSSL import OpenSSL
@@ -12,6 +13,7 @@ import zope.interface
from acme import challenges from acme import challenges
from acme import standalone as acme_standalone from acme import standalone as acme_standalone
from certbot import cli
from certbot import errors from certbot import errors
from certbot import interfaces from certbot import interfaces
@@ -119,6 +121,11 @@ def supported_challenges_validator(data):
It should be passed as `type` argument to `add_argument`. 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(",") challs = data.split(",")
# tls-sni-01 was dvsni during private beta # tls-sni-01 was dvsni during private beta
@@ -177,7 +184,7 @@ class Authenticator(common.Plugin):
@classmethod @classmethod
def add_parser_arguments(cls, add): def add_parser_arguments(cls, add):
add("supported-challenges", add("supported-challenges",
help="Supported challenges. Preferred in the order they are listed.", help=argparse.SUPPRESS,
type=supported_challenges_validator, type=supported_challenges_validator,
default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES)) default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES))
+16 -1
View File
@@ -67,10 +67,25 @@ class ServerManagerTest(unittest.TestCase):
class SupportedChallengesValidatorTest(unittest.TestCase): class SupportedChallengesValidatorTest(unittest.TestCase):
"""Tests for plugins.standalone.supported_challenges_validator.""" """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): def _call(self, data):
from certbot.plugins.standalone import ( from certbot.plugins.standalone import (
supported_challenges_validator) 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): def test_correct(self):
self.assertEqual("tls-sni-01", self._call("tls-sni-01")) self.assertEqual("tls-sni-01", self._call("tls-sni-01"))
+4 -4
View File
@@ -17,9 +17,9 @@ HTTP01 = challenges.HTTP01(
token=b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") token=b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA")
TLSSNI01 = challenges.TLSSNI01( TLSSNI01 = challenges.TLSSNI01(
token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) 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): def gen_combos(challbs):
@@ -45,9 +45,9 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name
# Pending ChallengeBody objects # Pending ChallengeBody objects
TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING) TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING)
HTTP01_P = chall_to_challb(HTTP01, 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): def gen_authzr(authz_status, domain, challs, statuses, combos=True):
+30 -7
View File
@@ -24,7 +24,7 @@ class ChallengeFactoryTest(unittest.TestCase):
from certbot.auth_handler import AuthHandler from certbot.auth_handler import AuthHandler
# Account is mocked... # 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.dom = "test"
self.handler.authzr[self.dom] = acme_util.gen_authzr( 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.mock_net = mock.MagicMock(spec=acme_client.Client)
self.handler = AuthHandler( 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) logging.disable(logging.CRITICAL)
@@ -111,7 +111,7 @@ class GetAuthorizationsTest(unittest.TestCase):
mock_poll.side_effect = self._validate_all 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.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"]) authzr = self.handler.get_authorizations(["0"])
@@ -125,7 +125,7 @@ class GetAuthorizationsTest(unittest.TestCase):
self.assertEqual(self.mock_auth.cleanup.call_count, 1) self.assertEqual(self.mock_auth.cleanup.call_count, 1)
# Test if list first element is TLSSNI01, use typ because it is an achall # 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]: 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 # Length of authorizations list
self.assertEqual(len(authzr), 1) self.assertEqual(len(authzr), 1)
@@ -167,6 +167,29 @@ class GetAuthorizationsTest(unittest.TestCase):
def test_no_domains(self): def test_no_domains(self):
self.assertRaises(errors.AuthorizationError, self.handler.get_authorizations, []) 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): def _validate_all(self, unused_1, unused_2):
for dom in six.iterkeys(self.handler.authzr): for dom in six.iterkeys(self.handler.authzr):
azr = self.handler.authzr[dom] azr = self.handler.authzr[dom]
@@ -189,7 +212,7 @@ class PollChallengesTest(unittest.TestCase):
# Account and network are mocked... # Account and network are mocked...
self.mock_net = mock.MagicMock() self.mock_net = mock.MagicMock()
self.handler = AuthHandler( 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.doms = ["0", "1", "2"]
self.handler.authzr[self.doms[0]] = acme_util.gen_authzr( 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 from certbot.auth_handler import challb_to_achall
self.mock_net.poll.side_effect = self._mock_poll_solve_one_valid self.mock_net.poll.side_effect = self._mock_poll_solve_one_valid
self.chall_update[self.doms[0]].append( 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( self.assertRaises(
errors.AuthorizationError, self.handler._poll_challenges, errors.AuthorizationError, self.handler._poll_challenges,
self.chall_update, False) self.chall_update, False)
@@ -342,7 +365,7 @@ class GenChallengePathTest(unittest.TestCase):
self.assertTrue(self._call(challbs[::-1], prefs, None)) self.assertTrue(self._call(challbs[::-1], prefs, None))
def test_not_supported(self): 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] prefs = [challenges.TLSSNI01]
combos = ((0, 1),) combos = ((0, 1),)
+12
View File
@@ -423,6 +423,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
namespace = parse(long_args) namespace = parse(long_args)
self.assertEqual(namespace.domains, ['example.com', 'another.net']) 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): def test_server_flag(self):
parse = self._get_argument_parser() parse = self._get_argument_parser()
namespace = parse('--server example.com'.split()) namespace = parse('--server example.com'.split())
+2 -2
View File
@@ -16,12 +16,12 @@ class FaiiledChallengesTest(unittest.TestCase):
from certbot.errors import FailedChallenges from certbot.errors import FailedChallenges
self.error = FailedChallenges(set([achallenges.DNS( self.error = FailedChallenges(set([achallenges.DNS(
domain="example.com", challb=messages.ChallengeBody( 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")))])) error=messages.Error(typ="tls", detail="detail")))]))
def test_str(self): def test_str(self):
self.assertTrue(str(self.error).startswith( 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): class StandaloneBindErrorTest(unittest.TestCase):
+40 -28
View File
@@ -60,7 +60,7 @@ an alternate method fo install ``certbot``.
Certbot-Auto 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 from your web server OS and putting others in a python virtual environment. You can
download and run it as follows:: 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. The ``certbot-auto`` command updates to the latest client release automatically.
Since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly Since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly
the same command line flags and arguments. For more information, see 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>`_. `Certbot command-line options <https://certbot.eff.org/docs/using.html#command-line-options>`_.
Running with Docker 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 certificates or configure your webserver, because our installer
plugins cannot reach your webserver from inside the Docker container. plugins cannot reach your webserver from inside the Docker container.
Most users should use the operating system packages (see instructions at Most users should use the operating system packages (see instructions at
certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only 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 use Docker if you are sure you know what you are doing and have a
good reason to do so. good reason to do so.
@@ -113,12 +113,12 @@ to, `install Docker`_, then issue the following command:
quay.io/letsencrypt/letsencrypt:latest certonly quay.io/letsencrypt/letsencrypt:latest certonly
Running Certbot with the ``certonly`` command will obtain a certificate and place it in the directory 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 within Docker, you must install the certificate manually according to the procedure
recommended by the provider of your webserver. recommended by the provider of your webserver.
For more information about the layout For more information about the layout
of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`.
.. _Docker: https://docker.com .. _Docker: https://docker.com
.. _`install Docker`: https://docs.docker.com/userguide/ .. _`install Docker`: https://docs.docker.com/userguide/
@@ -242,8 +242,8 @@ whole process is described in the :doc:`contributing`.
.. _plugins: .. _plugins:
Getting certificates Getting certificates (and chosing plugins)
==================== ==========================================
The Certbot client supports a number of different "plugins" that can be The Certbot client supports a number of different "plugins" that can be
used to obtain and/or install certificates. 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 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 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 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 obtained certificate. If you specify multiple domains to authenticate, they will
all be listed in a single certificate. To obtain multiple seperate certificates all be listed in a single certificate. To obtain multiple seperate certificates
you will need to run Certbot multiple times. 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 "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 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 when no command is specified. The "run" subcommand can also be used to specify
a combination of distinct authenticator and installer plugins. a combination of distinct authenticator and installer plugins.
=========== ==== ==== =============================================================== =========== ==== ==== =============================================================== =============================
Plugin Auth Inst Notes Plugin Auth Inst Notes Challenge types (and port)
=========== ==== ==== =============================================================== =========== ==== ==== =============================================================== =============================
apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on 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+. | Debian-based distributions with ``libaugeas0`` 1.0+.
webroot_ Y N Obtains a cert by writing to the webroot directory of an webroot_ Y N | Obtains a cert by writing to the webroot directory of an http-01_ (80)
already running webserver. | already running webserver.
standalone_ Y N Uses a "standalone" webserver to obtain a cert. Requires 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 | 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 | with no webserver, or when direct integration with the local
webserver is not supported or not desired. | webserver is not supported or not desired.
manual_ Y N Helps you obtain a cert by giving you instructions to perform manual_ Y N | Helps you obtain a cert by giving you instructions to perform http-01_ (80) or
domain validation yourself. | domain validation yourself. dns-01_ (53)
nginx_ Y Y Very experimental and not included in certbot-auto_. 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 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. 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 Apache
------ ------