mirror of
https://github.com/certbot/certbot.git
synced 2026-01-21 19:01:07 +03:00
Fix "reg vs new-reg" encoding problem.
This commit is contained in:
@@ -7,7 +7,7 @@ import os
|
||||
|
||||
import requests
|
||||
|
||||
from acme import interfaces
|
||||
from acme import fields
|
||||
from acme import jose
|
||||
from acme import other
|
||||
|
||||
@@ -32,12 +32,12 @@ class DVChallenge(Challenge): # pylint: disable=abstract-method
|
||||
"""Domain validation challenges."""
|
||||
|
||||
|
||||
class ChallengeResponse(interfaces.ClientRequestableResource,
|
||||
jose.TypedJSONObjectWithFields):
|
||||
class ChallengeResponse(jose.TypedJSONObjectWithFields):
|
||||
# _fields_to_partial_json | pylint: disable=abstract-method
|
||||
"""ACME challenge response."""
|
||||
TYPES = {}
|
||||
resource_type = 'challenge'
|
||||
resource = fields.Resource(resource_type)
|
||||
|
||||
|
||||
@Challenge.register
|
||||
|
||||
@@ -48,11 +48,13 @@ class SimpleHTTPResponseTest(unittest.TestCase):
|
||||
path='6tbIMBC5Anhl5bOlWT5ZFA', tls=False)
|
||||
self.msg_https = SimpleHTTPResponse(path='6tbIMBC5Anhl5bOlWT5ZFA')
|
||||
self.jmsg_http = {
|
||||
'resource': 'challenge',
|
||||
'type': 'simpleHttp',
|
||||
'path': '6tbIMBC5Anhl5bOlWT5ZFA',
|
||||
'tls': False,
|
||||
}
|
||||
self.jmsg_https = {
|
||||
'resource': 'challenge',
|
||||
'type': 'simpleHttp',
|
||||
'path': '6tbIMBC5Anhl5bOlWT5ZFA',
|
||||
'tls': True,
|
||||
@@ -184,6 +186,7 @@ class DVSNIResponseTest(unittest.TestCase):
|
||||
s=b'\xf5\xd6\xe3\xb2]\xe0L\x0bN\x9cKJ\x14I\xa1K\xa3#\xf9\xa8'
|
||||
b'\xcd\x8c7\x0e\x99\x19)\xdc\xb7\xf3\x9bw')
|
||||
self.jmsg = {
|
||||
'resource': 'challenge',
|
||||
'type': 'dvsni',
|
||||
's': '9dbjsl3gTAtOnEtKFEmhS6Mj-ajNjDcOmRkp3Lfzm3c',
|
||||
}
|
||||
@@ -257,7 +260,11 @@ class RecoveryContactResponseTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from acme.challenges import RecoveryContactResponse
|
||||
self.msg = RecoveryContactResponse(token='23029d88d9e123e')
|
||||
self.jmsg = {'type': 'recoveryContact', 'token': '23029d88d9e123e'}
|
||||
self.jmsg = {
|
||||
'resource': 'challenge',
|
||||
'type': 'recoveryContact',
|
||||
'token': '23029d88d9e123e',
|
||||
}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
@@ -305,7 +312,11 @@ class RecoveryTokenResponseTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from acme.challenges import RecoveryTokenResponse
|
||||
self.msg = RecoveryTokenResponse(token='23029d88d9e123e')
|
||||
self.jmsg = {'type': 'recoveryToken', 'token': '23029d88d9e123e'}
|
||||
self.jmsg = {
|
||||
'resource': 'challenge',
|
||||
'type': 'recoveryToken',
|
||||
'token': '23029d88d9e123e'
|
||||
}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
@@ -455,11 +466,13 @@ class ProofOfPossessionResponseTest(unittest.TestCase):
|
||||
signature=signature)
|
||||
|
||||
self.jmsg_to = {
|
||||
'resource': 'challenge',
|
||||
'type': 'proofOfPossession',
|
||||
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
|
||||
'signature': signature,
|
||||
}
|
||||
self.jmsg_from = {
|
||||
'resource': 'challenge',
|
||||
'type': 'proofOfPossession',
|
||||
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
|
||||
'signature': signature.to_json(),
|
||||
@@ -505,7 +518,10 @@ class DNSResponseTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from acme.challenges import DNSResponse
|
||||
self.msg = DNSResponse()
|
||||
self.jmsg = {'type': 'dns'}
|
||||
self.jmsg = {
|
||||
'resource': 'challenge',
|
||||
'type': 'dns',
|
||||
}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""ACME client API."""
|
||||
import datetime
|
||||
import heapq
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
@@ -71,8 +70,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
||||
def register(self, new_reg=None):
|
||||
"""Register.
|
||||
|
||||
:param contact: Contact list, as accepted by `.Registration`
|
||||
:type contact: `tuple`
|
||||
:param .NewRegistration new_reg:
|
||||
|
||||
:returns: Registration Resource.
|
||||
:rtype: `.RegistrationResource`
|
||||
@@ -80,7 +78,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
||||
:raises .UnexpectedUpdate:
|
||||
|
||||
"""
|
||||
new_reg = messages.Registration() if new_reg is None else new_reg
|
||||
new_reg = messages.NewRegistration() if new_reg is None else new_reg
|
||||
assert isinstance(new_reg, messages.NewRegistration)
|
||||
|
||||
response = self.net.post(self.new_reg_uri, new_reg)
|
||||
# TODO: handle errors
|
||||
@@ -105,7 +104,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
||||
:rtype: `.RegistrationResource`
|
||||
|
||||
"""
|
||||
response = self.net.post(regr.uri, regr.body)
|
||||
response = self.net.post(
|
||||
regr.uri, messages.UpdateRegistration(**dict(regr.body)))
|
||||
|
||||
# TODO: Boulder returns httplib.ACCEPTED
|
||||
#assert response.status_code == httplib.OK
|
||||
@@ -164,7 +164,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
||||
:rtype: `.AuthorizationResource`
|
||||
|
||||
"""
|
||||
new_authz = messages.Authorization(identifier=identifier)
|
||||
new_authz = messages.NewAuthorization(identifier=identifier)
|
||||
response = self.net.post(new_authzr_uri, new_authz)
|
||||
# TODO: handle errors
|
||||
assert response.status_code == http_client.CREATED
|
||||
@@ -451,17 +451,15 @@ class ClientNetwork(object):
|
||||
|
||||
.. todo:: Implement ``acmePath``.
|
||||
|
||||
:param .ClientRequestableResource obj:
|
||||
:param .JSONDeSerializable obj:
|
||||
:param bytes nonce:
|
||||
:rtype: `.JWS`
|
||||
|
||||
"""
|
||||
jobj = obj.to_json()
|
||||
jobj['resource'] = obj.resource_type
|
||||
dumps = json.dumps(jobj).encode()
|
||||
logger.debug('Serialized JSON: %s', dumps)
|
||||
jobj = obj.json_dumps().encode()
|
||||
logger.debug('Serialized JSON: %s', jobj)
|
||||
return jws.JWS.sign(
|
||||
payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps()
|
||||
payload=jobj, key=self.key, alg=self.alg, nonce=nonce).json_dumps()
|
||||
|
||||
@classmethod
|
||||
def _check_response(cls, response, content_type=None):
|
||||
|
||||
@@ -45,6 +45,7 @@ class ClientTest(unittest.TestCase):
|
||||
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
|
||||
reg = messages.Registration(
|
||||
contact=self.contact, key=KEY.public_key(), recovery_token='t')
|
||||
self.new_reg = messages.NewRegistration(**dict(reg))
|
||||
self.regr = messages.RegistrationResource(
|
||||
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1',
|
||||
new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg',
|
||||
@@ -82,19 +83,19 @@ class ClientTest(unittest.TestCase):
|
||||
'terms-of-service': {'url': self.regr.terms_of_service},
|
||||
})
|
||||
|
||||
self.assertEqual(self.regr, self.client.register(self.regr.body))
|
||||
self.assertEqual(self.regr, self.client.register(self.new_reg))
|
||||
# TODO: test POST call arguments
|
||||
|
||||
# TODO: split here and separate test
|
||||
reg_wrong_key = self.regr.body.update(key=KEY2.public_key())
|
||||
self.response.json.return_value = reg_wrong_key.to_json()
|
||||
self.assertRaises(
|
||||
errors.UnexpectedUpdate, self.client.register, self.regr.body)
|
||||
errors.UnexpectedUpdate, self.client.register, self.new_reg)
|
||||
|
||||
def test_register_missing_next(self):
|
||||
self.response.status_code = http_client.CREATED
|
||||
self.assertRaises(
|
||||
errors.ClientError, self.client.register, self.regr.body)
|
||||
errors.ClientError, self.client.register, self.new_reg)
|
||||
|
||||
def test_update_registration(self):
|
||||
# "Instance of 'Field' has no to_json/update member" bug:
|
||||
@@ -102,6 +103,7 @@ class ClientTest(unittest.TestCase):
|
||||
self.response.headers['Location'] = self.regr.uri
|
||||
self.response.json.return_value = self.regr.body.to_json()
|
||||
self.assertEqual(self.regr, self.client.update_registration(self.regr))
|
||||
# TODO: test POST call arguments
|
||||
|
||||
# TODO: split here and separate test
|
||||
self.response.json.return_value = self.regr.body.update(
|
||||
@@ -369,9 +371,8 @@ class ClientNetworkTest(unittest.TestCase):
|
||||
self.assertTrue(self.net.verify_ssl is self.verify_ssl)
|
||||
|
||||
def test_wrap_in_jws(self):
|
||||
class MockClientRequestableResource(jose.JSONDeSerializable):
|
||||
class MockJSONDeSerializable(jose.JSONDeSerializable):
|
||||
# pylint: disable=missing-docstring
|
||||
resource_type = 'mock'
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def to_partial_json(self):
|
||||
@@ -381,10 +382,9 @@ class ClientNetworkTest(unittest.TestCase):
|
||||
pass # pragma: no cover
|
||||
# pylint: disable=protected-access
|
||||
jws_dump = self.net._wrap_in_jws(
|
||||
MockClientRequestableResource('foo'), nonce=b'Tg')
|
||||
MockJSONDeSerializable('foo'), nonce=b'Tg')
|
||||
jws = acme_jws.JWS.json_loads(jws_dump)
|
||||
self.assertEqual(json.loads(jws.payload.decode()),
|
||||
{'foo': 'foo', 'resource': 'mock'})
|
||||
self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'})
|
||||
self.assertEqual(jws.signature.combined.nonce, b'Tg')
|
||||
|
||||
def test_check_response_not_ok_jobj_no_error(self):
|
||||
|
||||
@@ -23,3 +23,21 @@ class RFC3339Field(jose.Field):
|
||||
return pyrfc3339.parse(value)
|
||||
except ValueError as error:
|
||||
raise jose.DeserializationError(error)
|
||||
|
||||
|
||||
class Resource(jose.Field):
|
||||
"""Resource MITM field."""
|
||||
|
||||
def __init__(self, resource_type, *args, **kwargs):
|
||||
self.resource_type = resource_type
|
||||
super(Resource, self).__init__(
|
||||
# TODO: omitempty used only to trick
|
||||
# JSONObjectWithFieldsMeta._defaults..., server implementation
|
||||
'resource', default=resource_type, *args, **kwargs)
|
||||
|
||||
def decode(self, value):
|
||||
if value != self.resource_type:
|
||||
raise jose.DeserializationError(
|
||||
'Wrong resource type: {0} instead of {1}'.format(
|
||||
value, self.resource_type))
|
||||
return value
|
||||
|
||||
@@ -35,5 +35,19 @@ class RFC3339FieldTest(unittest.TestCase):
|
||||
jose.DeserializationError, RFC3339Field.default_decoder, '')
|
||||
|
||||
|
||||
class ResourceTest(unittest.TestCase):
|
||||
"""Tests for acme.fields.Resource."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.fields import Resource
|
||||
self.field = Resource('x')
|
||||
|
||||
def test_decode_good(self):
|
||||
self.assertEqual('x', self.field.decode('x'))
|
||||
|
||||
def test_decode_wrong(self):
|
||||
self.assertRaises(jose.DeserializationError, self.field.decode, 'y')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
"""ACME interfaces."""
|
||||
from acme import jose
|
||||
|
||||
|
||||
class ClientRequestableResource(jose.JSONDeSerializable):
|
||||
"""Resource that can be requested by client.
|
||||
|
||||
:ivar unicode resource_type: ACME resource identifier used in client
|
||||
HTTPS requests in order to protect against MITM.
|
||||
|
||||
"""
|
||||
# pylint: disable=abstract-method
|
||||
resource_type = NotImplemented
|
||||
@@ -214,7 +214,7 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
|
||||
def _defaults(cls):
|
||||
"""Get default fields values."""
|
||||
return dict([(slot, field.default) for slot, field
|
||||
in six.iteritems(cls._fields) if field.omitempty])
|
||||
in six.iteritems(cls._fields)])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# pylint: disable=star-args
|
||||
|
||||
@@ -5,7 +5,6 @@ from six.moves.urllib import parse as urllib_parse # pylint: disable=import-err
|
||||
|
||||
from acme import challenges
|
||||
from acme import fields
|
||||
from acme import interfaces
|
||||
from acme import jose
|
||||
|
||||
|
||||
@@ -151,7 +150,7 @@ class ResourceBody(jose.JSONObjectWithFields):
|
||||
"""ACME Resource Body."""
|
||||
|
||||
|
||||
class Registration(interfaces.ClientRequestableResource, ResourceBody):
|
||||
class Registration(ResourceBody):
|
||||
"""Registration Resource Body.
|
||||
|
||||
:ivar acme.jose.jwk.JWK key: Public key.
|
||||
@@ -161,8 +160,6 @@ class Registration(interfaces.ClientRequestableResource, ResourceBody):
|
||||
:ivar unicode agreement:
|
||||
|
||||
"""
|
||||
resource_type = 'new-reg'
|
||||
|
||||
# on new-reg key server ignores 'key' and populates it based on
|
||||
# JWS.signature.combined.jwk
|
||||
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
|
||||
@@ -199,9 +196,17 @@ class Registration(interfaces.ClientRequestableResource, ResourceBody):
|
||||
"""All emails found in the ``contact`` field."""
|
||||
return self._filter_contact(self.email_prefix)
|
||||
|
||||
class NewRegistration(Registration):
|
||||
"""New registration."""
|
||||
resource_type = 'new-reg'
|
||||
resource = fields.Resource(resource_type)
|
||||
|
||||
class RegistrationResource(interfaces.ClientRequestableResource,
|
||||
ResourceWithURI):
|
||||
class UpdateRegistration(Registration):
|
||||
"""Update registration."""
|
||||
resource_type = 'reg'
|
||||
resource = fields.Resource(resource_type)
|
||||
|
||||
class RegistrationResource(ResourceWithURI):
|
||||
"""Registration Resource.
|
||||
|
||||
:ivar acme.messages.Registration body:
|
||||
@@ -209,7 +214,6 @@ class RegistrationResource(interfaces.ClientRequestableResource,
|
||||
:ivar unicode terms_of_service: URL for the CA TOS.
|
||||
|
||||
"""
|
||||
resource_type = 'reg'
|
||||
body = jose.Field('body', decoder=Registration.from_json)
|
||||
new_authzr_uri = jose.Field('new_authzr_uri')
|
||||
terms_of_service = jose.Field('terms_of_service', omitempty=True)
|
||||
@@ -272,7 +276,7 @@ class ChallengeResource(Resource):
|
||||
return self.body.uri # pylint: disable=no-member
|
||||
|
||||
|
||||
class Authorization(interfaces.ClientRequestableResource, ResourceBody):
|
||||
class Authorization(ResourceBody):
|
||||
"""Authorization Resource Body.
|
||||
|
||||
:ivar acme.messages.Identifier identifier:
|
||||
@@ -283,7 +287,6 @@ class Authorization(interfaces.ClientRequestableResource, ResourceBody):
|
||||
:ivar datetime.datetime expires:
|
||||
|
||||
"""
|
||||
resource_type = 'new-authz'
|
||||
identifier = jose.Field('identifier', decoder=Identifier.from_json)
|
||||
challenges = jose.Field('challenges', omitempty=True)
|
||||
combinations = jose.Field('combinations', omitempty=True)
|
||||
@@ -305,6 +308,10 @@ class Authorization(interfaces.ClientRequestableResource, ResourceBody):
|
||||
return tuple(tuple(self.challenges[idx] for idx in combo)
|
||||
for combo in self.combinations)
|
||||
|
||||
class NewAuthorization(Authorization):
|
||||
"""New authorization."""
|
||||
resource_type = 'new-authz'
|
||||
resource = fields.Resource(resource_type)
|
||||
|
||||
class AuthorizationResource(ResourceWithURI):
|
||||
"""Authorization Resource.
|
||||
@@ -317,8 +324,7 @@ class AuthorizationResource(ResourceWithURI):
|
||||
new_cert_uri = jose.Field('new_cert_uri')
|
||||
|
||||
|
||||
class CertificateRequest(interfaces.ClientRequestableResource,
|
||||
jose.JSONObjectWithFields):
|
||||
class CertificateRequest(jose.JSONObjectWithFields):
|
||||
"""ACME new-cert request.
|
||||
|
||||
:ivar acme.jose.util.ComparableX509 csr:
|
||||
@@ -327,12 +333,12 @@ class CertificateRequest(interfaces.ClientRequestableResource,
|
||||
|
||||
"""
|
||||
resource_type = 'new-cert'
|
||||
resource = fields.Resource(resource_type)
|
||||
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
|
||||
authorizations = jose.Field('authorizations', decoder=tuple)
|
||||
|
||||
|
||||
class CertificateResource(interfaces.ClientRequestableResource,
|
||||
ResourceWithURI):
|
||||
class CertificateResource(ResourceWithURI):
|
||||
"""Certificate Resource.
|
||||
|
||||
:ivar acme.jose.util.ComparableX509 body:
|
||||
@@ -341,13 +347,11 @@ class CertificateResource(interfaces.ClientRequestableResource,
|
||||
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
|
||||
|
||||
"""
|
||||
resource_type = 'cert'
|
||||
cert_chain_uri = jose.Field('cert_chain_uri')
|
||||
authzrs = jose.Field('authzrs')
|
||||
|
||||
|
||||
class Revocation(interfaces.ClientRequestableResource,
|
||||
jose.JSONObjectWithFields):
|
||||
class Revocation(jose.JSONObjectWithFields):
|
||||
"""Revocation message.
|
||||
|
||||
:ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in
|
||||
@@ -355,6 +359,7 @@ class Revocation(interfaces.ClientRequestableResource,
|
||||
|
||||
"""
|
||||
resource_type = 'revoke-cert'
|
||||
resource = fields.Resource(resource_type)
|
||||
certificate = jose.Field(
|
||||
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ def register(config, account_storage, tos_cb=None):
|
||||
backend=default_backend())))
|
||||
acme = _acme_from_config_key(config, key)
|
||||
# TODO: add phone?
|
||||
regr = acme.register(messages.Registration.from_data(email=config.email))
|
||||
regr = acme.register(messages.NewRegistration.from_data(email=config.email))
|
||||
|
||||
if regr.terms_of_service is not None:
|
||||
if tos_cb is not None and not tos_cb(regr):
|
||||
|
||||
Reference in New Issue
Block a user