diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 8024728fa..8d8d21f9d 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -1,9 +1,7 @@ """ACME Identifier Validation Challenges.""" -import binascii import functools import hashlib import logging -import os import requests @@ -163,8 +161,7 @@ class SimpleHTTPResponse(ChallengeResponse): class DVSNI(DVChallenge): """ACME "dvsni" challenge. - :ivar str r: Random data, **not** base64-encoded. - :ivar str nonce: Random data, **not** hex-encoded. + :ivar str token: Random data, **not** base64-encoded. """ typ = "dvsni" @@ -172,25 +169,15 @@ class DVSNI(DVChallenge): DOMAIN_SUFFIX = ".acme.invalid" """Domain name suffix.""" - R_SIZE = 32 - """Required size of the :attr:`r` in bytes.""" - - NONCE_SIZE = 16 - """Required size of the :attr:`nonce` in bytes.""" + TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec + """Minimum size of the :attr:`token` in bytes.""" PORT = 443 """Port to perform DVSNI challenge.""" - r = jose.Field("r", encoder=jose.b64encode, # pylint: disable=invalid-name - decoder=functools.partial(jose.decode_b64jose, size=R_SIZE)) - nonce = jose.Field("nonce", encoder=binascii.hexlify, - decoder=functools.partial(functools.partial( - jose.decode_hex16, size=NONCE_SIZE))) - - @property - def nonce_domain(self): - """Domain name used in SNI.""" - return binascii.hexlify(self.nonce) + self.DOMAIN_SUFFIX + token = jose.Field( + "token", encoder=jose.b64encode, decoder=functools.partial( + jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) @ChallengeResponse.register @@ -205,31 +192,22 @@ class DVSNIResponse(ChallengeResponse): DOMAIN_SUFFIX = DVSNI.DOMAIN_SUFFIX """Domain name suffix.""" - S_SIZE = 32 - """Required size of the :attr:`s` in bytes.""" + validation = jose.Field("validation", decoder=jose.JWS.from_json) - s = jose.Field("s", encoder=jose.b64encode, # pylint: disable=invalid-name - decoder=functools.partial(jose.decode_b64jose, size=S_SIZE)) + @property + def z(self): # pylint: disable=invalid-name + """The ``z`` parameter.""" + # Instance of 'Field' has no 'signature' member + # pylint: disable=no-member + return hashlib.sha256(self.validation.signature.encode( + "signature")).hexdigest() - def __init__(self, s=None, *args, **kwargs): - s = os.urandom(self.S_SIZE) if s is None else s - super(DVSNIResponse, self).__init__(s=s, *args, **kwargs) - - def z(self, chall): # pylint: disable=invalid-name - """Compute the parameter ``z``. - - :param challenge: Corresponding challenge. - :type challenge: :class:`DVSNI` - - """ - z = hashlib.new("sha256") # pylint: disable=invalid-name - z.update(chall.r) - z.update(self.s) - return z.hexdigest() - - def z_domain(self, chall): + @property + def z_domain(self): """Domain name for certificate subjectAltName.""" - return self.z(chall) + self.DOMAIN_SUFFIX + z = self.z # pylint: disable=invalid-name + return "{0}.{1}{2}".format(z[:32], z[32:], self.DOMAIN_SUFFIX) + @Challenge.register class RecoveryContact(ContinuityChallenge): diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index a1214c2f9..eae9eedea 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -145,19 +145,12 @@ class DVSNITest(unittest.TestCase): def setUp(self): from acme.challenges import DVSNI self.msg = DVSNI( - r="O*\xb4-\xad\xec\x95>\xed\xa9\r0\x94\xe8\x97\x9c&6" - "\xbf'\xb3\xed\x9a9nX\x0f'\\m\xe7\x12", - nonce='\xa8-_\xf8\xeft\r\x12\x88\x1fm<"w\xab.') + token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) self.jmsg = { 'type': 'dvsni', - 'r': 'Tyq0La3slT7tqQ0wlOiXnCY2vyez7Zo5blgPJ1xt5xI', - 'nonce': 'a82d5ff8ef740d12881f6d3c2277ab2e', + 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', } - def test_nonce_domain(self): - self.assertEqual('a82d5ff8ef740d12881f6d3c2277ab2e.acme.invalid', - self.msg.nonce_domain) - def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) @@ -169,15 +162,9 @@ class DVSNITest(unittest.TestCase): from acme.challenges import DVSNI hash(DVSNI.from_json(self.jmsg)) - def test_from_json_invalid_r_length(self): + def test_from_json_invalid_token_length(self): from acme.challenges import DVSNI - self.jmsg['r'] = 'abcd' - self.assertRaises( - jose.DeserializationError, DVSNI.from_json, self.jmsg) - - def test_from_json_invalid_nonce_length(self): - from acme.challenges import DVSNI - self.jmsg['nonce'] = 'abcd' + self.jmsg['token'] = jose.b64encode('abcd') self.assertRaises( jose.DeserializationError, DVSNI.from_json, self.jmsg) @@ -186,37 +173,38 @@ class DVSNIResponseTest(unittest.TestCase): def setUp(self): from acme.challenges import DVSNIResponse - self.msg = DVSNIResponse( - s='\xf5\xd6\xe3\xb2]\xe0L\x0bN\x9cKJ\x14I\xa1K\xa3#\xf9\xa8' - '\xcd\x8c7\x0e\x99\x19)\xdc\xb7\xf3\x9bw') - self.jmsg = { + self.validation = jose.JWS.sign( + payload='foo', key=jose.JWKRSA(key=KEY), alg=jose.RS256) + self.msg = DVSNIResponse(validation=self.validation) + self.jmsg_to = { 'type': 'dvsni', - 's': '9dbjsl3gTAtOnEtKFEmhS6Mj-ajNjDcOmRkp3Lfzm3c', + 'validation': self.validation, + } + self.jmsg_from = { + 'type': 'dvsni', + 'validation': self.validation.to_json(), } def test_z_and_domain(self): - from acme.challenges import DVSNI - challenge = DVSNI( - r="O*\xb4-\xad\xec\x95>\xed\xa9\r0\x94\xe8\x97\x9c&6" - "\xbf'\xb3\xed\x9a9nX\x0f'\\m\xe7\x12", - nonce=long('439736375371401115242521957580409149254868992063' - '44333654741504362774620418661L')) # pylint: disable=invalid-name - z = '38e612b0397cc2624a07d351d7ef50e46134c0213d9ed52f7d7c611acaeed41b' - self.assertEqual(z, self.msg.z(challenge)) + z = '94b209f1b27afe1cb40f27c9ce7c1b4d75786fe6e380524c0bb80009f0105e4b' + label1 = '94b209f1b27afe1cb40f27c9ce7c1b4d' + label2 = '75786fe6e380524c0bb80009f0105e4b' + assert z == label1 + label2 + self.assertEqual(z, self.msg.z) self.assertEqual( - '{0}.acme.invalid'.format(z), self.msg.z_domain(challenge)) + '{0}.{1}.acme.invalid'.format(label1, label2), self.msg.z_domain) def test_to_partial_json(self): - self.assertEqual(self.jmsg, self.msg.to_partial_json()) + self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DVSNIResponse - self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg)) + self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg_from)) def test_from_json_hashable(self): from acme.challenges import DVSNIResponse - hash(DVSNIResponse.from_json(self.jmsg)) + hash(DVSNIResponse.from_json(self.jmsg_from)) class RecoveryContactTest(unittest.TestCase): diff --git a/acme/acme/jose/json_util.py b/acme/acme/jose/json_util.py index fe3831296..be852b675 100644 --- a/acme/acme/jose/json_util.py +++ b/acme/acme/jose/json_util.py @@ -218,6 +218,22 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable): super(JSONObjectWithFields, self).__init__( **(dict(self._defaults(), **kwargs))) + def encode(self, name): + """Encode a single field. + + :param str name: Name of the field to be encoded. + + :raises erors.SerializationError: if field cannot be serialized + :raises errors.Error: if field could not be found + + """ + try: + field = self._fields[name] + except KeyError: + raise errors.Error("Field not found: {0}".format(name)) + + return field.encode(getattr(self, name)) + def fields_to_partial_json(self): """Serialize fields to JSON.""" jobj = {} diff --git a/acme/acme/jose/json_util_test.py b/acme/acme/jose/json_util_test.py index 9e2a87858..3eaa80b84 100644 --- a/acme/acme/jose/json_util_test.py +++ b/acme/acme/jose/json_util_test.py @@ -159,6 +159,18 @@ class JSONObjectWithFieldsTest(unittest.TestCase): def test_init_defaults(self): self.assertEqual(self.mock, self.MockJSONObjectWithFields(y=2, z=3)) + def test_encode(self): + self.assertEqual(10, self.MockJSONObjectWithFields( + x=5, y=0, z=0).encode("x")) + + def test_encode_wrong_field(self): + self.assertRaises(errors.Error, self.mock.encode, 'foo') + + def test_encode_serialization_error_passthrough(self): + self.assertRaises( + errors.SerializationError, + self.MockJSONObjectWithFields(y=500, z=None).encode, "y") + def test_fields_to_partial_json_omits_empty(self): self.assertEqual(self.mock.fields_to_partial_json(), {'y': 2, 'Z': 3})