Merge branch 'master' into dselect

This commit is contained in:
Brad Warren 2016-03-23 15:35:16 -07:00
commit 0e4d9b00cd
7 changed files with 96 additions and 21 deletions

View file

@ -18,16 +18,17 @@ The Let's Encrypt Client is a fully-featured, extensible client for the Let's
Encrypt CA (or any other CA that speaks the `ACME
<https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md>`_
protocol) that can automate the tasks of obtaining certificates and
configuring webservers to use them.
configuring webservers to use them. This client runs on Unix-based operating
systems.
Installation
------------
If ``letsencrypt`` is packaged for your OS, you can install it from there, and
run it by typing ``letsencrypt``. Because not all operating systems have
packages yet, we provide a temporary solution via the ``letsencrypt-auto``
wrapper script, which obtains some dependencies from your OS and puts others
in a python virtual environment::
If ``letsencrypt`` is packaged for your Unix OS, you can install it from
there, and run it by typing ``letsencrypt``. Because not all operating
systems have packages yet, we provide a temporary solution via the
``letsencrypt-auto`` wrapper script, which obtains some dependencies
from your OS and puts others in a python virtual environment::
user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt
user@webserver:~$ cd letsencrypt

View file

@ -52,9 +52,8 @@ class AuthHandler(object):
:param bool best_effort: Whether or not all authorizations are
required (this is useful in renewal)
:returns: tuple of lists of authorization resources. Takes the
form of (`completed`, `failed`)
:rtype: tuple
:returns: List of authorization resources
:rtype: list
:raises .AuthorizationError: If unable to retrieve all
authorizations
@ -76,9 +75,16 @@ class AuthHandler(object):
# Just make sure all decisions are complete.
self.verify_authzr_complete()
# Only return valid authorizations
return [authzr for authzr in self.authzr.values()
if authzr.body.status == messages.STATUS_VALID]
retVal = [authzr for authzr in self.authzr.values()
if authzr.body.status == messages.STATUS_VALID]
if not retVal:
raise errors.AuthorizationError(
"Challenges failed for all domains")
return retVal
def _choose_challenges(self, domains):
"""Retrieve necessary challenges to satisfy server."""
@ -175,9 +181,11 @@ class AuthHandler(object):
chall_update[domain].remove(achall)
# We failed some challenges... damage control
else:
# Right now... just assume a loss and carry on...
if best_effort:
comp_domains.add(domain)
logger.warning(
"Challenge failed for domain %s",
domain)
else:
all_failed_achalls.update(
updated for _, updated in failed_achalls)

View file

@ -714,6 +714,9 @@ class HelpfulArgumentParser(object):
parsed_args.register_unsafely_without_email = True
if parsed_args.csr:
if parsed_args.allow_subset_of_names:
raise errors.Error("--allow-subset-of-names "
"cannot be used with --csr")
self.handle_csr(parsed_args)
if self.detect_defaults: # plumbing
@ -1090,6 +1093,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False):
"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(
"automation", "--allow-subset-of-names",
action="store_true",
help="When performing domain validation, do not consider it a failure "
"if authorizations can not be obtained for a strict subset of "
"the requested domains. This option cannot be used with --csr.")
helpful.add_group(
"renew", description="The 'renew' subcommand will attempt to renew all"

View file

@ -188,7 +188,7 @@ class Client(object):
self.auth_handler = None
def obtain_certificate_from_csr(self, domains, csr,
typ=OpenSSL.crypto.FILETYPE_ASN1):
typ=OpenSSL.crypto.FILETYPE_ASN1, authzr=None):
"""Obtain certificate.
Internal function with precondition that `domains` are
@ -198,6 +198,8 @@ class Client(object):
:param .le_util.CSR csr: DER-encoded Certificate Signing
Request. The key used to generate this CSR can be different
than `authkey`.
:param list authzr: List of
:class:`acme.messages.AuthorizationResource`
:returns: `.CertificateResource` and certificate chain (as
returned by `.fetch_chain`).
@ -214,14 +216,15 @@ class Client(object):
logger.debug("CSR: %s, domains: %s", csr, domains)
authzr = self.auth_handler.get_authorizations(domains)
if authzr is None:
authzr = self.auth_handler.get_authorizations(domains)
certr = self.acme.request_issuance(
jose.ComparableX509(
OpenSSL.crypto.load_certificate_request(typ, csr.data)),
authzr)
authzr)
return certr, self.acme.fetch_chain(certr)
def obtain_certificate(self, domains):
"""Obtains a certificate from the ACME server.
@ -236,12 +239,20 @@ class Client(object):
:rtype: tuple
"""
authzr = self.auth_handler.get_authorizations(
domains,
self.config.allow_subset_of_names)
domains = [a.body.identifier.value.encode('ascii')
for a in authzr]
# Create CSR from names
key = crypto_util.init_save_key(
self.config.rsa_key_size, self.config.key_dir)
csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
return self.obtain_certificate_from_csr(domains, csr) + (key, csr)
return (self.obtain_certificate_from_csr(domains, csr, authzr=authzr)
+ (key, csr))
def obtain_and_enroll_certificate(self, domains):
"""Obtain and enroll certificate.

