[tor-commits] [stem/master] Support Ed25519 extensions

atagar at torproject.org atagar at torproject.org
Thu Mar 30 04:18:03 UTC 2017


commit 1423b6d5d714b9ce08eb5096dd3a6dbd6c6e5e5d
Author: Damian Johnson <atagar at 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")
 
 
 





More information about the tor-commits mailing list