commit 1423b6d5d714b9ce08eb5096dd3a6dbd6c6e5e5d Author: Damian Johnson atagar@torproject.org Date: Sun Mar 26 15:57:26 2017 +0200
Support Ed25519 extensions
Adding support for Ed25519 extension fields along with a few more tests. --- stem/descriptor/certificate.py | 31 +++++++++++++++++++++++++++++++ test/unit/descriptor/certificate.py | 36 +++++++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 9 deletions(-)
diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py index f9fb6ec..52bb33b 100644 --- a/stem/descriptor/certificate.py +++ b/stem/descriptor/certificate.py @@ -29,6 +29,7 @@ to sign server descriptors. """
import base64 +import collections import datetime
from stem.util import enum @@ -89,6 +90,8 @@ class Ed25519CertificateV1(Ed25519Certificate): :var datetime expiration: expiration of the certificate :var int key_type: format of the key :var bytes key: key content + :var list extensions: :class:`~stem.descriptor.certificate.Ed25519Extension` in this certificate + :var bytes signature: certificate signature """
def __init__(self, version, encoded, decoded): @@ -117,11 +120,39 @@ class Ed25519CertificateV1(Ed25519Certificate):
self.key_type = stem.util.str_tools._to_int(decoded[6]) self.key = decoded[7:39] + self.signature = decoded[-ED25519_SIGNATURE_LENGTH:]
+ self.extensions = [] + extension_count = stem.util.str_tools._to_int(decoded[39]) + remaining_data = decoded[40:-ED25519_SIGNATURE_LENGTH]
+ for i in range(extension_count): + if len(remaining_data) < 4: + raise ValueError('Ed25519 extension is missing header field data')
+ extension_length = stem.util.str_tools._to_int(remaining_data[:2]) + extension_type = stem.util.str_tools._to_int(remaining_data[2]) + extension_flags = stem.util.str_tools._to_int(remaining_data[3]) + extension_data = remaining_data[4:4 + extension_length]
+ if extension_length != len(extension_data): + raise ValueError("Ed25519 extension is truncated. It should have %i bytes of data but there's only %i." % (extension_length, len(extension_data)))
+ self.extensions.append(Ed25519Extension(extension_type, extension_flags, extension_data)) + remaining_data = remaining_data[4 + extension_length:] + + if remaining_data: + raise ValueError('Ed25519 certificate had %i bytes of unused extension data' % len(remaining_data)) + + +class Ed25519Extension(collections.namedtuple('Ed25519Extension', ['extension_type', 'flags', 'data'])): + """ + Extension within an Ed25519 certificate. + + :var int extension_type: extension type + :var int flags: extension attributes + :var bytes data: data the extension concerns + """
diff --git a/test/unit/descriptor/certificate.py b/test/unit/descriptor/certificate.py index 5297579..fde7fa3 100644 --- a/test/unit/descriptor/certificate.py +++ b/test/unit/descriptor/certificate.py @@ -11,7 +11,7 @@ import stem.descriptor.certificate import stem.prereq import test.runner
-from stem.descriptor.certificate import ED25519_SIGNATURE_LENGTH, CertType, Ed25519Certificate, Ed25519CertificateV1 +from stem.descriptor.certificate import ED25519_SIGNATURE_LENGTH, CertType, Ed25519Certificate, Ed25519CertificateV1, Ed25519Extension
ED25519_CERT = """ AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABnprVR @@ -19,15 +19,21 @@ ptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8sGG8lTjx1 g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98Ljhdp2w4= """.strip()
+EXPECTED_CERT_KEY = '\xa5\xb6\x1a\x80D\x0fR#cp:\x7f\xa1\x8d\xa8\x11%\xe4\x0f7|=\x99k\xdb\xa9\x1aG\xb9\xd4\x91\xaa' +EXPECTED_EXTENSION_DATA = 'g\xa6\xb5Q\xa6\xd2+\xe3v\xd6>\x8d\x9f#:7\xb8\xec\xb0~\x83+\xaf*k\xa5\xb9\xb8\x1e\x10\xa4d' +EXPECTED_SIGNATURE = '\xc6\x8e\xd3\xae\x0b?\xedJ6\xe2\xef\x95\xcf,\x18o%N<u\x83\x897\x10\xbb\x96b\x01\xd8YNk\x02&\xbb\x9e^ Q\xf0Y8G\xc7\x01\xf2\x84K\xb9ww\xad\xdd\x04H\xc4_\xdf\x0b\x8e\x17i\xdb\x0e'
-def certificate(version = 1, cert_type = 4): + +def certificate(version = 1, cert_type = 4, extension_data = []): return base64.b64encode(''.join([ chr(version), chr(cert_type), - b'\x00' * 4, # expiration date, leaving this as the epoch - b'\x01', # key type - b'\x03' * 32, # key - b'\x00' + b'\x00' * ED25519_SIGNATURE_LENGTH])) + b'\x00' * 4, # expiration date, leaving this as the epoch + b'\x01', # key type + b'\x03' * 32, # key + chr(len(extension_data)), # extension count + b''.join(extension_data), + b'\x01' * ED25519_SIGNATURE_LENGTH]))
class TestEd25519Certificate(unittest.TestCase): @@ -35,7 +41,7 @@ class TestEd25519Certificate(unittest.TestCase): self.assertRaisesRegexp(ValueError, re.escape(exc_msg), Ed25519Certificate.parse, parse_arg)
def test_basic_parsing(self): - cert_bytes = certificate() + cert_bytes = certificate(extension_data = [b'\x00\x02\x04\x07\x15\x12', b'\x00\x00\x05\x03']) cert = Ed25519Certificate.parse(cert_bytes)
self.assertEqual(Ed25519CertificateV1, type(cert)) @@ -45,6 +51,12 @@ class TestEd25519Certificate(unittest.TestCase): self.assertEqual(datetime.datetime(1970, 1, 1, 1, 0), cert.expiration) self.assertEqual(1, cert.key_type) self.assertEqual(b'\x03' * 32, cert.key) + self.assertEqual(b'\x01' * ED25519_SIGNATURE_LENGTH, cert.signature) + + self.assertEqual([ + Ed25519Extension(extension_type = 4, flags = 7, data = b'\x15\x12'), + Ed25519Extension(extension_type = 5, flags = 3, data = b''), + ], cert.extensions)
def test_with_real_cert(self): cert = Ed25519Certificate.parse(ED25519_CERT) @@ -55,7 +67,9 @@ class TestEd25519Certificate(unittest.TestCase): self.assertEqual(CertType.SIGNING, cert.cert_type) self.assertEqual(datetime.datetime(2015, 8, 28, 19, 0), cert.expiration) self.assertEqual(1, cert.key_type) - self.assertEqual('\xa5\xb6\x1a\x80D\x0fR#cp:\x7f\xa1\x8d\xa8\x11%\xe4\x0f7|=\x99k\xdb\xa9\x1aG\xb9\xd4\x91\xaa', cert.key) + self.assertEqual(EXPECTED_CERT_KEY, cert.key) + self.assertEqual([Ed25519Extension(extension_type = 4, flags = 0, data = EXPECTED_EXTENSION_DATA)], cert.extensions) + self.assertEqual(EXPECTED_SIGNATURE, cert.signature)
def test_non_base64(self): self.assert_raises('\x02\x0323\x04', "Ed25519 certificate wasn't propoerly base64 encoded (Incorrect padding):") @@ -71,8 +85,12 @@ class TestEd25519Certificate(unittest.TestCase): self.assert_raises(certificate(cert_type = 0), 'Ed25519 certificate cannot have a type of 0. This is reserved to avoid conflicts with tor CERTS cells.') self.assert_raises(certificate(cert_type = 7), 'Ed25519 certificate cannot have a type of 7. This is reserved for RSA identity cross-certification.')
+ def test_truncated_extension(self): + self.assert_raises(certificate(extension_data = [b'']), 'Ed25519 extension is missing header field data') + self.assert_raises(certificate(extension_data = [b'\x50\x00\x00\x00\x15\x12']), "Ed25519 extension is truncated. It should have 20480 bytes of data but there's only 2.")
- + def test_extra_extension_data(self): + self.assert_raises(certificate(extension_data = [b'\x00\x01\x00\x00\x15\x12']), "Ed25519 certificate had 1 bytes of unused extension data")
tor-commits@lists.torproject.org