From 458a61a177e39057377cdce82c9401a197250539 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 14 Apr 2015 06:44:57 +0000 Subject: [PATCH 1/4] Revert to ChallengeResource/ChallengeBody/Challenge triplet --- letsencrypt/acme/challenges.py | 42 ++++++++++++---------------------- letsencrypt/acme/messages2.py | 31 ++++++++++++++++--------- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/letsencrypt/acme/challenges.py b/letsencrypt/acme/challenges.py index cb8badc91..7a51d7447 100644 --- a/letsencrypt/acme/challenges.py +++ b/letsencrypt/acme/challenges.py @@ -5,20 +5,24 @@ import hashlib import Crypto.Random -from letsencrypt.acme import fields from letsencrypt.acme import jose -from letsencrypt.acme import messages2 from letsencrypt.acme import other # pylint: disable=too-few-public-methods -class ContinuityChallenge(messages2.Challenge): # pylint: disable=abstract-method +class Challenge(jose.TypedJSONObjectWithFields): + # _fields_to_json | pylint: disable=abstract-method + """ACME challenge.""" + TYPES = {} + + +class ContinuityChallenge(Challenge): # pylint: disable=abstract-method """Client validation challenges.""" -class DVChallenge(messages2.Challenge): # pylint: disable=abstract-method +class DVChallenge(Challenge): # pylint: disable=abstract-method """Domain validation challenges.""" @@ -37,7 +41,7 @@ class ChallengeResponse(jose.TypedJSONObjectWithFields): return super(ChallengeResponse, cls).from_json(jobj) -@messages2.Challenge.register +@Challenge.register class SimpleHTTPS(DVChallenge): """ACME "simpleHttps" challenge.""" typ = "simpleHttps" @@ -65,7 +69,7 @@ class SimpleHTTPSResponse(ChallengeResponse): return self.URI_TEMPLATE.format(domain=domain, path=self.path) -@messages2.Challenge.register +@Challenge.register class DVSNI(DVChallenge): """ACME "dvsni" challenge. @@ -89,9 +93,6 @@ class DVSNI(DVChallenge): nonce = jose.Field("nonce", encoder=binascii.hexlify, decoder=functools.partial(functools.partial( jose.decode_hex16, size=NONCE_SIZE))) - uri = jose.Field('uri') - status = jose.Field('status', decoder=messages2.Status.from_json) - validated = fields.RFC3339Field('validated', omitempty=True) @property def nonce_domain(self): @@ -137,7 +138,7 @@ class DVSNIResponse(ChallengeResponse): """Domain name for certificate subjectAltName.""" return self.z(chall) + self.DOMAIN_SUFFIX -@messages2.Challenge.register +@Challenge.register class RecoveryContact(ContinuityChallenge): """ACME "recoveryContact" challenge.""" typ = "recoveryContact" @@ -146,10 +147,6 @@ class RecoveryContact(ContinuityChallenge): success_url = jose.Field("successURL", omitempty=True) contact = jose.Field("contact", omitempty=True) - uri = jose.Field('uri') - status = jose.Field('status', decoder=messages2.Status.from_json) - validated = fields.RFC3339Field('validated', omitempty=True) - @ChallengeResponse.register class RecoveryContactResponse(ChallengeResponse): @@ -158,15 +155,11 @@ class RecoveryContactResponse(ChallengeResponse): token = jose.Field("token", omitempty=True) -@messages2.Challenge.register +@Challenge.register class RecoveryToken(ContinuityChallenge): """ACME "recoveryToken" challenge.""" typ = "recoveryToken" - uri = jose.Field('uri') - status = jose.Field('status', decoder=messages2.Status.from_json) - validated = fields.RFC3339Field('validated', omitempty=True) - @ChallengeResponse.register class RecoveryTokenResponse(ChallengeResponse): @@ -175,7 +168,7 @@ class RecoveryTokenResponse(ChallengeResponse): token = jose.Field("token", omitempty=True) -@messages2.Challenge.register +@Challenge.register class ProofOfPossession(ContinuityChallenge): """ACME "proofOfPossession" challenge. @@ -187,10 +180,6 @@ class ProofOfPossession(ContinuityChallenge): NONCE_SIZE = 16 - uri = jose.Field('uri') - status = jose.Field('status', decoder=messages2.Status.from_json) - validated = fields.RFC3339Field('validated', omitempty=True) - class Hints(jose.JSONObjectWithFields): """Hints for "proofOfPossession" challenge. @@ -247,15 +236,12 @@ class ProofOfPossessionResponse(ChallengeResponse): return self.signature.verify(self.nonce) -@messages2.Challenge.register +@Challenge.register class DNS(DVChallenge): """ACME "dns" challenge.""" typ = "dns" token = jose.Field("token") - uri = jose.Field('uri') - status = jose.Field('status', decoder=messages2.Status.from_json) - validated = fields.RFC3339Field('validated', omitempty=True) @ChallengeResponse.register class DNSResponse(ChallengeResponse): diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 4d4919255..116f2fe94 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -1,4 +1,5 @@ """ACME protocol v02 messages.""" +from letsencrypt.acme import challenges from letsencrypt.acme import fields from letsencrypt.acme import jose @@ -110,12 +111,8 @@ class Resource(jose.ImmutableMap): __slots__ = ('body', 'uri') -class TypedResourceBody(jose.TypedJSONObjectWithFields): - """ACME Resource Body with type.""" - - class ResourceBody(jose.JSONObjectWithFields): - """ACME Resource Body""" + """ACME Resource Body.""" class RegistrationResource(Resource): @@ -148,7 +145,7 @@ class Registration(ResourceBody): class ChallengeResource(Resource, jose.JSONObjectWithFields): """Challenge Resource. - :ivar letsencrypt.acme.messages2.Challenge body: + :ivar letsencrypt.acme.messages2.ChallengeBody body: :ivar str authzr_uri: URI found in the 'up' ``Link`` header. """ @@ -161,22 +158,34 @@ class ChallengeResource(Resource, jose.JSONObjectWithFields): return self.body.uri -class Challenge(TypedResourceBody): +class ChallengeBody(ResourceBody): """Challenge Resource Body. + .. todo:: + Confusingly, this has a similar name to `.challenges.Challenge`, + as well as `.achallenges.AnnotateChallenge`. Please use names + such as ``challb`` to distinguish instanced of this class from + ``achall``. + :ivar letsencrypt.acme.messages2.Status status: :ivar datetime.datetime validated: """ - TYPES = {} + __slots__ = ('chall',) uri = jose.Field('uri') status = jose.Field('status', decoder=Status.from_json) validated = fields.RFC3339Field('validated', omitempty=True) def to_json(self): - jobj = super(Challenge, self).to_json() + jobj = super(ChallengeBody, self).to_json() + jobj.update(self.chall.to_json()) return jobj + @classmethod + def fields_from_json(cls, jobj): + jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj) + jobj_fields['chall'] = challenges.Challenge.from_json(jobj) + return jobj_fields class AuthorizationResource(Resource): @@ -193,7 +202,7 @@ class Authorization(ResourceBody): """Authorization Resource Body. :ivar letsencrypt.acme.messages2.Identifier identifier: - :ivar list challenges: `list` of `.Challenge` + :ivar list challenges: `list` of `.ChallengeBody` :ivar tuple combinations: Challenge combinations (`tuple` of `tuple` of `int`, as opposed to `list` of `list` from the spec). :ivar letsencrypt.acme.jose.jwk.JWK key: Public key. @@ -220,7 +229,7 @@ class Authorization(ResourceBody): @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(Challenge.from_json(chall) for chall in value) + return tuple(ChallengeBody.from_json(chall) for chall in value) @property def resolved_combinations(self): From 5298d8123d3a6302ac9af71d156548097de6e060 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 14 Apr 2015 13:05:01 +0000 Subject: [PATCH 2/4] ChallengeBody __getattr__ proxy --- letsencrypt/acme/messages2.py | 6 ++++++ letsencrypt/acme/messages2_test.py | 3 +++ letsencrypt/client/achallenges.py | 7 +++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 116f2fe94..b04291af3 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -167,6 +167,9 @@ class ChallengeBody(ResourceBody): such as ``challb`` to distinguish instanced of this class from ``achall``. + :ivar letsencrypt.acme.challenges.Challenge: Wrapped challenge. + Conveniently, all challenge fields are proxied, i.e. you can + call ``challb.x`` to get ``challb.chall.x`` contents. :ivar letsencrypt.acme.messages2.Status status: :ivar datetime.datetime validated: @@ -187,6 +190,9 @@ class ChallengeBody(ResourceBody): jobj_fields['chall'] = challenges.Challenge.from_json(jobj) return jobj_fields + def __getattr__(self, name): + return getattr(self.chall, name) + class AuthorizationResource(Resource): """Authorization Resource. diff --git a/letsencrypt/acme/messages2_test.py b/letsencrypt/acme/messages2_test.py index 5297d6362..614895b98 100644 --- a/letsencrypt/acme/messages2_test.py +++ b/letsencrypt/acme/messages2_test.py @@ -103,6 +103,9 @@ class ChallengeBodyTest(unittest.TestCase): from letsencrypt.acme.messages2 import ChallengeBody self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from)) + def test_getattr_proxy(self): + self.assertEqual('foo', self.challb.token) + class AuthorizationTest(unittest.TestCase): """Tests for letsencrypt.acme.messages2.Authorization.""" diff --git a/letsencrypt/client/achallenges.py b/letsencrypt/client/achallenges.py index 05bd3c67d..707e9c867 100644 --- a/letsencrypt/client/achallenges.py +++ b/letsencrypt/client/achallenges.py @@ -26,10 +26,13 @@ from letsencrypt.client import crypto_util class AnnotatedChallenge(jose_util.ImmutableMap): """Client annotated challenge. - Wraps around :class:`~letsencrypt.acme.challenges.Challenge` and - annotates with data useful for the client. + Wraps around server provided challenge and annotates with data + useful for the client. + + :ivar chall: Wrapped `~.ChallengeBody` (or just `~.challenges.Challenge`). """ + __slots__ = ('chall',) acme_type = NotImplemented def __getattr__(self, name): From 1672e07b2c041b2f1d24a7b28a50183bd318366a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 14 Apr 2015 13:06:00 +0000 Subject: [PATCH 3/4] Return RegistrationResource in agree_to_tos --- letsencrypt/client/network2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 534cac14b..7a50a40bf 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -224,7 +224,7 @@ class Network(object): :rtype: `.RegistrationResource` """ - self.update_registration( + return self.update_registration( regr.update(body=regr.body.update(agreement=regr.terms_of_service))) def _authzr_from_response(self, response, identifier, From fc52600c4d4c8a12522776cd129d2b21b1d99658 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 14 Apr 2015 13:40:56 +0000 Subject: [PATCH 4/4] Adjust achallanges to be used with ChallengeBody --- letsencrypt/client/achallenges.py | 29 +++++++++------- letsencrypt/client/auth_handler.py | 54 ++++++++++++++++-------------- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/letsencrypt/client/achallenges.py b/letsencrypt/client/achallenges.py index 707e9c867..1a5cf9c8e 100644 --- a/letsencrypt/client/achallenges.py +++ b/letsencrypt/client/achallenges.py @@ -1,17 +1,20 @@ """Client annotated ACME challenges. Please use names such as ``achall`` to distiguish from variables "of type" -:class:`letsencrypt.acme.challenges.Challenge` (denoted by ``chall``):: +:class:`letsencrypt.acme.challenges.Challenge` (denoted by ``chall``) +and :class:`.ChallengeBody` (denoted by ``challb``):: from letsencrypt.acme import challenges + from letsencrypt.acme import messages2 from letsencrypt.client import achallenges chall = challenges.DNS(token='foo') - achall = achallenges.DNS(chall=chall, domain='example.com') + challb = messages2.ChallengeBody(chall=chall) + achall = achallenges.DNS(chall=challb, domain='example.com') Note, that all annotated challenges act as a proxy objects:: - achall.token == chall.token + achall.token == challb.token """ from letsencrypt.acme import challenges @@ -29,19 +32,19 @@ class AnnotatedChallenge(jose_util.ImmutableMap): Wraps around server provided challenge and annotates with data useful for the client. - :ivar chall: Wrapped `~.ChallengeBody` (or just `~.challenges.Challenge`). + :ivar challb: Wrapped `~.ChallengeBody`. """ - __slots__ = ('chall',) + __slots__ = ('challb',) acme_type = NotImplemented def __getattr__(self, name): - return getattr(self.chall, name) + return getattr(self.challb, name) class DVSNI(AnnotatedChallenge): """Client annotated "dvsni" ACME challenge.""" - __slots__ = ('chall', 'domain', 'key') + __slots__ = ('challb', 'domain', 'key') acme_type = challenges.DVSNI def gen_cert_and_response(self, s=None): # pylint: disable=invalid-name @@ -55,35 +58,35 @@ class DVSNI(AnnotatedChallenge): """ response = challenges.DVSNIResponse(s=s) cert_pem = crypto_util.make_ss_cert(self.key.pem, [ - self.nonce_domain, self.domain, response.z_domain(self.chall)]) + self.nonce_domain, self.domain, response.z_domain(self.challb)]) return cert_pem, response class SimpleHTTPS(AnnotatedChallenge): """Client annotated "simpleHttps" ACME challenge.""" - __slots__ = ('chall', 'domain', 'key') + __slots__ = ('challb', 'domain', 'key') acme_type = challenges.SimpleHTTPS class DNS(AnnotatedChallenge): """Client annotated "dns" ACME challenge.""" - __slots__ = ('chall', 'domain') + __slots__ = ('challb', 'domain') acme_type = challenges.DNS class RecoveryContact(AnnotatedChallenge): """Client annotated "recoveryContact" ACME challenge.""" - __slots__ = ('chall', 'domain') + __slots__ = ('challb', 'domain') acme_type = challenges.RecoveryContact class RecoveryToken(AnnotatedChallenge): """Client annotated "recoveryToken" ACME challenge.""" - __slots__ = ('chall', 'domain') + __slots__ = ('challb', 'domain') acme_type = challenges.RecoveryToken class ProofOfPossession(AnnotatedChallenge): """Client annotated "proofOfPossession" ACME challenge.""" - __slots__ = ('chall', 'domain') + __slots__ = ('challb', 'domain') acme_type = challenges.ProofOfPossession diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index fce5fd87a..09c731cf0 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -138,7 +138,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes for achall, resp in itertools.izip(achalls, resps): # Don't send challenges for None and False authenticator responses if resp: - challr = self.network.answer_challenge(achall.chall, resp) + challr = self.network.answer_challenge(achall.challb, resp) if achall.domain in chall_update: chall_update[achall.domain].append(achall) else: @@ -267,31 +267,32 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes cont_chall = set() for index in path: - chall = self.authzr[domain].body.challenges[index] + challb = self.authzr[domain].body.challenges[index] + chall = challb.chall if isinstance(chall, challenges.DVSNI): logging.info(" DVSNI challenge for %s.", domain) achall = achallenges.DVSNI( - chall=chall, domain=domain, key=self.authkey) + challb=challb, domain=domain, key=self.authkey) elif isinstance(chall, challenges.SimpleHTTPS): logging.info(" SimpleHTTPS challenge for %s.", domain) achall = achallenges.SimpleHTTPS( - chall=chall, domain=domain, key=self.authkey) + challb=challb, domain=domain, key=self.authkey) elif isinstance(chall, challenges.DNS): logging.info(" DNS challenge for %s.", domain) - achall = achallenges.DNS(chall=chall, domain=domain) + achall = achallenges.DNS(challb=challb, domain=domain) elif isinstance(chall, challenges.RecoveryToken): logging.info(" Recovery Token Challenge for %s.", domain) - achall = achallenges.RecoveryToken(chall=chall, domain=domain) + achall = achallenges.RecoveryToken(challb=challb, domain=domain) elif isinstance(chall, challenges.RecoveryContact): logging.info(" Recovery Contact Challenge for %s.", domain) achall = achallenges.RecoveryContact( - chall=chall, domain=domain) + challb=challb, domain=domain) elif isinstance(chall, challenges.ProofOfPossession): logging.info(" Proof-of-Possession Challenge for %s", domain) achall = achallenges.ProofOfPossession( - chall=chall, domain=domain) + challb=challb, domain=domain) else: raise errors.LetsEncryptClientError( @@ -306,15 +307,15 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes return dv_chall, cont_chall -def gen_challenge_path(challs, preferences, combinations): +def gen_challenge_path(challbs, preferences, combinations): """Generate a plan to get authority over the identity. .. todo:: This can be possibly be rewritten to use resolved_combinations. - :param tuple challs: A tuple of challenges - (:class:`letsencrypt.acme.challenges.Challenge`) from - :class:`letsencrypt.acme.messages.Challenge` server message to - be fulfilled by the client in order to prove possession of the + :param tuple challbs: A tuple of challenges + (:class:`letsencrypt.acme.messages2.Challenge`) from + :class:`letsencrypt.acme.messages2.AuthorizationResource` to be + fulfilled by the client in order to prove possession of the identifier. :param list preferences: List of challenge preferences for domain @@ -333,12 +334,12 @@ def gen_challenge_path(challs, preferences, combinations): """ if combinations: - return _find_smart_path(challs, preferences, combinations) + return _find_smart_path(challbs, preferences, combinations) else: - return _find_dumb_path(challs, preferences) + return _find_dumb_path(challbs, preferences) -def _find_smart_path(challs, preferences, combinations): +def _find_smart_path(challbs, preferences, combinations): """Find challenge path with server hints. Can be called if combinations is included. Function uses a simple @@ -360,8 +361,8 @@ def _find_smart_path(challs, preferences, combinations): combo_total = 0 for combo in combinations: for challenge_index in combo: - combo_total += chall_cost.get(challs[ - challenge_index].__class__, max_cost) + combo_total += chall_cost.get(challbs[ + challenge_index].chall.__class__, max_cost) if combo_total < best_combo_cost: best_combo = combo @@ -378,7 +379,7 @@ def _find_smart_path(challs, preferences, combinations): return best_combo -def _find_dumb_path(challs, preferences): +def _find_dumb_path(challbs, preferences): """Find challenge path without server hints. Should be called if the combinations hint is not included by the @@ -391,11 +392,11 @@ def _find_dumb_path(challs, preferences): path = [] satisfied = set() for pref_c in preferences: - for i, offered_chall in enumerate(challs): - if (isinstance(offered_chall, pref_c) and - is_preferred(offered_chall, satisfied)): + for i, offered_challb in enumerate(challbs): + if (isinstance(offered_challb.chall, pref_c) and + is_preferred(offered_challb, satisfied)): path.append(i) - satisfied.add(offered_chall) + satisfied.add(offered_challb) return path @@ -415,11 +416,12 @@ def mutually_exclusive(obj1, obj2, groups, different=False): return True -def is_preferred(offered_chall, satisfied, +def is_preferred(offered_challb, satisfied, exclusive_groups=constants.EXCLUSIVE_CHALLENGES): """Return whether or not the challenge is preferred in path.""" - for chall in satisfied: + for challb in satisfied: if not mutually_exclusive( - offered_chall, chall, exclusive_groups, different=True): + offered_challb.chall, challb.chall, exclusive_groups, + different=True): return False return True