JSONDeSerializable acme.messages.Resource.

Provides API necessary to implement JSON-based account storage as
described at
https://github.com/letsencrypt/lets-encrypt-preview/pull/362#issuecomment-97946817
This commit is contained in:
Jakub Warmuz
2015-06-22 20:41:45 +00:00
parent 1720864b44
commit e0a1e8f4e8
4 changed files with 197 additions and 53 deletions
+1
View File
@@ -466,6 +466,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
updated_authzr, response = self.poll(updated[authzr]) updated_authzr, response = self.poll(updated[authzr])
updated[authzr] = updated_authzr updated[authzr] = updated_authzr
# pylint: disable=no-member
if updated_authzr.body.status != messages.STATUS_VALID: if updated_authzr.body.status != messages.STATUS_VALID:
# push back to the priority queue, with updated retry_after # push back to the priority queue, with updated retry_after
heapq.heappush(waiting, (self.retry_after( heapq.heappush(waiting, (self.retry_after(
+12 -20
View File
@@ -5,7 +5,6 @@ import os
import pkg_resources import pkg_resources
import unittest import unittest
import M2Crypto
import mock import mock
import requests import requests
@@ -14,16 +13,9 @@ from acme import errors
from acme import jose from acme import jose
from acme import jws as acme_jws from acme import jws as acme_jws
from acme import messages from acme import messages
from acme import messages_test
CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string(
pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'cert.der')),
M2Crypto.X509.FORMAT_DER))
CSR = jose.ComparableX509(M2Crypto.X509.load_request_string(
pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'csr.der')),
M2Crypto.X509.FORMAT_DER))
KEY = jose.JWKRSA.load(pkg_resources.resource_string( KEY = jose.JWKRSA.load(pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))
KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( KEY2 = jose.JWKRSA.load(pkg_resources.resource_string(
@@ -82,7 +74,7 @@ class ClientTest(unittest.TestCase):
# Request issuance # Request issuance
self.certr = messages.CertificateResource( self.certr = messages.CertificateResource(
body=CERT, authzrs=(self.authzr,), body=messages_test.CERT, authzrs=(self.authzr,),
uri='https://www.letsencrypt-demo.org/acme/cert/1', uri='https://www.letsencrypt-demo.org/acme/cert/1',
cert_chain_uri='https://www.letsencrypt-demo.org/ca') cert_chain_uri='https://www.letsencrypt-demo.org/ca')
@@ -380,27 +372,27 @@ class ClientTest(unittest.TestCase):
self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr) self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr)
def test_request_issuance(self): def test_request_issuance(self):
self.response.content = CERT.as_der() self.response.content = messages_test.CERT.as_der()
self.response.headers['Location'] = self.certr.uri self.response.headers['Location'] = self.certr.uri
self.response.links['up'] = {'url': self.certr.cert_chain_uri} self.response.links['up'] = {'url': self.certr.cert_chain_uri}
self._mock_post_get() self._mock_post_get()
self.assertEqual( self.assertEqual(self.certr, self.net.request_issuance(
self.certr, self.net.request_issuance(CSR, (self.authzr,))) messages_test.CSR, (self.authzr,)))
# TODO: check POST args # TODO: check POST args
def test_request_issuance_missing_up(self): def test_request_issuance_missing_up(self):
self.response.content = CERT.as_der() self.response.content = messages_test.CERT.as_der()
self.response.headers['Location'] = self.certr.uri self.response.headers['Location'] = self.certr.uri
self._mock_post_get() self._mock_post_get()
self.assertEqual( self.assertEqual(
self.certr.update(cert_chain_uri=None), self.certr.update(cert_chain_uri=None),
self.net.request_issuance(CSR, (self.authzr,))) self.net.request_issuance(messages_test.CSR, (self.authzr,)))
def test_request_issuance_missing_location(self): def test_request_issuance_missing_location(self):
self._mock_post_get() self._mock_post_get()
self.assertRaises( self.assertRaises(
errors.ClientError, self.net.request_issuance, errors.ClientError, self.net.request_issuance,
CSR, (self.authzr,)) messages_test.CSR, (self.authzr,))
@mock.patch('acme.client.datetime') @mock.patch('acme.client.datetime')
@mock.patch('acme.client.time') @mock.patch('acme.client.time')
@@ -484,10 +476,10 @@ class ClientTest(unittest.TestCase):
def test_check_cert(self): def test_check_cert(self):
self.response.headers['Location'] = self.certr.uri self.response.headers['Location'] = self.certr.uri
self.response.content = CERT.as_der() self.response.content = messages_test.CERT.as_der()
self._mock_post_get() self._mock_post_get()
self.assertEqual( self.assertEqual(self.certr.update(body=messages_test.CERT),
self.certr.update(body=CERT), self.net.check_cert(self.certr)) self.net.check_cert(self.certr))
# TODO: split here and separate test # TODO: split here and separate test
self.response.headers['Location'] = 'foo' self.response.headers['Location'] = 'foo'
@@ -495,7 +487,7 @@ class ClientTest(unittest.TestCase):
errors.UnexpectedUpdate, self.net.check_cert, self.certr) errors.UnexpectedUpdate, self.net.check_cert, self.certr)
def test_check_cert_missing_location(self): def test_check_cert_missing_location(self):
self.response.content = CERT.as_der() self.response.content = messages_test.CERT.as_der()
self._mock_post_get() self._mock_post_get()
self.assertRaises(errors.ClientError, self.net.check_cert, self.certr) self.assertRaises(errors.ClientError, self.net.check_cert, self.certr)
+87 -31
View File
@@ -51,6 +51,7 @@ class Error(jose.JSONObjectWithFields, Exception):
else: else:
return str(self.detail) return str(self.detail)
class _Constant(jose.JSONDeSerializable): class _Constant(jose.JSONDeSerializable):
"""ACME constant.""" """ACME constant."""
__slots__ = ('name',) __slots__ = ('name',)
@@ -107,31 +108,29 @@ class Identifier(jose.JSONObjectWithFields):
value = jose.Field('value') value = jose.Field('value')
class Resource(jose.ImmutableMap): class Resource(jose.JSONObjectWithFields):
"""ACME Resource. """ACME Resource.
:ivar str uri: Location of the resource.
:ivar acme.messages.ResourceBody body: Resource body. :ivar acme.messages.ResourceBody body: Resource body.
"""
body = jose.Field('body')
class ResourceWithURI(Resource):
"""ACME Resource with URI.
:ivar str uri: Location of the resource. :ivar str uri: Location of the resource.
""" """
__slots__ = ('body', 'uri') uri = jose.Field('uri') # no ChallengeResource.uri
class ResourceBody(jose.JSONObjectWithFields): class ResourceBody(jose.JSONObjectWithFields):
"""ACME Resource Body.""" """ACME Resource Body."""
class RegistrationResource(Resource):
"""Registration Resource.
:ivar acme.messages.Registration body:
:ivar str new_authzr_uri: URI found in the 'next' ``Link`` header
:ivar str terms_of_service: URL for the CA TOS.
"""
__slots__ = ('body', 'uri', 'new_authzr_uri', 'terms_of_service')
class Registration(ResourceBody): class Registration(ResourceBody):
"""Registration Resource Body. """Registration Resource Body.
@@ -146,21 +145,59 @@ class Registration(ResourceBody):
recovery_token = jose.Field('recoveryToken', omitempty=True) recovery_token = jose.Field('recoveryToken', omitempty=True)
agreement = jose.Field('agreement', omitempty=True) agreement = jose.Field('agreement', omitempty=True)
phone_prefix = 'tel:'
email_prefix = 'mailto:'
class ChallengeResource(Resource, jose.JSONObjectWithFields): @classmethod
"""Challenge Resource. def from_data(cls, phone=None, email=None, **kwargs):
"""Create registration resource from contact detauls."""
details = list(kwargs.pop('contact', ()))
if phone is not None:
details.append(cls.phone_prefix + phone)
if email is not None:
details.append(cls.email_prefix + email)
kwargs['contact'] = tuple(details)
return cls(**kwargs)
:ivar acme.messages.ChallengeBody body: def _filter_contact(self, prefix):
:ivar str authzr_uri: URI found in the 'up' ``Link`` header. return tuple(
detail[len(prefix):] for detail in self.contact
""" if detail.startswith(prefix))
__slots__ = ('body', 'authzr_uri')
@property @property
def uri(self): # pylint: disable=missing-docstring,no-self-argument def phones(self):
# bug? 'method already defined line None' """All phones found in the ``contact`` field."""
# pylint: disable=function-redefined return self._filter_contact(self.phone_prefix)
return self.body.uri
@property
def emails(self):
"""All emails found in the ``contact`` field."""
return self._filter_contact(self.email_prefix)
@property
def phone(self):
"""Phone."""
assert len(self.phones) == 1
return self.phones[0]
@property
def email(self):
"""Email."""
assert len(self.emails) == 1
return self.emails[0]
class RegistrationResource(ResourceWithURI):
"""Registration Resource.
:ivar acme.messages.Registration body:
:ivar str new_authzr_uri: URI found in the 'next' ``Link`` header
:ivar str terms_of_service: URL for the CA TOS.
"""
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)
class ChallengeBody(ResourceBody): class ChallengeBody(ResourceBody):
@@ -199,14 +236,21 @@ class ChallengeBody(ResourceBody):
return getattr(self.chall, name) return getattr(self.chall, name)
class AuthorizationResource(Resource): class ChallengeResource(Resource, jose.JSONObjectWithFields):
"""Authorization Resource. """Challenge Resource.
:ivar acme.messages.Authorization body: :ivar acme.messages.ChallengeBody body:
:ivar str new_cert_uri: URI found in the 'next' ``Link`` header :ivar str authzr_uri: URI found in the 'up' ``Link`` header.
""" """
__slots__ = ('body', 'uri', 'new_cert_uri') body = jose.Field('body', decoder=ChallengeBody.from_json)
authzr_uri = jose.Field('authzr_uri')
@property
def uri(self): # pylint: disable=missing-docstring,no-self-argument
# bug? 'method already defined line None'
# pylint: disable=function-redefined
return self.body.uri # pylint: disable=no-member
class Authorization(ResourceBody): class Authorization(ResourceBody):
@@ -244,6 +288,17 @@ class Authorization(ResourceBody):
for combo in self.combinations) for combo in self.combinations)
class AuthorizationResource(ResourceWithURI):
"""Authorization Resource.
:ivar acme.messages.Authorization body:
:ivar str new_cert_uri: URI found in the 'next' ``Link`` header
"""
body = jose.Field('body', decoder=Authorization.from_json)
new_cert_uri = jose.Field('new_cert_uri')
class CertificateRequest(jose.JSONObjectWithFields): class CertificateRequest(jose.JSONObjectWithFields):
"""ACME new-cert request. """ACME new-cert request.
@@ -256,7 +311,7 @@ class CertificateRequest(jose.JSONObjectWithFields):
authorizations = jose.Field('authorizations', decoder=tuple) authorizations = jose.Field('authorizations', decoder=tuple)
class CertificateResource(Resource): class CertificateResource(ResourceWithURI):
"""Certificate Resource. """Certificate Resource.
:ivar acme.jose.util.ComparableX509 body: :ivar acme.jose.util.ComparableX509 body:
@@ -265,7 +320,8 @@ class CertificateResource(Resource):
:ivar tuple authzrs: `tuple` of `AuthorizationResource`. :ivar tuple authzrs: `tuple` of `AuthorizationResource`.
""" """
__slots__ = ('body', 'uri', 'cert_chain_uri', 'authzrs') cert_chain_uri = jose.Field('cert_chain_uri')
authzrs = jose.Field('authzrs')
class Revocation(jose.JSONObjectWithFields): class Revocation(jose.JSONObjectWithFields):
+97 -2
View File
@@ -4,14 +4,23 @@ import os
import pkg_resources import pkg_resources
import unittest import unittest
from Crypto.PublicKey import RSA
import M2Crypto
import mock import mock
import pytz import pytz
from Crypto.PublicKey import RSA
from acme import challenges from acme import challenges
from acme import jose from acme import jose
CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string(
pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'cert.der')),
M2Crypto.X509.FORMAT_DER))
CSR = jose.ComparableX509(M2Crypto.X509.load_request_string(
pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'csr.der')),
M2Crypto.X509.FORMAT_DER))
KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string( KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))))
@@ -94,12 +103,16 @@ class ConstantTest(unittest.TestCase):
self.assertTrue(self.const_a != self.const_b) self.assertTrue(self.const_a != self.const_b)
self.assertFalse(self.const_a != const_a_prime) self.assertFalse(self.const_a != const_a_prime)
class RegistrationTest(unittest.TestCase): class RegistrationTest(unittest.TestCase):
"""Tests for acme.messages.Registration.""" """Tests for acme.messages.Registration."""
def setUp(self): def setUp(self):
key = jose.jwk.JWKRSA(key=KEY.publickey()) key = jose.jwk.JWKRSA(key=KEY.publickey())
contact = ('mailto:letsencrypt-client@letsencrypt.org',) contact = (
'mailto:admin@foo.com',
'tel:1234',
)
recovery_token = 'XYZ' recovery_token = 'XYZ'
agreement = 'https://letsencrypt.org/terms' agreement = 'https://letsencrypt.org/terms'
@@ -117,6 +130,26 @@ class RegistrationTest(unittest.TestCase):
self.jobj_from = self.jobj_to.copy() self.jobj_from = self.jobj_to.copy()
self.jobj_from['key'] = key.to_json() self.jobj_from['key'] = key.to_json()
def test_from_data(self):
from acme.messages import Registration
reg = Registration.from_data(phone='1234', email='admin@foo.com')
self.assertEqual(reg.contact, (
'tel:1234',
'mailto:admin@foo.com',
))
def test_phones(self):
self.assertEqual(('1234',), self.reg.phones)
def test_emails(self):
self.assertEqual(('admin@foo.com',), self.reg.emails)
def test_phone(self):
self.assertEqual('1234', self.reg.phone)
def test_email(self):
self.assertEqual('admin@foo.com', self.reg.email)
def test_to_partial_json(self): def test_to_partial_json(self):
self.assertEqual(self.jobj_to, self.reg.to_partial_json()) self.assertEqual(self.jobj_to, self.reg.to_partial_json())
@@ -129,6 +162,25 @@ class RegistrationTest(unittest.TestCase):
hash(Registration.from_json(self.jobj_from)) hash(Registration.from_json(self.jobj_from))
class RegistrationResourceTest(unittest.TestCase):
"""Tests for acme.messages.RegistrationResource."""
def setUp(self):
from acme.messages import RegistrationResource
self.regr = RegistrationResource(
body=mock.sentinel.body, uri=mock.sentinel.uri,
new_authzr_uri=mock.sentinel.new_authzr_uri,
terms_of_service=mock.sentinel.terms_of_service)
def test_to_partial_json(self):
self.assertEqual(self.regr.to_json(), {
'body': mock.sentinel.body,
'uri': mock.sentinel.uri,
'new_authzr_uri': mock.sentinel.new_authzr_uri,
'terms_of_service': mock.sentinel.terms_of_service,
})
class ChallengeResourceTest(unittest.TestCase): class ChallengeResourceTest(unittest.TestCase):
"""Tests for acme.messages.ChallengeResource.""" """Tests for acme.messages.ChallengeResource."""
@@ -220,6 +272,49 @@ class AuthorizationTest(unittest.TestCase):
)) ))
class AuthorizationResourceTest(unittest.TestCase):
"""Tests for acme.messages.AuthorizationResource."""
def test_json_de_serializable(self):
from acme.messages import AuthorizationResource
authzr = AuthorizationResource(
uri=mock.sentinel.uri,
body=mock.sentinel.body,
new_cert_uri=mock.sentinel.new_cert_uri,
)
self.assertTrue(isinstance(authzr, jose.JSONDeSerializable))
class CertificateRequestTest(unittest.TestCase):
"""Tests for acme.messages.CertificateRequest."""
def setUp(self):
from acme.messages import CertificateRequest
self.req = CertificateRequest(csr=CSR, authorizations=('foo',))
def test_json_de_serializable(self):
self.assertTrue(isinstance(self.req, jose.JSONDeSerializable))
from acme.messages import CertificateRequest
self.assertEqual(
self.req, CertificateRequest.from_json(self.req.to_json()))
class CertificateResourceTest(unittest.TestCase):
"""Tests for acme.messages.CertificateResourceTest."""
def setUp(self):
from acme.messages import CertificateResource
self.certr = CertificateResource(
body=CERT, uri=mock.sentinel.uri, authzrs=(),
cert_chain_uri=mock.sentinel.cert_chain_uri)
def test_json_de_serializable(self):
self.assertTrue(isinstance(self.certr, jose.JSONDeSerializable))
from acme.messages import CertificateResource
self.assertEqual(
self.certr, CertificateResource.from_json(self.certr.to_json()))
class RevocationTest(unittest.TestCase): class RevocationTest(unittest.TestCase):
"""Tests for acme.messages.RevocationTest.""" """Tests for acme.messages.RevocationTest."""