Merge pull request #594 from kuba/acme-resource

Enforce "resource" field in request objects.
This commit is contained in:
James Kasten
2015-07-09 11:42:13 -07:00
5 changed files with 59 additions and 15 deletions
+10 -2
View File
@@ -7,6 +7,7 @@ import os
import requests import requests
from acme import interfaces
from acme import jose from acme import jose
from acme import other from acme import other
@@ -31,10 +32,17 @@ class DVChallenge(Challenge): # pylint: disable=abstract-method
"""Domain validation challenges.""" """Domain validation challenges."""
class ChallengeResponse(jose.TypedJSONObjectWithFields): class ChallengeResponse(interfaces.ClientRequestableResource,
jose.TypedJSONObjectWithFields):
# _fields_to_partial_json | pylint: disable=abstract-method # _fields_to_partial_json | pylint: disable=abstract-method
"""ACME challenge response.""" """ACME challenge response.
:ivar str mitm_resource: ACME resource identifier used in client
HTTPS requests in order to protect against MITM.
"""
TYPES = {} TYPES = {}
resource_type = 'challenge'
@classmethod @classmethod
def from_json(cls, jobj): def from_json(cls, jobj):
+7 -2
View File
@@ -2,6 +2,7 @@
import datetime import datetime
import heapq import heapq
import httplib import httplib
import json
import logging import logging
import time import time
@@ -81,6 +82,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes
response = self.net.post(self.new_reg_uri, new_reg) response = self.net.post(self.new_reg_uri, new_reg)
assert response.status_code == httplib.CREATED # TODO: handle errors assert response.status_code == httplib.CREATED # TODO: handle errors
# "Instance of 'Field' has no key/contact member" bug:
# pylint: disable=no-member
regr = self._regr_from_response(response) regr = self._regr_from_response(response)
if (regr.body.key != self.key.public_key() or if (regr.body.key != self.key.public_key() or
regr.body.contact != new_reg.contact): regr.body.contact != new_reg.contact):
@@ -443,11 +446,13 @@ class ClientNetwork(object):
.. todo:: Implement ``acmePath``. .. todo:: Implement ``acmePath``.
:param JSONDeSerializable obj: :param .ClientRequestableResource obj:
:rtype: `.JWS` :rtype: `.JWS`
""" """
dumps = obj.json_dumps() jobj = obj.to_json()
jobj['resource'] = obj.resource_type
dumps = json.dumps(jobj)
logger.debug('Serialized JSON: %s', dumps) logger.debug('Serialized JSON: %s', dumps)
return jws.JWS.sign( return jws.JWS.sign(
payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps() payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps()
+11 -4
View File
@@ -1,6 +1,7 @@
"""Tests for acme.client.""" """Tests for acme.client."""
import datetime import datetime
import httplib import httplib
import json
import os import os
import pkg_resources import pkg_resources
import unittest import unittest
@@ -74,6 +75,8 @@ class ClientTest(unittest.TestCase):
cert_chain_uri='https://www.letsencrypt-demo.org/ca') cert_chain_uri='https://www.letsencrypt-demo.org/ca')
def test_register(self): def test_register(self):
# "Instance of 'Field' has no to_json/update member" bug:
# pylint: disable=no-member
self.response.status_code = httplib.CREATED self.response.status_code = httplib.CREATED
self.response.json.return_value = self.regr.body.to_json() self.response.json.return_value = self.regr.body.to_json()
self.response.headers['Location'] = self.regr.uri self.response.headers['Location'] = self.regr.uri
@@ -97,6 +100,8 @@ class ClientTest(unittest.TestCase):
errors.ClientError, self.client.register, self.regr.body) errors.ClientError, self.client.register, self.regr.body)
def test_update_registration(self): def test_update_registration(self):
# "Instance of 'Field' has no to_json/update member" bug:
# pylint: disable=no-member
self.response.headers['Location'] = self.regr.uri self.response.headers['Location'] = self.regr.uri
self.response.json.return_value = self.regr.body.to_json() self.response.json.return_value = self.regr.body.to_json()
self.assertEqual(self.regr, self.client.update_registration(self.regr)) self.assertEqual(self.regr, self.client.update_registration(self.regr))
@@ -367,20 +372,22 @@ class ClientNetworkTest(unittest.TestCase):
self.assertTrue(self.net.verify_ssl is self.verify_ssl) self.assertTrue(self.net.verify_ssl is self.verify_ssl)
def test_wrap_in_jws(self): def test_wrap_in_jws(self):
class MockJSONDeSerializable(jose.JSONDeSerializable): class MockClientRequestableResource(jose.JSONDeSerializable):
# pylint: disable=missing-docstring # pylint: disable=missing-docstring
resource_type = 'mock'
def __init__(self, value): def __init__(self, value):
self.value = value self.value = value
def to_partial_json(self): def to_partial_json(self):
return self.value return {'foo': self.value}
@classmethod @classmethod
def from_json(cls, value): def from_json(cls, value):
pass # pragma: no cover pass # pragma: no cover
# pylint: disable=protected-access # pylint: disable=protected-access
jws_dump = self.net._wrap_in_jws( jws_dump = self.net._wrap_in_jws(
MockJSONDeSerializable('foo'), nonce='Tg') MockClientRequestableResource('foo'), nonce='Tg')
jws = acme_jws.JWS.json_loads(jws_dump) jws = acme_jws.JWS.json_loads(jws_dump)
self.assertEqual(jws.payload, '"foo"') self.assertEqual(json.loads(jws.payload),
{'foo': 'foo', 'resource': 'mock'})
self.assertEqual(jws.signature.combined.nonce, 'Tg') self.assertEqual(jws.signature.combined.nonce, 'Tg')
# TODO: check that nonce is in protected header # TODO: check that nonce is in protected header
+13
View File
@@ -0,0 +1,13 @@
"""ACME interfaces."""
from acme import jose
class ClientRequestableResource(jose.JSONDeSerializable):
"""Resource that can be requested by client.
:ivar str resource_type: ACME resource identifier used in client
HTTPS requests in order to protect against MITM.
"""
# pylint: disable=abstract-method
resource_type = NotImplemented
+18 -7
View File
@@ -3,6 +3,7 @@ import urlparse
from acme import challenges from acme import challenges
from acme import fields from acme import fields
from acme import interfaces
from acme import jose from acme import jose
@@ -117,7 +118,6 @@ class Identifier(jose.JSONObjectWithFields):
class Resource(jose.JSONObjectWithFields): 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.
""" """
@@ -137,13 +137,15 @@ class ResourceBody(jose.JSONObjectWithFields):
"""ACME Resource Body.""" """ACME Resource Body."""
class Registration(ResourceBody): class Registration(interfaces.ClientRequestableResource, ResourceBody):
"""Registration Resource Body. """Registration Resource Body.
:ivar acme.jose.jwk.JWK key: Public key. :ivar acme.jose.jwk.JWK key: Public key.
:ivar tuple contact: Contact information following ACME spec :ivar tuple contact: Contact information following ACME spec
""" """
resource_type = 'new-regr'
# on new-reg key server ignores 'key' and populates it based on # on new-reg key server ignores 'key' and populates it based on
# JWS.signature.combined.jwk # JWS.signature.combined.jwk
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
@@ -205,7 +207,8 @@ class Registration(ResourceBody):
return None return None
class RegistrationResource(ResourceWithURI): class RegistrationResource(interfaces.ClientRequestableResource,
ResourceWithURI):
"""Registration Resource. """Registration Resource.
:ivar acme.messages.Registration body: :ivar acme.messages.Registration body:
@@ -213,6 +216,7 @@ class RegistrationResource(ResourceWithURI):
:ivar str terms_of_service: URL for the CA TOS. :ivar str terms_of_service: URL for the CA TOS.
""" """
resource_type = 'reg'
body = jose.Field('body', decoder=Registration.from_json) body = jose.Field('body', decoder=Registration.from_json)
new_authzr_uri = jose.Field('new_authzr_uri') new_authzr_uri = jose.Field('new_authzr_uri')
terms_of_service = jose.Field('terms_of_service', omitempty=True) terms_of_service = jose.Field('terms_of_service', omitempty=True)
@@ -274,7 +278,7 @@ class ChallengeResource(Resource):
return self.body.uri # pylint: disable=no-member return self.body.uri # pylint: disable=no-member
class Authorization(ResourceBody): class Authorization(interfaces.ClientRequestableResource, ResourceBody):
"""Authorization Resource Body. """Authorization Resource Body.
:ivar acme.messages.Identifier identifier: :ivar acme.messages.Identifier identifier:
@@ -287,6 +291,7 @@ class Authorization(ResourceBody):
:ivar datetime.datetime expires: :ivar datetime.datetime expires:
""" """
resource_type = 'new-authz'
identifier = jose.Field('identifier', decoder=Identifier.from_json) identifier = jose.Field('identifier', decoder=Identifier.from_json)
challenges = jose.Field('challenges', omitempty=True) challenges = jose.Field('challenges', omitempty=True)
combinations = jose.Field('combinations', omitempty=True) combinations = jose.Field('combinations', omitempty=True)
@@ -320,7 +325,8 @@ class AuthorizationResource(ResourceWithURI):
new_cert_uri = jose.Field('new_cert_uri') new_cert_uri = jose.Field('new_cert_uri')
class CertificateRequest(jose.JSONObjectWithFields): class CertificateRequest(interfaces.ClientRequestableResource,
jose.JSONObjectWithFields):
"""ACME new-cert request. """ACME new-cert request.
:ivar acme.jose.util.ComparableX509 csr: :ivar acme.jose.util.ComparableX509 csr:
@@ -328,11 +334,13 @@ class CertificateRequest(jose.JSONObjectWithFields):
:ivar tuple authorizations: `tuple` of URIs (`str`) :ivar tuple authorizations: `tuple` of URIs (`str`)
""" """
resource_type = 'new-cert'
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
authorizations = jose.Field('authorizations', decoder=tuple) authorizations = jose.Field('authorizations', decoder=tuple)
class CertificateResource(ResourceWithURI): class CertificateResource(interfaces.ClientRequestableResource,
ResourceWithURI):
"""Certificate Resource. """Certificate Resource.
:ivar acme.jose.util.ComparableX509 body: :ivar acme.jose.util.ComparableX509 body:
@@ -341,17 +349,20 @@ class CertificateResource(ResourceWithURI):
:ivar tuple authzrs: `tuple` of `AuthorizationResource`. :ivar tuple authzrs: `tuple` of `AuthorizationResource`.
""" """
resource_type = 'cert'
cert_chain_uri = jose.Field('cert_chain_uri') cert_chain_uri = jose.Field('cert_chain_uri')
authzrs = jose.Field('authzrs') authzrs = jose.Field('authzrs')
class Revocation(jose.JSONObjectWithFields): class Revocation(interfaces.ClientRequestableResource,
jose.JSONObjectWithFields):
"""Revocation message. """Revocation message.
:ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in :ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in
`.ComparableX509` `.ComparableX509`
""" """
resource_type = 'revoke-cert'
certificate = jose.Field( certificate = jose.Field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)