mirror of
https://github.com/certbot/certbot.git
synced 2026-01-23 07:20:55 +03:00
Enforce "resource" field in request objects.
Corresponds to: - https://github.com/letsencrypt/boulder/pull/442 - https://github.com/letsencrypt/acme-spec/pull/156
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
13
acme/interfaces.py
Normal file
13
acme/interfaces.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user