diff --git a/acme/challenges.py b/acme/challenges.py index 45db23e72..8024728fa 100644 --- a/acme/challenges.py +++ b/acme/challenges.py @@ -7,6 +7,7 @@ import os import requests +from acme import interfaces from acme import jose from acme import other @@ -31,10 +32,17 @@ class DVChallenge(Challenge): # pylint: disable=abstract-method """Domain validation challenges.""" -class ChallengeResponse(jose.TypedJSONObjectWithFields): +class ChallengeResponse(interfaces.ClientRequestableResource, + jose.TypedJSONObjectWithFields): # _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 = {} + resource_type = 'challenge' @classmethod def from_json(cls, jobj): diff --git a/acme/client.py b/acme/client.py index 064bd8cd2..33e4e4f7f 100644 --- a/acme/client.py +++ b/acme/client.py @@ -2,6 +2,7 @@ import datetime import heapq import httplib +import json import logging 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) 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) if (regr.body.key != self.key.public_key() or regr.body.contact != new_reg.contact): @@ -443,11 +446,13 @@ class ClientNetwork(object): .. todo:: Implement ``acmePath``. - :param JSONDeSerializable obj: + :param .ClientRequestableResource obj: :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) return jws.JWS.sign( payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps() diff --git a/acme/client_test.py b/acme/client_test.py index bbed2ed87..ec4231093 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -1,6 +1,7 @@ """Tests for acme.client.""" import datetime import httplib +import json import os import pkg_resources import unittest @@ -74,6 +75,8 @@ class ClientTest(unittest.TestCase): cert_chain_uri='https://www.letsencrypt-demo.org/ca') 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.json.return_value = self.regr.body.to_json() self.response.headers['Location'] = self.regr.uri @@ -97,6 +100,8 @@ class ClientTest(unittest.TestCase): errors.ClientError, self.client.register, self.regr.body) 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.json.return_value = self.regr.body.to_json() 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) def test_wrap_in_jws(self): - class MockJSONDeSerializable(jose.JSONDeSerializable): + class MockClientRequestableResource(jose.JSONDeSerializable): # pylint: disable=missing-docstring + resource_type = 'mock' def __init__(self, value): self.value = value def to_partial_json(self): - return self.value + return {'foo': self.value} @classmethod def from_json(cls, value): pass # pragma: no cover # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( - MockJSONDeSerializable('foo'), nonce='Tg') + MockClientRequestableResource('foo'), nonce='Tg') 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') # TODO: check that nonce is in protected header diff --git a/acme/interfaces.py b/acme/interfaces.py new file mode 100644 index 000000000..9899b1093 --- /dev/null +++ b/acme/interfaces.py @@ -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 diff --git a/acme/messages.py b/acme/messages.py index 4d82a7723..1be2c5632 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -3,6 +3,7 @@ import urlparse from acme import challenges from acme import fields +from acme import interfaces from acme import jose @@ -117,7 +118,6 @@ class Identifier(jose.JSONObjectWithFields): class Resource(jose.JSONObjectWithFields): """ACME Resource. - :ivar str uri: Location of the resource. :ivar acme.messages.ResourceBody body: Resource body. """ @@ -137,13 +137,15 @@ class ResourceBody(jose.JSONObjectWithFields): """ACME Resource Body.""" -class Registration(ResourceBody): +class Registration(interfaces.ClientRequestableResource, ResourceBody): """Registration Resource Body. :ivar acme.jose.jwk.JWK key: Public key. :ivar tuple contact: Contact information following ACME spec """ + resource_type = 'new-regr' + # 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) @@ -205,7 +207,8 @@ class Registration(ResourceBody): return None -class RegistrationResource(ResourceWithURI): +class RegistrationResource(interfaces.ClientRequestableResource, + ResourceWithURI): """Registration Resource. :ivar acme.messages.Registration body: @@ -213,6 +216,7 @@ class RegistrationResource(ResourceWithURI): :ivar str 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) @@ -274,7 +278,7 @@ class ChallengeResource(Resource): return self.body.uri # pylint: disable=no-member -class Authorization(ResourceBody): +class Authorization(interfaces.ClientRequestableResource, ResourceBody): """Authorization Resource Body. :ivar acme.messages.Identifier identifier: @@ -287,6 +291,7 @@ class Authorization(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) @@ -320,7 +325,8 @@ class AuthorizationResource(ResourceWithURI): new_cert_uri = jose.Field('new_cert_uri') -class CertificateRequest(jose.JSONObjectWithFields): +class CertificateRequest(interfaces.ClientRequestableResource, + jose.JSONObjectWithFields): """ACME new-cert request. :ivar acme.jose.util.ComparableX509 csr: @@ -328,11 +334,13 @@ class CertificateRequest(jose.JSONObjectWithFields): :ivar tuple authorizations: `tuple` of URIs (`str`) """ + resource_type = 'new-cert' csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) authorizations = jose.Field('authorizations', decoder=tuple) -class CertificateResource(ResourceWithURI): +class CertificateResource(interfaces.ClientRequestableResource, + ResourceWithURI): """Certificate Resource. :ivar acme.jose.util.ComparableX509 body: @@ -341,17 +349,20 @@ class CertificateResource(ResourceWithURI): :ivar tuple authzrs: `tuple` of `AuthorizationResource`. """ + resource_type = 'cert' cert_chain_uri = jose.Field('cert_chain_uri') authzrs = jose.Field('authzrs') -class Revocation(jose.JSONObjectWithFields): +class Revocation(interfaces.ClientRequestableResource, + jose.JSONObjectWithFields): """Revocation message. :ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` """ + resource_type = 'revoke-cert' certificate = jose.Field( 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)