diff --git a/letsencrypt/client/achallenges.py b/letsencrypt/client/achallenges.py index cc7c322fe..7bb548dfc 100644 --- a/letsencrypt/client/achallenges.py +++ b/letsencrypt/client/achallenges.py @@ -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 diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index 8e5020dc2..f5825370f 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -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 diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 2fcb45d40..70b0796a1 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -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. diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index c2f535096..4242bb0a6 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -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. diff --git a/letsencrypt/client/tests/network2_test.py b/letsencrypt/client/tests/network2_test.py index c2a7d877a..9bed09ff1 100644 --- a/letsencrypt/client/tests/network2_test.py +++ b/letsencrypt/client/tests/network2_test.py @@ -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