mirror of
https://github.com/certbot/certbot.git
synced 2026-06-06 15:22:38 -04:00
Merge pull request #347 from kuba/boulder
Revert to ChallengeResource/ChallengeBody/Challenge triplet
This commit is contained in:
commit
31915c5a01
6 changed files with 91 additions and 79 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue