mirror of
https://github.com/certbot/certbot.git
synced 2026-06-13 02:30:25 -04:00
acme: Update DVSNI to v03.
This commit is contained in:
parent
2f2137ef6b
commit
1bb6763595
4 changed files with 69 additions and 75 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue