Merge pull request #347 from kuba/boulder

Revert to ChallengeResource/ChallengeBody/Challenge triplet
This commit is contained in:
James Kasten 2015-04-14 13:00:59 -07:00
commit 31915c5a01
6 changed files with 91 additions and 79 deletions

View file

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

View file

@ -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,40 @@ 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.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:
"""
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
def __getattr__(self, name):
return getattr(self.chall, name)
class AuthorizationResource(Resource):
@ -193,7 +208,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 +235,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):

View file

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

View file

@ -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
@ -26,19 +29,22 @@ 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 challb: Wrapped `~.ChallengeBody`.
"""
__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
@ -52,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

View file

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

View file

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