[tor-commits] [stem/master] Unit tests for ed25519 crypto validation

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


commit ee2330f349d038e9961d7f9afa528dd6c890bf86
Author: Damian Johnson <atagar at torproject.org>
Date:   Wed Mar 29 19:43:57 2017 +0200

    Unit tests for ed25519 crypto validation
    
    A server descriptor unit test indirectly provides some coverage, but adding
    three tests that specifically focus on the validate() method. Also adding some
    quick docs.
---
 stem/descriptor/certificate.py      |  3 ++
 test/unit/descriptor/certificate.py | 96 ++++++++++++++++++++++++++++++++++++-
 2 files changed, 97 insertions(+), 2 deletions(-)

diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py
index bab0a44..d49df50 100644
--- a/stem/descriptor/certificate.py
+++ b/stem/descriptor/certificate.py
@@ -231,6 +231,9 @@ class Ed25519CertificateV1(Ed25519Certificate):
 
     # ed25519 signature validates descriptor content up until the signature itself
 
+    if b'router-sig-ed25519 ' not in descriptor_content:
+      raise ValueError("Descriptor doesn't have a router-sig-ed25519 entry.")
+
     signed_content = descriptor_content[:descriptor_content.index(b'router-sig-ed25519 ') + 19]
     descriptor_sha256_digest = hashlib.sha256(ED25519_ROUTER_SIGNATURE_PREFIX + signed_content).digest()
 
diff --git a/test/unit/descriptor/certificate.py b/test/unit/descriptor/certificate.py
index f46965a..9226f30 100644
--- a/test/unit/descriptor/certificate.py
+++ b/test/unit/descriptor/certificate.py
@@ -12,6 +12,7 @@ import stem.prereq
 import test.runner
 
 from stem.descriptor.certificate import ED25519_SIGNATURE_LENGTH, CertType, ExtensionType, ExtensionFlag, Ed25519Certificate, Ed25519CertificateV1, Ed25519Extension
+from test.unit.descriptor import get_resource
 
 ED25519_CERT = """
 AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABnprVR
@@ -25,6 +26,14 @@ EXPECTED_SIGNATURE = '\xc6\x8e\xd3\xae\x0b?\xedJ6\xe2\xef\x95\xcf,\x18o%N<u\x83\
 
 
 def certificate(version = 1, cert_type = 4, extension_data = []):
+  """
+  Provides base64 encoded Ed25519 certifificate content.
+
+  :param int version: certificate version
+  :param int cert_type: certificate type
+  :param list extension_data: extensions to embed within the certificate
+  """
+
   return base64.b64encode(''.join([
     chr(version),
     chr(cert_type),
@@ -41,6 +50,10 @@ class TestEd25519Certificate(unittest.TestCase):
     self.assertRaisesRegexp(ValueError, re.escape(exc_msg), Ed25519Certificate.parse, parse_arg)
 
   def test_basic_parsing(self):
+    """
+    Parse a basic test certificate.
+    """
+
     signing_key = b'\x11' * 32
     cert_bytes = certificate(extension_data = [b'\x00\x20\x04\x07' + signing_key, b'\x00\x00\x05\x04'])
     cert = Ed25519Certificate.parse(cert_bytes)
@@ -63,6 +76,10 @@ class TestEd25519Certificate(unittest.TestCase):
     self.assertTrue(cert.is_expired())
 
   def test_with_real_cert(self):
+    """
+    Parse a certificate from a real server descriptor.
+    """
+
     cert = Ed25519Certificate.parse(ED25519_CERT)
 
     self.assertEqual(Ed25519CertificateV1, type(cert))
@@ -76,29 +93,104 @@ class TestEd25519Certificate(unittest.TestCase):
     self.assertEqual(EXPECTED_SIGNATURE, cert.signature)
 
   def test_non_base64(self):
+    """
+    Parse data that isn't base64 encoded.
+    """
+
     self.assert_raises('\x02\x0323\x04', "Ed25519 certificate wasn't propoerly base64 encoded (Incorrect padding):")
 
   def test_too_short(self):
+    """
+    Parse data that's too short to be a valid certificate.
+    """
+
     self.assert_raises('', "Ed25519 certificate wasn't propoerly base64 encoded (empty):")
     self.assert_raises('AQQABhtZAaW2GoBED1IjY3A6', 'Ed25519 certificate was 18 bytes, but should be at least 104')
 
   def test_with_invalid_version(self):
+    """
+    We cannot support other certificate versions until they're documented.
+    Assert we raise if we don't handle a cert version yet.
+    """
+
     self.assert_raises(certificate(version = 2), 'Ed25519 certificate is version 2. Parser presently only supports version 1.')
 
   def test_with_invalid_cert_type(self):
+    """
+    Provide an invalid certificate version. Tor specifies a couple ranges that
+    are reserved.
+    """
+
     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):
+    """
+    Include an extension without as much data as it specifies.
+    """
+
     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):
+    """
+    Include an extension with more data than it specifies.
+    """
+
+    self.assert_raises(certificate(extension_data = [b'\x00\x01\x00\x00\x15\x12']), "Ed25519 certificate had 1 bytes of unused extension data")
+
   def test_truncated_signing_key(self):
+    """
+    Include an extension with an incorrect signing key size.
+    """
+
     self.assert_raises(certificate(extension_data = [b'\x00\x02\x04\x07\11\12']), "Ed25519 HAS_SIGNING_KEY extension must be 32 bytes, but was 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")
+  def test_validation_with_descriptor_key(self):
+    """
+    Validate a descriptor signature using the ed25519 master key within the
+    descriptor.
+    """
+
+    if not stem.prereq._is_pynacl_available():
+      test.runner.skip(self, '(requires pynacl module)')
+      return
+
+    with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file:
+      desc = next(stem.descriptor.parse_file(descriptor_file, validate = False))
+
+    desc.certificate.validate(desc)
+
+  def test_validation_with_embedded_key(self):
+    """
+    Validate a descriptor signature using the signing key within the ed25519
+    certificate.
+    """
+
+    if not stem.prereq._is_pynacl_available():
+      test.runner.skip(self, '(requires pynacl module)')
+      return
+
+    with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file:
+      desc = next(stem.descriptor.parse_file(descriptor_file, validate = False))
+
+    desc.ed25519_master_key = None
+    desc.certificate.validate(desc)
+
+  def test_validation_with_invalid_descriptor(self):
+    """
+    Validate a descriptor without a valid signature.
+    """
+
+    if not stem.prereq._is_pynacl_available():
+      test.runner.skip(self, '(requires pynacl module)')
+      return
+
+    with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file:
+      desc = next(stem.descriptor.parse_file(descriptor_file, validate = False))
 
+    cert = Ed25519Certificate.parse(certificate())
+    self.assertRaisesRegexp(ValueError, re.escape('Ed25519KeyCertificate signing key is invalid (Signature was forged or corrupt)'), cert.validate, desc)
 
 
 class TestCertificate(unittest.TestCase):





More information about the tor-commits mailing list