mirror of
https://github.com/certbot/certbot.git
synced 2026-07-28 16:54:59 +02:00
Towards interoperability
This commit is contained in:
@@ -29,7 +29,7 @@ class AnnotatedChallenge(jose_util.ImmutableMap):
|
||||
"""Client annotated challenge.
|
||||
|
||||
Wraps around :class:`~letsencrypt.acme.challenges.Challenge` and
|
||||
annotates with data usfeul for the client.
|
||||
annotates with data useful for the client.
|
||||
|
||||
"""
|
||||
acme_type = NotImplemented
|
||||
|
||||
@@ -6,7 +6,7 @@ import Crypto.PublicKey.RSA
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme import messages
|
||||
from letsencrypt.acme import messages2
|
||||
|
||||
from letsencrypt.client import achallenges
|
||||
from letsencrypt.client import constants
|
||||
@@ -26,59 +26,39 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
:ivar network: Network object for sending and receiving authorization
|
||||
messages
|
||||
:type network: :class:`letsencrypt.client.network.Network`
|
||||
:type network: :class:`letsencrypt.client.network2.Network`
|
||||
|
||||
:ivar list domains: list of str domains to get authorization
|
||||
:ivar dict authkey: Authorized Keys for each domain.
|
||||
values are of type :class:`letsencrypt.client.le_util.Key`
|
||||
:ivar dict responses: keys: domain, values: list of responses
|
||||
(:class:`letsencrypt.acme.challenges.ChallengeResponse`.
|
||||
:ivar dict msgs: ACME Challenge messages with domain as a key.
|
||||
:ivar dict paths: optimal path for authorization. eg. paths[domain]
|
||||
:ivar dict dv_c: Keys - domain, Values are DV challenges in the form of
|
||||
:ivar dict authzr: ACME Challenge messages with domain as a key.
|
||||
:ivar list dv_c: Keys - DV challenges in the form of
|
||||
:class:`letsencrypt.client.achallenges.Indexed`
|
||||
:ivar dict cont_c: Keys - domain, Values are Continuity challenges in the
|
||||
:ivar list cont_c: Keys - Continuity challenges in the
|
||||
form of :class:`letsencrypt.client.achallenges.Indexed`
|
||||
|
||||
"""
|
||||
def __init__(self, dv_auth, cont_auth, network):
|
||||
def __init__(self, dv_auth, cont_auth, network, authkey):
|
||||
self.dv_auth = dv_auth
|
||||
self.cont_auth = cont_auth
|
||||
self.network = network
|
||||
|
||||
self.domains = []
|
||||
self.authkey = dict()
|
||||
self.responses = dict()
|
||||
self.msgs = dict()
|
||||
self.paths = dict()
|
||||
self.authkey = authkey
|
||||
self.authzr = dict()
|
||||
|
||||
self.dv_c = dict()
|
||||
self.cont_c = dict()
|
||||
self.dv_c = []
|
||||
self.cont_c = []
|
||||
|
||||
def add_chall_msg(self, domain, msg, authkey):
|
||||
"""Add a challenge message to the AuthHandler.
|
||||
|
||||
:param str domain: domain for authorization
|
||||
|
||||
:param msg: ACME "challenge" message
|
||||
:type msg: :class:`letsencrypt.acme.message.Challenge`
|
||||
|
||||
:param authkey: authorized key for the challenge
|
||||
:type authkey: :class:`letsencrypt.client.le_util.Key`
|
||||
|
||||
"""
|
||||
if domain in self.domains:
|
||||
raise errors.LetsEncryptAuthHandlerError(
|
||||
"Multiple ACMEChallengeMessages for the same domain "
|
||||
"is not supported.")
|
||||
self.domains.append(domain)
|
||||
self.responses[domain] = [None] * len(msg.challenges)
|
||||
self.msgs[domain] = msg
|
||||
self.authkey[domain] = authkey
|
||||
|
||||
def get_authorizations(self):
|
||||
def get_authorizations(self, domains):
|
||||
"""Retrieve all authorizations for challenges.
|
||||
|
||||
:param set domains: Domains for authorization
|
||||
|
||||
:returns: tuple of lists of authorization resources. Takes the form of
|
||||
(`completed`, `failed`)
|
||||
rtype: tuple
|
||||
|
||||
:raises LetsEncryptAuthHandlerError: If unable to retrieve all
|
||||
authorizations
|
||||
|
||||
@@ -143,31 +123,23 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
"""
|
||||
logging.info("Performing the following challenges:")
|
||||
for dom in self.domains:
|
||||
self.paths[dom] = gen_challenge_path(
|
||||
self.msgs[dom].challenges,
|
||||
path = gen_challenge_path(
|
||||
self.authzr[dom].challenges,
|
||||
self._get_chall_pref(dom),
|
||||
self.msgs[dom].combinations)
|
||||
self.authzr[dom].combinations)
|
||||
|
||||
self.dv_c[dom], self.cont_c[dom] = self._challenge_factory(
|
||||
dom, self.paths[dom])
|
||||
|
||||
# Flatten challs for authenticator functions and remove index
|
||||
# Order is important here as we will not expose the outside
|
||||
# Authenticator to our own indices.
|
||||
flat_cont = []
|
||||
flat_dv = []
|
||||
|
||||
for dom in self.domains:
|
||||
flat_cont.extend(ichall.achall for ichall in self.cont_c[dom])
|
||||
flat_dv.extend(ichall.achall for ichall in self.dv_c[dom])
|
||||
dom_dv_c, dom_cont_c = self._challenge_factory(
|
||||
dom, path)
|
||||
self.dv_c.extend(dom_dv_c)
|
||||
self.cont_c.extend(dom_cont_c)
|
||||
|
||||
cont_resp = []
|
||||
dv_resp = []
|
||||
try:
|
||||
if flat_cont:
|
||||
cont_resp = self.cont_auth.perform(flat_cont)
|
||||
if flat_dv:
|
||||
dv_resp = self.dv_auth.perform(flat_dv)
|
||||
if self.cont_c:
|
||||
cont_resp = self.cont_auth.perform(self.cont_c)
|
||||
if self.dv_c:
|
||||
dv_resp = self.dv_auth.perform(self.dv_c)
|
||||
# This will catch both specific types of errors.
|
||||
except errors.LetsEncryptAuthHandlerError as err:
|
||||
logging.critical("Failure in setting up challenges:")
|
||||
@@ -179,33 +151,29 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
raise errors.LetsEncryptAuthHandlerError(
|
||||
"Unable to perform challenges")
|
||||
|
||||
assert len(cont_resp) == len(self.cont_c)
|
||||
assert len(dv_resp) == len(self.dv_c)
|
||||
|
||||
logging.info("Ready for verification...")
|
||||
|
||||
# Assemble Responses
|
||||
if cont_resp:
|
||||
self._assign_responses(cont_resp, self.cont_c)
|
||||
if dv_resp:
|
||||
self._assign_responses(dv_resp, self.dv_c)
|
||||
# Send all Responses
|
||||
self._respond(cont_resp, dv_resp)
|
||||
|
||||
def _assign_responses(self, flat_list, ichall_dict):
|
||||
"""Assign responses from flat_list back to the Indexed dicts.
|
||||
def _respond(self, cont_resp, dv_resp):
|
||||
"""Send/Recieve confirmation of all challenges.
|
||||
|
||||
:param list flat_list: flat_list of responses from an IAuthenticator
|
||||
:param dict ichall_dict: Master dict mapping all domains to a list of
|
||||
their associated 'continuity' and 'dv' Indexed challenges, or their
|
||||
:class:`letsencrypt.client.achallenges.Indexed` list
|
||||
.. note:: This method also cleans up the auth_handler state.
|
||||
|
||||
"""
|
||||
flat_index = 0
|
||||
for dom in self.domains:
|
||||
for ichall in ichall_dict[dom]:
|
||||
self.responses[dom][ichall.index] = flat_list[flat_index]
|
||||
flat_index += 1
|
||||
completed = []
|
||||
for chall, resp in itertools.izip(self.cont_c, cont_resp):
|
||||
if cont_resp[i]:
|
||||
self.network.answer_challenge(self.cont_c[i], cont_resp[i])
|
||||
for i in range(len(dv_resp)):
|
||||
if dv_resp[i]:
|
||||
self.network.answer_challenge(self.dv_c[i], cont_resp[i])
|
||||
|
||||
|
||||
def _path_satisfied(self, dom):
|
||||
"""Returns whether a path has been completely satisfied."""
|
||||
# Make sure that there are no 'None' or 'False' entries along path.
|
||||
return all(self.responses[dom][i] for i in self.paths[dom])
|
||||
|
||||
def _get_chall_pref(self, domain):
|
||||
"""Return list of challenge preferences.
|
||||
@@ -218,40 +186,14 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
chall_prefs.extend(self.dv_auth.get_chall_pref(domain))
|
||||
return chall_prefs
|
||||
|
||||
def _cleanup_challenges(self, domain):
|
||||
"""Cleanup configuration challenges
|
||||
def _cleanup_challenges(self):
|
||||
"""Cleanup all configuration challenges."""
|
||||
logging.info("Cleaning up all challenges")
|
||||
|
||||
:param str domain: domain for which to clean up challenges
|
||||
|
||||
"""
|
||||
logging.info("Cleaning up challenges for %s", domain)
|
||||
# These are indexed challenges... give just the challenges to the auth
|
||||
# Chose to make these lists instead of a generator to make it easier to
|
||||
# work with...
|
||||
dv_list = [ichall.achall for ichall in self.dv_c[domain]]
|
||||
cont_list = [ichall.achall for ichall in self.cont_c[domain]]
|
||||
if dv_list:
|
||||
self.dv_auth.cleanup(dv_list)
|
||||
if cont_list:
|
||||
self.cont_auth.cleanup(cont_list)
|
||||
|
||||
def _cleanup_state(self, delete_list):
|
||||
"""Cleanup state after an authorization is received.
|
||||
|
||||
:param list delete_list: list of domains in str form
|
||||
|
||||
"""
|
||||
for domain in delete_list:
|
||||
del self.msgs[domain]
|
||||
del self.responses[domain]
|
||||
del self.paths[domain]
|
||||
|
||||
del self.authkey[domain]
|
||||
|
||||
del self.cont_c[domain]
|
||||
del self.dv_c[domain]
|
||||
|
||||
self.domains.remove(domain)
|
||||
if self.dv_c:
|
||||
self.dv_auth.cleanup(self.dv_c)
|
||||
if self.cont_c:
|
||||
self.cont_auth.cleanup(self.cont_c)
|
||||
|
||||
def _challenge_factory(self, domain, path):
|
||||
"""Construct Namedtuple Challenges
|
||||
|
||||
@@ -7,6 +7,7 @@ import Crypto.PublicKey.RSA
|
||||
import M2Crypto
|
||||
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme.jose import jwk
|
||||
from letsencrypt.acme import messages
|
||||
|
||||
from letsencrypt.client import auth_handler
|
||||
@@ -14,7 +15,7 @@ from letsencrypt.client import continuity_auth
|
||||
from letsencrypt.client import crypto_util
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import le_util
|
||||
from letsencrypt.client import network
|
||||
from letsencrypt.client import network2
|
||||
from letsencrypt.client import reverter
|
||||
from letsencrypt.client import revoker
|
||||
|
||||
@@ -27,11 +28,14 @@ class Client(object):
|
||||
"""ACME protocol client.
|
||||
|
||||
:ivar network: Network object for sending and receiving messages
|
||||
:type network: :class:`letsencrypt.client.network.Network`
|
||||
:type network: :class:`letsencrypt.client.network2.Network`
|
||||
|
||||
:ivar authkey: Authorization Key
|
||||
:type authkey: :class:`letsencrypt.client.le_util.Key`
|
||||
|
||||
:ivar reg: Registration Resource
|
||||
:type reg: :class:`letsencrypt.acme.messages2.RegistrationResource`
|
||||
|
||||
:ivar auth_handler: Object that supports the IAuthenticator interface.
|
||||
auth_handler contains both a dv_authenticator and a
|
||||
continuity_authenticator
|
||||
@@ -55,22 +59,49 @@ class Client(object):
|
||||
:type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
|
||||
"""
|
||||
self.network = network.Network(config.server)
|
||||
self.authkey = authkey
|
||||
self.regr = None
|
||||
self.installer = installer
|
||||
|
||||
# TODO: Allow for other alg types besides RS256
|
||||
self.network = network2.Network(
|
||||
config.server+"/acme/new-registration",
|
||||
jwk.JWKRSA.load(authkey.pem))
|
||||
self.config = config
|
||||
|
||||
if dv_auth is not None:
|
||||
cont_auth = continuity_auth.ContinuityAuthenticator(config)
|
||||
self.auth_handler = auth_handler.AuthHandler(
|
||||
dv_auth, cont_auth, self.network)
|
||||
dv_auth, cont_auth, self.network, self.authkey)
|
||||
else:
|
||||
self.auth_handler = None
|
||||
|
||||
def register(self, email=None, phone=None):
|
||||
"""New Registration with the ACME server.
|
||||
|
||||
:param str email: User's email address
|
||||
:param str phone: User's phone number
|
||||
|
||||
"""
|
||||
# TODO: properly format/scrub phone number
|
||||
details = (
|
||||
"mailto:" + email if email is not None else None,
|
||||
"tel:" + phone if phone is not None else None
|
||||
)
|
||||
|
||||
self.regr = self.network.register(
|
||||
tuple(detail for detail in details if detail is not None))
|
||||
|
||||
def set_regr(self, regr):
|
||||
"""Set a preexisting registration resource."""
|
||||
self.regr = regr
|
||||
|
||||
def obtain_certificate(self, domains, csr=None):
|
||||
"""Obtains a certificate from the ACME server.
|
||||
|
||||
:param str domains: list of domains to get a certificate
|
||||
:meth:`.register` must be called before :meth:`.obtain_certificate`
|
||||
|
||||
:param set domains: domains to get a certificate
|
||||
|
||||
:param csr: CSR must contain requested domains, the key used to generate
|
||||
this CSR can be different than self.authkey
|
||||
@@ -81,16 +112,20 @@ class Client(object):
|
||||
|
||||
"""
|
||||
if self.auth_handler is None:
|
||||
logging.warning("Unable to obtain a certificate, because client "
|
||||
"does not have a valid auth handler.")
|
||||
|
||||
# Request Challenges
|
||||
for name in domains:
|
||||
self.auth_handler.add_chall_msg(
|
||||
name, self.acme_challenge(name), self.authkey)
|
||||
msg = ("Unable to obtain certificate because authenticator is "
|
||||
"not set.")
|
||||
logging.warning(msg)
|
||||
raise errors.LetsEncryptClientError(msg)
|
||||
if self.regr is None:
|
||||
raise errors.LetsEncryptClientError(
|
||||
"Please register with the ACME server first.")
|
||||
|
||||
# Perform Challenges/Get Authorizations
|
||||
self.auth_handler.get_authorizations()
|
||||
if self.regr.new_authzr_uri:
|
||||
self.auth_handler.get_authorizations(domains, self.regr)
|
||||
else:
|
||||
self.auth_handler.get_authorizations(
|
||||
domains, self.config.server + "/acme/new-authorization")
|
||||
|
||||
# Create CSR from names
|
||||
if csr is None:
|
||||
@@ -330,7 +365,7 @@ def init_csr(privkey, names, cert_dir):
|
||||
:param privkey: Key to include in the CSR
|
||||
:type privkey: :class:`letsencrypt.client.le_util.Key`
|
||||
|
||||
:param list names: `str` names to include in the CSR
|
||||
:param set names: `str` names to include in the CSR
|
||||
|
||||
:param str cert_dir: Certificate save directory.
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ class Network(object):
|
||||
'contact'].default):
|
||||
"""Register.
|
||||
|
||||
:param contact: Contact list, as accpeted by `.RegistrationResource`
|
||||
:param contact: Contact list, as accepted by `.RegistrationResource`
|
||||
:type contact: `tuple`
|
||||
|
||||
:returns: Registration Resource.
|
||||
@@ -230,25 +230,24 @@ class Network(object):
|
||||
raise errors.UnexpectedUpdate(authzr)
|
||||
return authzr
|
||||
|
||||
def request_challenges(self, identifier, regr):
|
||||
def request_challenges(self, identifier, new_authzr_uri):
|
||||
"""Request challenges.
|
||||
|
||||
:param identifier: Identifier to be challenged.
|
||||
:type identifier: `.messages2.Identifier`
|
||||
|
||||
:param regr: Registration Resource.
|
||||
:type regr: `.RegistrationResource`
|
||||
:param str new_authzr_uri: new-authorization URI
|
||||
|
||||
:returns: Authorization Resource.
|
||||
:rtype: `.AuthorizationResource`
|
||||
|
||||
"""
|
||||
new_authz = messages2.Authorization(identifier=identifier)
|
||||
response = self._post(regr.new_authzr_uri, self._wrap_in_jws(new_authz))
|
||||
response = self._post(new_authzr_uri, self._wrap_in_jws(new_authz))
|
||||
assert response.status_code == httplib.CREATED # TODO: handle errors
|
||||
return self._authzr_from_response(response, identifier)
|
||||
|
||||
def request_domain_challenges(self, domain, regr):
|
||||
def request_domain_challenges(self, domain, new_authz_uri):
|
||||
"""Request challenges for domain names.
|
||||
|
||||
This is simply a convenience function that wraps around
|
||||
@@ -256,10 +255,14 @@ class Network(object):
|
||||
generic identifiers.
|
||||
|
||||
:param str domain: Domain name to be challenged.
|
||||
:param str new_authzr_uri: new-authorization URI
|
||||
|
||||
:returns: Authorization Resource.
|
||||
:rtype: `.AuthorizationResource`
|
||||
|
||||
"""
|
||||
return self.request_challenges(messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value=domain), regr)
|
||||
typ=messages2.IDENTIFIER_FQDN, value=domain), new_authz_uri)
|
||||
|
||||
def answer_challenge(self, challb, response):
|
||||
"""Answer challenge.
|
||||
@@ -289,17 +292,6 @@ class Network(object):
|
||||
raise errors.UnexpectedUpdate(challr.uri)
|
||||
return challr
|
||||
|
||||
def answer_challenges(self, challbs, responses):
|
||||
"""Answer multiple challenges.
|
||||
|
||||
.. note:: This is a convenience function to make integration
|
||||
with old proto code easier and shall probably be removed
|
||||
once restification is over.
|
||||
|
||||
"""
|
||||
return [self.answer_challenge(challb, response)
|
||||
for challb, response in itertools.izip(challbs, responses)]
|
||||
|
||||
@classmethod
|
||||
def retry_after(cls, response, default):
|
||||
"""Compute next `poll` time based on response ``Retry-After`` header.
|
||||
|
||||
@@ -217,7 +217,7 @@ class NetworkTest(unittest.TestCase):
|
||||
}
|
||||
|
||||
self._mock_post_get()
|
||||
self.net.request_challenges(self.identifier, self.regr)
|
||||
self.net.request_challenges(self.identifier, self.authzr.uri)
|
||||
# TODO: test POST call arguments
|
||||
|
||||
# TODO: split here and separate test
|
||||
|
||||
Reference in New Issue
Block a user