Use new framework for ACME messages

This commit is contained in:
Jakub Warmuz
2015-02-06 16:38:35 +00:00
parent fe98a4ca48
commit 753b9ca15c
7 changed files with 281 additions and 292 deletions
+89 -185
View File
@@ -1,8 +1,4 @@
"""ACME protocol messages.""" """ACME protocol messages."""
import json
import pkg_resources
import jsonschema
import M2Crypto import M2Crypto
import zope.interface import zope.interface
@@ -13,25 +9,7 @@ from letsencrypt.acme import other
from letsencrypt.acme import util from letsencrypt.acme import util
SCHEMATA = dict([ class Message(util.JSONDeSerializable, util.ImmutableMap):
(schema, json.load(open(pkg_resources.resource_filename(
__name__, "schemata/%s.json" % schema)))) for schema in [
"authorization",
"authorizationRequest",
"certificate",
"certificateRequest",
"challenge",
"challengeRequest",
"defer",
"error",
"revocation",
"revocationRequest",
"statusRequest",
]
])
class Message(object):
"""ACME message. """ACME message.
Messages are considered immutable. Messages are considered immutable.
@@ -51,17 +29,6 @@ class Message(object):
cls.TYPES[msg_cls.acme_type] = msg_cls cls.TYPES[msg_cls.acme_type] = msg_cls
return msg_cls return msg_cls
@classmethod
def schema(cls, schemata=None):
"""Get JSON schema for this ACME message.
:param dict schemata: Mapping from type name to JSON Schema
definition. Useful for testing.
"""
schemata = SCHEMATA if schemata is None else schemata
return schemata[cls.acme_type]
def to_json(self): def to_json(self):
"""Get JSON serializable object. """Get JSON serializable object.
@@ -85,34 +52,23 @@ class Message(object):
:rtype: dict :rtype: dict
""" """
raise NotImplementedError raise NotImplementedError()
def json_dumps(self):
"""Dump to JSON using proper serializer.
:returns: JSON serialized string.
:rtype: str
"""
return json.dumps(self, default=util.dump_ijsonserializable)
@classmethod @classmethod
def validate(cls, jobj, schemata=None): def from_json(cls, jobj, validate=True):
"""Is JSON object a valid ACME message? """Deserialize validated ACME message from JSON string.
:param str jobj: JSON object :param str jobj: JSON object.
:param bool validate: Validate against schema before deserializing.
:param dict schemata: Mapping from type name to JSON Schema Useful if :class:`JWK` is part of already validated json object.
definition. Useful for testing.
:returns: ACME message class, subclassing :class:`Message`.
:raises letsencrypt.acme.errors.ValidationError: if validation :raises letsencrypt.acme.errors.ValidationError: if validation
was unsuccessful was unsuccessful
""" :returns: Valid ACME message.
schemata = SCHEMATA if schemata is None else schemata :rtype: subclass of :class:`Message`
"""
if not isinstance(jobj, dict): if not isinstance(jobj, dict):
raise errors.ValidationError( raise errors.ValidationError(
"{0} is not a dictionary object".format(jobj)) "{0} is not a dictionary object".format(jobj))
@@ -122,64 +78,22 @@ class Message(object):
raise errors.ValidationError("missing type field") raise errors.ValidationError("missing type field")
try: try:
schema = schemata[msg_type] # pylint: disable=redefined-outer-name
msg_cls = cls.TYPES[msg_type] msg_cls = cls.TYPES[msg_type]
except KeyError: except KeyError:
raise errors.UnrecognnizedMessageTypeError(msg_type) raise errors.UnrecognnizedMessageTypeError(msg_type)
try: if validate:
jsonschema.validate(jobj, schema) msg_cls.validate_json(jobj)
except jsonschema.ValidationError as error:
raise errors.SchemaValidationError(error)
return msg_cls
@classmethod
def from_json(cls, json_string, schemata=None):
"""Deserialize validated ACME message from JSON string.
:param str json_string: JSON serialize string.
:param dict schemata: Mapping from type name to JSON Schema
definition. Useful for testing.
:raises letsencrypt.acme.errors.ValidationError: if validation
was unsuccessful
:returns: Valid ACME message.
:rtype: subclass of :class:`Message`
"""
jobj = json.loads(json_string)
msg_cls = cls.validate(jobj, schemata)
# pylint: disable=protected-access # pylint: disable=protected-access
return msg_cls._valid_from_json(jobj) return msg_cls._from_valid_json(jobj)
@classmethod
def _valid_from_json(cls, jobj):
"""Deserialize from valid ACME message JSON object.
Subclasses must override.
:param jobj: Schema validated ACME message JSON object.
:type jobj: dict
:returns: Valid ACME message.
:rtype: subclass of :class:`Message`
"""
raise NotImplementedError
@Message.register # pylint: disable=too-few-public-methods @Message.register # pylint: disable=too-few-public-methods
class Challenge(Message): class Challenge(Message):
"""ACME "challenge" message.""" """ACME "challenge" message."""
acme_type = "challenge" acme_type = "challenge"
schema = util.load_schema(acme_type)
def __init__(self, session_id, nonce, challenges, combinations=None): __slots__ = ("session_id", "nonce", "challenges", "combinations")
self.session_id = session_id
self.nonce = nonce
self.challenges = challenges
self.combinations = [] if combinations is None else combinations
def _fields_to_json(self): def _fields_to_json(self):
fields = { fields = {
@@ -192,9 +106,11 @@ class Challenge(Message):
return fields return fields
@classmethod @classmethod
def _valid_from_json(cls, jobj): def _from_valid_json(cls, jobj):
return cls(jobj["sessionID"], jose.b64decode(jobj["nonce"]), return cls(session_id=jobj["sessionID"],
jobj["challenges"], jobj.get("combinations")) nonce=jose.b64decode(jobj["nonce"]),
challenges=jobj["challenges"],
combinations=jobj.get("combinations", []))
@Message.register # pylint: disable=too-few-public-methods @Message.register # pylint: disable=too-few-public-methods
@@ -205,9 +121,8 @@ class ChallengeRequest(Message):
""" """
acme_type = "challengeRequest" acme_type = "challengeRequest"
schema = util.load_schema(acme_type)
def __init__(self, identifier): __slots__ = ("identifier",)
self.identifier = identifier
def _fields_to_json(self): def _fields_to_json(self):
return { return {
@@ -215,19 +130,16 @@ class ChallengeRequest(Message):
} }
@classmethod @classmethod
def _valid_from_json(cls, json_string): def _from_valid_json(cls, jobj):
return cls(json_string["identifier"]) return cls(identifier=jobj["identifier"])
@Message.register # pylint: disable=too-few-public-methods @Message.register # pylint: disable=too-few-public-methods
class Authorization(Message): class Authorization(Message):
"""ACME "authorization" message.""" """ACME "authorization" message."""
acme_type = "authorization" acme_type = "authorization"
schema = util.load_schema(acme_type)
def __init__(self, recovery_token=None, identifier=None, jwk=None): __slots__ = ("recovery_token", "identifier", "jwk")
self.recovery_token = recovery_token
self.identifier = identifier
self.jwk = jwk
def _fields_to_json(self): def _fields_to_json(self):
fields = {} fields = {}
@@ -240,11 +152,12 @@ class Authorization(Message):
return fields return fields
@classmethod @classmethod
def _valid_from_json(cls, jobj): def _from_valid_json(cls, jobj):
jwk = jobj.get("jwk") jwk = jobj.get("jwk")
if jwk is not None: if jwk is not None:
jwk = jose.JWK.from_json(jwk) jwk = jose.JWK.from_json(jwk, validate=False)
return cls(jobj.get("recoveryToken"), jobj.get("identifier"), jwk) return cls(recovery_token=jobj.get("recoveryToken"),
identifier=jobj.get("identifier"), jwk=jwk)
@Message.register @Message.register
@@ -259,19 +172,15 @@ class AuthorizationRequest(Message):
""" """
acme_type = "authorizationRequest" acme_type = "authorizationRequest"
schema = util.load_schema(acme_type)
def __init__(self, session_id, nonce, responses, signature, contact=None): __slots__ = ("session_id", "nonce", "responses", "signature", "contact")
self.session_id = session_id
self.nonce = nonce
self.responses = responses
self.signature = signature
self.contact = [] if contact is None else contact
@classmethod @classmethod
def create(cls, session_id, nonce, responses, name, key, def create(cls, name, key, sig_nonce=None, **kwargs):
sig_nonce=None, contact=None):
"""Create signed "authorizationRequest". """Create signed "authorizationRequest".
:param str name: TODO
:param key: Key used for signing. :param key: Key used for signing.
:type key: :class:`Crypto.PublicKey.RSA` :type key: :class:`Crypto.PublicKey.RSA`
@@ -282,8 +191,10 @@ class AuthorizationRequest(Message):
""" """
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
signature = other.Signature.from_msg(name + nonce, key, sig_nonce) signature = other.Signature.from_msg(
return cls(session_id, nonce, responses, signature, contact) name + kwargs["nonce"], key, sig_nonce)
return cls(
signature=signature, contact=kwargs.pop("contact", []), **kwargs)
def verify(self, name): def verify(self, name):
"""Verify signature. """Verify signature.
@@ -310,11 +221,13 @@ class AuthorizationRequest(Message):
return fields return fields
@classmethod @classmethod
def _valid_from_json(cls, jobj): def _from_valid_json(cls, jobj):
return cls(jobj["sessionID"], jose.b64decode(jobj["nonce"]), return cls(session_id=jobj["sessionID"],
jobj["responses"], nonce=jose.b64decode(jobj["nonce"]),
other.Signature.from_json(jobj["signature"]), responses=jobj["responses"],
jobj.get("contact")) signature=other.Signature.from_json(
jobj["signature"], validate=False),
contact=jobj.get("contact", []))
@Message.register # pylint: disable=too-few-public-methods @Message.register # pylint: disable=too-few-public-methods
@@ -326,11 +239,8 @@ class Certificate(Message):
""" """
acme_type = "certificate" acme_type = "certificate"
schema = util.load_schema(acme_type)
def __init__(self, certificate, chain=None, refresh=None): __slots__ = ("certificate", "chain", "refresh")
self.certificate = certificate
self.chain = [] if chain is None else chain
self.refresh = refresh
def _fields_to_json(self): def _fields_to_json(self):
fields = { fields = {
@@ -342,10 +252,11 @@ class Certificate(Message):
return fields return fields
@classmethod @classmethod
def _valid_from_json(cls, jobj): def _from_valid_json(cls, jobj):
certificate = M2Crypto.X509.load_cert_der_string( certificate = M2Crypto.X509.load_cert_der_string(
jose.b64decode(jobj["certificate"])) jose.b64decode(jobj["certificate"]))
return cls(certificate, jobj.get("chain"), jobj.get("refresh")) return cls(certificate=certificate, chain=jobj.get("chain", []),
refresh=jobj.get("refresh"))
@Message.register @Message.register
@@ -358,25 +269,24 @@ class CertificateRequest(Message):
""" """
acme_type = "certificateRequest" acme_type = "certificateRequest"
schema = util.load_schema(acme_type)
def __init__(self, csr, signature): __slots__ = ("csr", "signature")
self.csr = csr
self.signature = signature
@classmethod @classmethod
def create(cls, csr, key, nonce=None): def create(cls, key, sig_nonce=None, **kwargs):
"""Create signed "certificateRequest". """Create signed "certificateRequest".
:param key: Key used for signing. :param key: Key used for signing.
:type key: :class:`Crypto.PublicKey.RSA` :type key: :class:`Crypto.PublicKey.RSA`
:param str nonce: Nonce used for signature. Useful for testing. :param str sig_nonce: Nonce used for signature. Useful for testing.
:returns: Signed "certificateRequest" ACME message. :returns: Signed "certificateRequest" ACME message.
:rtype: :class:`CertificateRequest` :rtype: :class:`CertificateRequest`
""" """
return cls(csr, other.Signature.from_msg(csr, key, nonce)) return cls(signature=other.Signature.from_msg(
kwargs["csr"], key, sig_nonce), **kwargs)
def verify(self): def verify(self):
"""Verify signature. """Verify signature.
@@ -396,20 +306,18 @@ class CertificateRequest(Message):
} }
@classmethod @classmethod
def _valid_from_json(cls, jobj): def _from_valid_json(cls, jobj):
return cls(jose.b64decode(jobj["csr"]), return cls(csr=jose.b64decode(jobj["csr"]),
other.Signature.from_json(jobj["signature"])) signature=other.Signature.from_json(
jobj["signature"], validate=False))
@Message.register # pylint: disable=too-few-public-methods @Message.register # pylint: disable=too-few-public-methods
class Defer(Message): class Defer(Message):
"""ACME "defer" message.""" """ACME "defer" message."""
acme_type = "defer" acme_type = "defer"
schema = util.load_schema(acme_type)
def __init__(self, token, interval=None, message=None): __slots__ = ("token", "interval", "message")
self.token = token
self.interval = interval # TODO: int
self.message = message
def _fields_to_json(self): def _fields_to_json(self):
fields = {"token": self.token} fields = {"token": self.token}
@@ -420,14 +328,17 @@ class Defer(Message):
return fields return fields
@classmethod @classmethod
def _valid_from_json(cls, jobj): def _from_valid_json(cls, jobj):
return cls(jobj["token"], jobj.get("interval"), jobj.get("message")) return cls(token=jobj["token"], interval=jobj.get("interval"),
message=jobj.get("message"))
@Message.register # pylint: disable=too-few-public-methods @Message.register # pylint: disable=too-few-public-methods
class Error(Message): class Error(Message):
"""ACME "error" message.""" """ACME "error" message."""
acme_type = "error" acme_type = "error"
schema = util.load_schema(acme_type)
__slots__ = ("error", "message", "more_info")
CODES = { CODES = {
"malformed": "The request message was malformed", "malformed": "The request message was malformed",
@@ -438,12 +349,6 @@ class Error(Message):
"badCSR": "The CSR is unacceptable (e.g., due to a short key)", "badCSR": "The CSR is unacceptable (e.g., due to a short key)",
} }
def __init__(self, error, message=None, more_info=None):
assert error in self.CODES # TODO: already checked by schema validation
self.error = error
self.message = message
self.more_info = more_info
def _fields_to_json(self): def _fields_to_json(self):
fields = {"error": self.error} fields = {"error": self.error}
if self.message is not None: if self.message is not None:
@@ -453,20 +358,23 @@ class Error(Message):
return fields return fields
@classmethod @classmethod
def _valid_from_json(cls, jobj): def _from_valid_json(cls, jobj):
return cls(jobj["error"], jobj.get("message"), jobj.get("more_info")) return cls(error=jobj["error"], message=jobj.get("message"),
more_info=jobj.get("more_info"))
@Message.register # pylint: disable=too-few-public-methods @Message.register # pylint: disable=too-few-public-methods
class Revocation(Message): class Revocation(Message):
"""ACME "revocation" message.""" """ACME "revocation" message."""
acme_type = "revocation" acme_type = "revocation"
schema = util.load_schema(acme_type)
__slots__ = ()
def _fields_to_json(self): def _fields_to_json(self):
return {} return {}
@classmethod @classmethod
def _valid_from_json(cls, jobj): def _from_valid_json(cls, jobj):
return cls() return cls()
@@ -481,26 +389,24 @@ class RevocationRequest(Message):
""" """
acme_type = "revocationRequest" acme_type = "revocationRequest"
schema = util.load_schema(acme_type)
def __init__(self, certificate, signature): __slots__ = ("certificate", "signature")
self.certificate = certificate
self.signature = signature
@classmethod @classmethod
def create(cls, certificate, key, nonce=None): def create(cls, key, sig_nonce=None, **kwargs):
"""Create signed "revocationRequest". """Create signed "revocationRequest".
:param key: Key used for signing. :param key: Key used for signing.
:type key: :class:`Crypto.PublicKey.RSA` :type key: :class:`Crypto.PublicKey.RSA`
:param str nonce: Nonce used for signature. Useful for testing. :param str sig_nonce: Nonce used for signature. Useful for testing.
:returns: Signed "revocationRequest" ACME message. :returns: Signed "revocationRequest" ACME message.
:rtype: :class:`RevocationRequest` :rtype: :class:`RevocationRequest`
""" """
return cls(certificate, return cls(signature=other.Signature.from_msg(
other.Signature.from_msg(certificate, key, nonce)) kwargs["certificate"], key, sig_nonce), **kwargs)
def verify(self): def verify(self):
"""Verify signature. """Verify signature.
@@ -520,9 +426,10 @@ class RevocationRequest(Message):
} }
@classmethod @classmethod
def _valid_from_json(cls, json_string): def _from_valid_json(cls, jobj):
return cls(jose.b64decode(json_string["certificate"]), return cls(certificate=jose.b64decode(jobj["certificate"]),
other.Signature.from_json(json_string["signature"])) signature=other.Signature.from_json(
jobj["signature"], validate=False))
@Message.register # pylint: disable=too-few-public-methods @Message.register # pylint: disable=too-few-public-methods
@@ -533,15 +440,12 @@ class StatusRequest(Message):
""" """
acme_type = "statusRequest" acme_type = "statusRequest"
schema = util.load_schema(acme_type)
def __init__(self, token): __slots__ = ("token",)
self.token = token
def _fields_to_json(self): def _fields_to_json(self):
return { return {"token": self.token}
"token": self.token,
}
@classmethod @classmethod
def _valid_from_json(cls, json_string): def _from_valid_json(cls, jobj):
return cls(json_string["token"]) return cls(token=jobj["token"])
+160 -87
View File
@@ -6,6 +6,9 @@ import Crypto.PublicKey.RSA
import mock import mock
from letsencrypt.acme import errors from letsencrypt.acme import errors
from letsencrypt.acme import jose
from letsencrypt.acme import other
KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string( KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
'letsencrypt.client.tests', 'testdata/rsa256_key.pem')) 'letsencrypt.client.tests', 'testdata/rsa256_key.pem'))
@@ -15,158 +18,228 @@ class MessageTest(unittest.TestCase):
"""Tests for letsencrypt.acme.messages.Message.""" """Tests for letsencrypt.acme.messages.Message."""
def setUp(self): def setUp(self):
self.schemata = { # pylint: disable=missing-docstring,too-few-public-methods
'foo': { from letsencrypt.acme.messages import Message
class TestMessage(Message):
acme_type = 'test'
schema = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'price': {'type': 'number'}, 'price': {'type': 'number'},
'name': {'type': 'string'}, 'name': {'type': 'string'},
}, },
},
} }
@classmethod
def _from_valid_json(cls, jobj):
return jobj
def _validate(self, jobj): def _fields_to_json(self):
pass
self.msg_cls = TestMessage
@classmethod
def _from_json(cls, jobj, validate=True):
from letsencrypt.acme.messages import Message from letsencrypt.acme.messages import Message
return Message.validate(jobj, self.schemata) return Message.from_json(jobj, validate)
def test_validate_non_dictionary_fails(self): def test_from_json_non_dict_fails(self):
self.assertRaises(errors.ValidationError, self._validate, []) self.assertRaises(errors.ValidationError, self._from_json, [])
def test_validate_dict_without_type_fails(self): def test_from_json_dict_no_type_fails(self):
self.assertRaises(errors.ValidationError, self._validate, {}) self.assertRaises(errors.ValidationError, self._from_json, {})
def test_validate_unknown_type_fails(self): def test_from_json_unknown_type_fails(self):
self.assertRaises(errors.UnrecognnizedMessageTypeError, self.assertRaises(errors.UnrecognnizedMessageTypeError,
self._validate, {'type': 'bar'}) self._from_json, {'type': 'bar'})
def test_validate_unregistered_type_fails(self):
self.assertRaises(errors.UnrecognnizedMessageTypeError,
self._validate, {'type': 'foo'})
@mock.patch('letsencrypt.acme.messages.Message.TYPES') @mock.patch('letsencrypt.acme.messages.Message.TYPES')
def test_validate_invalid_fails(self, types): def test_from_json_validate_errors(self, types):
types.__getitem__.side_effect = lambda x: {'foo': 'bar'}[x] types.__getitem__.side_effect = lambda x: {'foo': self.msg_cls}[x]
self.assertRaises(errors.SchemaValidationError, self.assertRaises(errors.SchemaValidationError,
self._validate, {'type': 'foo', 'price': 'asd'}) self._from_json, {'type': 'foo', 'price': 'asd'})
@mock.patch('letsencrypt.acme.messages.Message.TYPES') @mock.patch('letsencrypt.acme.messages.Message.TYPES')
def test_validate_valid_returns_cls(self, types): def test_from_json_valid_returns_cls(self, types):
types.__getitem__.side_effect = lambda x: {'foo': 'bar'}[x] types.__getitem__.side_effect = lambda x: {'foo': self.msg_cls}[x]
self.assertEqual(self._validate({'type': 'foo'}), 'bar') self.assertEqual(self._from_json({'type': 'foo'}, validate=False),
{'type': 'foo'})
class ChallengeRequestTest(unittest.TestCase): class ChallengeRequestTest(unittest.TestCase):
# pylint: disable=too-few-public-methods
def test_it(self): def setUp(self):
from letsencrypt.acme.messages import ChallengeRequest from letsencrypt.acme.messages import ChallengeRequest
msg = ChallengeRequest('example.com') self.msg = ChallengeRequest(identifier='example.com')
jmsg = msg._fields_to_json() # pylint: disable=protected-access self.jmsg = {
self.assertEqual(jmsg, { 'type': 'challengeRequest',
'identifier': 'example.com', 'identifier': 'example.com',
}) }
def test_to_json(self):
self.assertEqual(self.msg.to_json(), self.jmsg)
def test_from_json(self):
from letsencrypt.acme.messages import ChallengeRequest
self.assertEqual(ChallengeRequest.from_json(self.jmsg), self.msg)
class AuthorizationRequestTest(unittest.TestCase): class AuthorizationRequestTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.nonce = '\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9' self.responses = [
self.b64nonce = '7Nbyb1lI6xPVI3Hg3aKSqQ' {'type': 'simpleHttps', 'path': 'Hf5GrX4Q7EBax9hc2jJnfw'},
self.csr = 'TODO: real DER CSR?'
def test_authorization_request(self):
from letsencrypt.acme.messages import AuthorizationRequest
responses = [
{
'type': 'simpleHttps',
'path': 'Hf5GrX4Q7EBax9hc2jJnfw',
},
None, # null None, # null
{ {'type': 'recoveryToken', 'token': '23029d88d9e123e'},
'type': 'recoveryToken',
'token': '23029d88d9e123e',
}
] ]
msg = AuthorizationRequest.create( signature = other.Signature(
'aefoGaavieG9Wihuk2aufai3aeZ5EeW4', alg='RS256', jwk=jose.JWK(key=KEY.publickey()),
'czpsrF0KMH6dgajig3TGHw', sig='-v\xd8\xc2\xa3\xba0\xd6\x92\x16\xb5.\xbe\xa1[\x04\xbe'
responses, '\x1b\xa1X\xd2)\x18\x94\x8f\xd7\xd0\xc0\xbbcI`W\xdf v'
'example.com', '\xe4\xed\xe8\x03J\xe8\xc8<?\xc8W\x94\x94cj(\xe7\xaa$'
KEY, '\x92\xe9\x96\x11\xc2\xefx\x0bR',
self.nonce, nonce='\xab?\x08o\xe6\x81$\x9f\xa1\xc9\x025\x1c\x1b\xa5+')
)
msg.verify('example.com')
jmsg = msg._fields_to_json() # pylint: disable=protected-access from letsencrypt.acme.messages import AuthorizationRequest
jmsg.pop('signature') self.msg = AuthorizationRequest(
self.assertEqual(jmsg, { session_id='aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9',
responses=self.responses,
signature=signature,
contact=[],
)
self.jmsg = {
'type': 'authorizationRequest',
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4', 'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
'nonce': 'Y3pwc3JGMEtNSDZkZ2FqaWczVEdIdw', 'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ',
'responses': responses, 'responses': self.responses,
}) 'signature': signature,
}
def test_create(self):
from letsencrypt.acme.messages import AuthorizationRequest
self.assertEqual(self.msg, AuthorizationRequest.create(
name='example.com', key=KEY, responses=self.responses,
nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9',
session_id='aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
sig_nonce='\xab?\x08o\xe6\x81$\x9f\xa1\xc9\x025\x1c\x1b\xa5+'))
def test_verify(self):
self.assertTrue(self.msg.verify('example.com'))
def test_to_json(self):
self.assertEqual(self.msg.to_json(), self.jmsg)
def test_from_json(self):
from letsencrypt.acme.messages import AuthorizationRequest
self.jmsg['signature'] = self.jmsg['signature'].to_json()
self.jmsg['signature']['jwk'] = self.jmsg['signature']['jwk'].to_json()
self.assertEqual(self.msg, AuthorizationRequest.from_json(self.jmsg))
class CertificateRequestTest(unittest.TestCase): class CertificateRequestTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.nonce = '\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'
self.b64nonce = '7Nbyb1lI6xPVI3Hg3aKSqQ'
self.csr = 'TODO: real DER CSR?' self.csr = 'TODO: real DER CSR?'
signature = other.Signature(
alg='RS256', jwk=jose.JWK(key=KEY.publickey()),
sig='\x1cD\x157\x83\x14\xd7 \xeb\x02\xb3\xf6O\xb5\x99C]\x97'
'\x94p\xa7\xe48\x13>\x06\xf9yd\xf9\xfe\xf8\xd1>\x9aKH'
'\xd7\xba\xb9a1\xf5!p\x1b\xd7}\xbaj\xa7\xe3\xd9\xd9\t%'
'\xbb\xba\xc9\x00\xdaW\x16\xe9',
nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9')
def test_it(self):
from letsencrypt.acme.messages import CertificateRequest from letsencrypt.acme.messages import CertificateRequest
msg = CertificateRequest.create(self.csr, KEY, self.nonce) self.msg = CertificateRequest(csr=self.csr, signature=signature)
self.assertTrue(msg.verify())
jmsg = msg._fields_to_json() # pylint: disable=protected-access self.jmsg = {
jmsg.pop('signature') 'type': 'certificateRequest',
self.assertEqual(jmsg, {
'csr': 'VE9ETzogcmVhbCBERVIgQ1NSPw', 'csr': 'VE9ETzogcmVhbCBERVIgQ1NSPw',
}) 'signature': signature,
}
def test_create(self):
from letsencrypt.acme.messages import CertificateRequest
self.assertEqual(self.msg, CertificateRequest.create(
csr=self.csr, key=KEY,
sig_nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'))
def test_verify(self):
self.assertTrue(self.msg.verify())
def test_to_json(self):
self.assertEqual(self.msg.to_json(), self.jmsg)
def test_from_json(self):
from letsencrypt.acme.messages import CertificateRequest
self.jmsg['signature'] = self.jmsg['signature'].to_json()
self.jmsg['signature']['jwk'] = self.jmsg['signature']['jwk'].to_json()
self.assertEqual(self.msg, CertificateRequest.from_json(self.jmsg))
class RevocationRequestTest(unittest.TestCase): class RevocationRequestTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.sig_nonce = '\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'
self.nonce = '\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9' self.nonce = '\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'
self.b64nonce = '7Nbyb1lI6xPVI3Hg3aKSqQ'
self.certificate = 'TODO: real DER cert?' self.certificate = 'TODO: real DER cert?'
def test_it(self): signature = other.Signature(
from letsencrypt.acme.messages import RevocationRequest alg='RS256', jwk=jose.JWK(key=KEY.publickey()),
msg = RevocationRequest.create(self.certificate, KEY, self.nonce) sig='\x00\x15\xc0\xd4\x8b2M\xa9S\\\x8a#\xc6a\xa7!A\xb2d\x04'
self.assertTrue(msg.verify()) '\xa6\xbe\xa1/M\x0f|\x8c\x9eJ\x16\xcd\x85N\xcc\x0b\x12k('
'\xa8U\xdfS\xa9y\xfd\xfa.\xb3\xeblms\x9f,\xdf\xbb>7\xd9'
'\xe5u\x8f\xbe',
nonce=self.sig_nonce)
jmsg = msg._fields_to_json() # pylint: disable=protected-access from letsencrypt.acme.messages import RevocationRequest
jmsg.pop('signature') self.msg = RevocationRequest(
self.assertEqual(jmsg, { certificate=self.certificate, signature=signature)
self.jmsg = {
'type': 'revocationRequest',
'certificate': 'VE9ETzogcmVhbCBERVIgY2VydD8', 'certificate': 'VE9ETzogcmVhbCBERVIgY2VydD8',
}) 'signature': signature,
}
def test_create(self):
from letsencrypt.acme.messages import RevocationRequest
RevocationRequest.create(
certificate=self.certificate, key=KEY, sig_nonce=self.sig_nonce)
def test_verify(self):
self.assertTrue(self.msg.verify())
def test_to_json(self):
self.assertEqual(self.msg.to_json(), self.jmsg)
def test_from_json(self):
from letsencrypt.acme.messages import RevocationRequest
self.jmsg['signature'] = self.jmsg['signature'].to_json()
self.jmsg['signature']['jwk'] = self.jmsg['signature']['jwk'].to_json()
self.assertEqual(self.msg, RevocationRequest.from_json(self.jmsg))
class StatusRequestTest(unittest.TestCase): class StatusRequestTest(unittest.TestCase):
def setUp(self): def setUp(self):
from letsencrypt.acme.messages import StatusRequest from letsencrypt.acme.messages import StatusRequest
self.token = u'O7-s9MNq1siZHlgrMzi9_A' self.msg = StatusRequest(token=u'O7-s9MNq1siZHlgrMzi9_A')
self.msg = StatusRequest(self.token)
self.jmsg = { self.jmsg = {
'token': self.token, 'type': 'statusRequest',
'token': u'O7-s9MNq1siZHlgrMzi9_A',
} }
def test_attributes(self): def test_to_json(self):
self.assertEqual(self.msg.token, self.token) self.assertEqual(self.msg.to_json(), self.jmsg)
def test_json(self):
jmsg = self.msg._fields_to_json() # pylint: disable=protected-access
self.assertEqual(jmsg, self.jmsg)
def test_from_json(self):
from letsencrypt.acme.messages import StatusRequest from letsencrypt.acme.messages import StatusRequest
# pylint: disable=protected-access self.assertEqual(StatusRequest.from_json(self.jmsg), self.msg)
msg = StatusRequest._valid_from_json(self.jmsg)
self.assertEqual(msg.token, self.msg.token)
if __name__ == '__main__': if __name__ == '__main__':
+6 -5
View File
@@ -111,11 +111,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
try: try:
auth = self.network.send_and_receive_expected( auth = self.network.send_and_receive_expected(
acme.messages.AuthorizationRequest.create( acme.messages.AuthorizationRequest.create(
self.msgs[domain].session_id, session_id=self.msgs[domain].session_id,
self.msgs[domain].nonce, nonce=self.msgs[domain].nonce,
self.responses[domain], responses=self.responses[domain],
domain, name=domain,
Crypto.PublicKey.RSA.importKey(self.authkey[domain].pem)), key=Crypto.PublicKey.RSA.importKey(
self.authkey[domain].pem)),
acme.messages.Authorization) acme.messages.Authorization)
logging.info("Received Authorization for %s", domain) logging.info("Received Authorization for %s", domain)
return auth return auth
+4 -2
View File
@@ -122,7 +122,8 @@ class Client(object):
""" """
return self.network.send_and_receive_expected( return self.network.send_and_receive_expected(
acme.messages.ChallengeRequest(domain), acme.messages.Challenge) acme.messages.ChallengeRequest(identifier=domain),
acme.messages.Challenge)
def acme_certificate(self, csr_der): def acme_certificate(self, csr_der):
"""Handle ACME "certificate" phase. """Handle ACME "certificate" phase.
@@ -136,7 +137,8 @@ class Client(object):
logging.info("Preparing and sending CSR...") logging.info("Preparing and sending CSR...")
return self.network.send_and_receive_expected( return self.network.send_and_receive_expected(
acme.messages.CertificateRequest.create( acme.messages.CertificateRequest.create(
csr_der, Crypto.PublicKey.RSA.importKey(self.authkey.pem)), csr=csr_der, key=Crypto.PublicKey.RSA.importKey(
self.authkey.pem)),
acme.messages.Certificate) acme.messages.Certificate)
def save_certificate(self, certificate_msg, cert_path, chain_path): def save_certificate(self, certificate_msg, cert_path, chain_path):
+2 -2
View File
@@ -53,7 +53,7 @@ class Network(object):
raise errors.LetsEncryptClientError( raise errors.LetsEncryptClientError(
'Sending ACME message to server has failed: %s' % error) 'Sending ACME message to server has failed: %s' % error)
return acme.messages.Message.from_json(response.json()) return acme.messages.Message.from_json(response.json(), validate=True)
def send_and_receive_expected(self, msg, expected): def send_and_receive_expected(self, msg, expected):
"""Send ACME message to server and return expected message. """Send ACME message to server and return expected message.
@@ -101,7 +101,7 @@ class Network(object):
logging.info("Waiting for %d seconds...", delay) logging.info("Waiting for %d seconds...", delay)
time.sleep(delay) time.sleep(delay)
response = self.send( response = self.send(
acme.messages.StatusRequest(response.token)) acme.messages.StatusRequest(token=response.token))
else: else:
logging.fatal("Received unexpected message") logging.fatal("Received unexpected message")
logging.fatal("Expected: %s", expected) logging.fatal("Expected: %s", expected)
+1 -1
View File
@@ -38,7 +38,7 @@ class Revoker(object):
revocation = self.network.send_and_receive_expected( revocation = self.network.send_and_receive_expected(
acme.messages.RevocationRequest.create( acme.messages.RevocationRequest.create(
cert_der, Crypto.PublicKey.RSA.importKey(key)), certificate=cert_der, key=Crypto.PublicKey.RSA.importKey(key)),
acme.messages.Revocation) acme.messages.Revocation)
zope.component.getUtility(interfaces.IDisplay).generic_notification( zope.component.getUtility(interfaces.IDisplay).generic_notification(
+18 -9
View File
@@ -41,7 +41,8 @@ class SatisfyChallengesTest(unittest.TestCase):
def test_name1_dvsni1(self): def test_name1_dvsni1(self):
dom = "0" dom = "0"
challenge = [acme_util.CHALLENGES["dvsni"]] challenge = [acme_util.CHALLENGES["dvsni"]]
msg = acme.messages.Challenge(dom, "nonce0", challenge) msg = acme.messages.Challenge(session_id=dom, nonce="nonce0",
challenges=challenge, combinations=[])
self.handler.add_chall_msg(dom, msg, "dummy_key") self.handler.add_chall_msg(dom, msg, "dummy_key")
self.handler._satisfy_challenges() # pylint: disable=protected-access self.handler._satisfy_challenges() # pylint: disable=protected-access
@@ -60,7 +61,8 @@ class SatisfyChallengesTest(unittest.TestCase):
for i in range(5): for i in range(5):
self.handler.add_chall_msg( self.handler.add_chall_msg(
str(i), str(i),
acme.messages.Challenge(str(i), "nonce%d" % i, challenge), acme.messages.Challenge(session_id=str(i), nonce="nonce%d" % i,
challenges=challenge, combinations=[]),
"dummy_key") "dummy_key")
self.handler._satisfy_challenges() # pylint: disable=protected-access self.handler._satisfy_challenges() # pylint: disable=protected-access
@@ -87,7 +89,8 @@ class SatisfyChallengesTest(unittest.TestCase):
combos = acme_util.gen_combos(challenges) combos = acme_util.gen_combos(challenges)
self.handler.add_chall_msg( self.handler.add_chall_msg(
dom, dom,
acme.messages.Challenge("0", "nonce0", challenges, combos), acme.messages.Challenge(session_id="0", nonce="nonce0",
challenges=challenges, combinations=combos),
"dummy_key") "dummy_key")
path = gen_path(["simpleHttps"], challenges) path = gen_path(["simpleHttps"], challenges)
@@ -116,7 +119,8 @@ class SatisfyChallengesTest(unittest.TestCase):
combos = acme_util.gen_combos(challenges) combos = acme_util.gen_combos(challenges)
self.handler.add_chall_msg( self.handler.add_chall_msg(
dom, dom,
acme.messages.Challenge(dom, "nonce0", challenges, combos), acme.messages.Challenge(session_id=dom, nonce="nonce0",
challenges=challenges, combinations=combos),
"dummy_key") "dummy_key")
path = gen_path(["simpleHttps", "recoveryToken"], challenges) path = gen_path(["simpleHttps", "recoveryToken"], challenges)
@@ -147,7 +151,8 @@ class SatisfyChallengesTest(unittest.TestCase):
self.handler.add_chall_msg( self.handler.add_chall_msg(
str(i), str(i),
acme.messages.Challenge( acme.messages.Challenge(
str(i), "nonce%d" % i, challenges, combos), session_id=str(i), nonce="nonce%d" % i,
challenges=challenges, combinations=combos),
"dummy_key") "dummy_key")
path = gen_path(["dvsni", "recoveryContact"], challenges) path = gen_path(["dvsni", "recoveryContact"], challenges)
@@ -197,7 +202,8 @@ class SatisfyChallengesTest(unittest.TestCase):
self.handler.add_chall_msg( self.handler.add_chall_msg(
dom, dom,
acme.messages.Challenge( acme.messages.Challenge(
dom, "nonce%d" % i, challenge_list[i]), session_id=dom, nonce="nonce%d" % i,
challenges=challenge_list[i], combinations=[]),
"dummy_key") "dummy_key")
mock_chall_path.side_effect = paths mock_chall_path.side_effect = paths
@@ -266,7 +272,8 @@ class GetAuthorizationsTest(unittest.TestCase):
for i in range(3): for i in range(3):
self.handler.add_chall_msg( self.handler.add_chall_msg(
str(i), str(i),
acme.messages.Challenge(str(i), "nonce%d" % i, challenge), acme.messages.Challenge(session_id=str(i), nonce="nonce%d" % i,
challenges=challenge, combinations=[]),
"dummy_key") "dummy_key")
self.mock_sat_chall.side_effect = self._sat_solved_at_once self.mock_sat_chall.side_effect = self._sat_solved_at_once
@@ -294,7 +301,8 @@ class GetAuthorizationsTest(unittest.TestCase):
challenges = acme_util.get_challenges() challenges = acme_util.get_challenges()
self.handler.add_chall_msg( self.handler.add_chall_msg(
"0", "0",
acme.messages.Challenge("0", "nonce0", challenges), acme.messages.Challenge(session_id="0", nonce="nonce0",
challenges=challenges, combinations=[]),
"dummy_key") "dummy_key")
# Don't do anything to satisfy challenges # Don't do anything to satisfy challenges
@@ -322,7 +330,8 @@ class GetAuthorizationsTest(unittest.TestCase):
dom = str(i) dom = str(i)
self.handler.add_chall_msg( self.handler.add_chall_msg(
dom, dom,
acme.messages.Challenge(dom, "nonce%d" % i, challs[i]), acme.messages.Challenge(session_id=dom, nonce="nonce%d" % i,
challenges=challs[i], combinations=[]),
"dummy_key") "dummy_key")
self.mock_sat_chall.side_effect = self._sat_incremental self.mock_sat_chall.side_effect = self._sat_incremental