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
commit 1584ee8ac6
15 changed files with 292 additions and 92 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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