View file

@ -126,6 +126,7 @@ class GetAuthorizationsTest(unittest.TestCase):
for achall in self.mock_auth.cleanup.call_args[0][0]:
self.assertTrue(achall.typ in ["tls-sni-01", "http-01", "dns"])
# Length of authorizations list
self.assertEqual(len(authzr), 1)
@mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges")
@ -162,6 +163,9 @@ class GetAuthorizationsTest(unittest.TestCase):
self.assertRaises(
errors.AuthorizationError, self.handler.get_authorizations, ["0"])
def test_no_domains(self):
self.assertRaises(errors.AuthorizationError, self.handler.get_authorizations, [])
def _validate_all(self, unused_1, unused_2):
for dom in self.handler.authzr.keys():
azr = self.handler.authzr[dom]

View file

@ -364,6 +364,10 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self._call,
['-d', '204.11.231.35'])
def test_csr_with_besteffort(self):
args = ["--csr", CSR, "--allow-subset-of-names"]
self.assertRaises(errors.Error, self._call, args)
def test_run_with_csr(self):
# This is an error because you can only use --csr with certonly
try:

View file

@ -96,7 +96,7 @@ class ClientTest(unittest.TestCase):
def setUp(self):
self.config = mock.MagicMock(
no_verify_ssl=False, config_dir="/etc/letsencrypt")
no_verify_ssl=False, config_dir="/etc/letsencrypt", allow_subset_of_names=False)
# pylint: disable=star-args
self.account = mock.MagicMock(**{"key.pem": KEY})
self.eg_domains = ["example.com", "www.example.com"]
@ -115,15 +115,22 @@ class ClientTest(unittest.TestCase):
def _mock_obtain_certificate(self):
self.client.auth_handler = mock.MagicMock()
self.client.auth_handler.get_authorizations.return_value = [None]
self.acme.request_issuance.return_value = mock.sentinel.certr
self.acme.fetch_chain.return_value = mock.sentinel.chain
def _check_obtain_certificate(self):
self.client.auth_handler.get_authorizations.assert_called_once_with(self.eg_domains)
self.client.auth_handler.get_authorizations.assert_called_once_with(
self.eg_domains,
self.config.allow_subset_of_names)
authzr = self.client.auth_handler.get_authorizations()
self.acme.request_issuance.assert_called_once_with(
jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)),
self.client.auth_handler.get_authorizations())
authzr)
self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr)
# FIXME move parts of this to test_cli.py...
@ -151,12 +158,28 @@ class ClientTest(unittest.TestCase):
self.assertRaises(errors.ConfigurationError,
cli.HelpfulArgumentParser.handle_csr, mock_parser, mock_parsed_args)
authzr = self.client.auth_handler.get_authorizations(self.eg_domains, False)
self.assertEqual(
(mock.sentinel.certr, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(self.eg_domains, test_csr))
self.client.obtain_certificate_from_csr(
self.eg_domains,
test_csr,
authzr=authzr))
# and that the cert was obtained correctly
self._check_obtain_certificate()
# Test for authzr=None
self.assertEqual(
(mock.sentinel.certr, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(
self.eg_domains,
test_csr,
authzr=None))
self.client.auth_handler.get_authorizations.assert_called_with(
self.eg_domains)
# Test for no auth_handler
self.client.auth_handler = None
self.assertRaises(
@ -175,6 +198,21 @@ class ClientTest(unittest.TestCase):
mock_crypto_util.init_save_key.return_value = mock.sentinel.key
domains = ["example.com", "www.example.com"]
# return_value is essentially set to (None, None) in
# _mock_obtain_certificate(), which breaks this test.
# Thus fixed by the next line.
authzr = []
for domain in domains:
authzr.append(
mock.MagicMock(
body=mock.MagicMock(
identifier=mock.MagicMock(
value=domain))))
self.client.auth_handler.get_authorizations.return_value = authzr
self.assertEqual(
self.client.obtain_certificate(domains),
(mock.sentinel.certr, mock.sentinel.chain, mock.sentinel.key, csr))