[tor-commits] [stem/master] Revised basic Ed25519Certificate parsing

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


commit 90834c9b437bf099f016f30a190f7eb8e125f013
Author: Damian Johnson <atagar at torproject.org>
Date:   Sun Mar 26 13:41:43 2017 +0200

    Revised basic Ed25519Certificate parsing
    
    The present parser is good but retooling it to...
    
      * Not require pynacl for basic certificate parsing (like the cryptography
        module that's only necessary if the user wants validation).
    
      * Be more ameanable to future versions by extending a common
        Ed25519Certificate base class.
    
      * Use an enumeration for the certificate type.
    
      * Use a datetime object for the expiration time.
    
      * Add additional tests and clarify the exception messages a bit.
    
    Far from done. Just handling a handful of the basic attributes. Still needs
    to fold in extensions and of course validation. Leaving the prior
    implementation around to help with that.
---
 stem/descriptor/certificate.py      | 132 ++++++++++++++++++++++++++++++++++--
 test/settings.cfg                   |   2 +-
 test/unit/descriptor/certificate.py |  67 ++++++++++++++++++
 3 files changed, 194 insertions(+), 7 deletions(-)

diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py
index 191e87e..f9fb6ec 100644
--- a/stem/descriptor/certificate.py
+++ b/stem/descriptor/certificate.py
@@ -2,17 +2,138 @@
 # See LICENSE for licensing information
 
 """
-Parsing for Tor Ed25519 certificates, which is used to validate the key used to
-sign server descriptors.
+Parsing for Tor Ed25519 certificates, which are used to validate the key used
+to sign server descriptors.
 
-Certificates can optionally contain CertificateExtension objects depending on
-their type and purpose. Currently Ed25519KeyCertificate certificates will
-contain one SignedWithEd25519KeyCertificateExtension.
+.. versionadded:: 1.6.0
 
 **Module Overview:**
 
 ::
 
+  Ed25519Certificate - Ed25519 signing key certificate
+    +- parse - reads base64 encoded certificate data
+
+.. data:: CertType (enum)
+
+  Purpose of Ed25519 certificate. As new certificate versions are added this
+  enumeration will expand.
+
+  ==============  ===========
+  CertType        Description
+  ==============  ===========
+  **SIGNING**     signing a signing key with an identity key
+  **LINK_CERT**   TLS link certificate signed with ed25519 signing key
+  **AUTH**        authentication key signed with ed25519 signing key
+  ==============  ===========
+"""
+
+import base64
+import datetime
+
+from stem.util import enum
+
+ED25519_HEADER_LENGTH = 40
+ED25519_SIGNATURE_LENGTH = 64
+
+CertType = enum.UppercaseEnum('SIGNING', 'LINK_CERT', 'AUTH')
+
+
+class Ed25519Certificate(object):
+  """
+  Base class for an Ed25519 certificate.
+
+  :var int version: certificate format version
+  :var str encoded: base64 encoded ed25519 certificate
+  """
+
+  def __init__(self, version, encoded):
+    self.version = version
+    self.encoded = encoded
+
+  @staticmethod
+  def parse(content):
+    """
+    Parses the given base64 encoded data as an Ed25519 certificate.
+
+    :param str content: base64 encoded certificate
+
+    :returns: :class:`~stem.descriptor.certificate.Ed25519Certificate` subclsss
+      for the given certificate
+
+    :raises: **ValueError** if content is malformed
+    """
+
+    try:
+      decoded = base64.b64decode(content)
+
+      if not decoded:
+        raise TypeError('empty')
+    except TypeError as exc:
+      raise ValueError("Ed25519 certificate wasn't propoerly base64 encoded (%s):\n%s" % (exc, content))
+
+    version = stem.util.str_tools._to_int(decoded[0])
+
+    if version == 1:
+      return Ed25519CertificateV1(version, content, decoded)
+    else:
+      raise ValueError('Ed25519 certificate is version %i. Parser presently only supports version 1.' % version)
+
+
+class Ed25519CertificateV1(Ed25519Certificate):
+  """
+  Version 1 Ed25519 certificate, which are used for signing tor server
+  descriptors.
+
+  :var CertType cert_type: certificate purpose
+  :var datetime expiration: expiration of the certificate
+  :var int key_type: format of the key
+  :var bytes key: key content
+  """
+
+  def __init__(self, version, encoded, decoded):
+    super(Ed25519CertificateV1, self).__init__(version, encoded)
+
+    if len(decoded) < ED25519_HEADER_LENGTH + ED25519_SIGNATURE_LENGTH:
+      raise ValueError('Ed25519 certificate was %i bytes, but should be at least %i' % (len(decoded), ED25519_HEADER_LENGTH + ED25519_SIGNATURE_LENGTH))
+
+    cert_type = stem.util.str_tools._to_int(decoded[1])
+
+    if cert_type in (0, 1, 2, 3):
+      raise ValueError('Ed25519 certificate cannot have a type of %i. This is reserved to avoid conflicts with tor CERTS cells.' % cert_type)
+    elif cert_type == 4:
+      self.cert_type = CertType.SIGNING
+    elif cert_type == 5:
+      self.cert_type = CertType.LINK_CERT
+    elif cert_type == 6:
+      self.cert_type = CertType.AUTH
+    elif cert_type == 7:
+      raise ValueError('Ed25519 certificate cannot have a type of 7. This is reserved for RSA identity cross-certification.')
+    else:
+      raise ValueError("BUG: Ed25519 certificate type is decoded from one byte. It shouldn't be possible to have a value of %i." % cert_type)
+
+    # expiration time is in hours since epoch
+    self.expiration = datetime.datetime.fromtimestamp(stem.util.str_tools._to_int(decoded[2:6]) * 60 * 60)
+
+    self.key_type = stem.util.str_tools._to_int(decoded[6])
+    self.key = decoded[7:39]
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+Certificates can optionally contain CertificateExtension objects depending on
+their type and purpose. Currently Ed25519KeyCertificate certificates will
+contain one SignedWithEd25519KeyCertificateExtension.
+
   Certificate - Tor Certificate
     +- Ed25519KeyCertificate - Certificate for Ed25519 signing key
        +- verify_descriptor_signature - verify a relay descriptor against a signature
@@ -21,7 +142,6 @@ contain one SignedWithEd25519KeyCertificateExtension.
     +- SignedWithEd25519KeyCertificateExtension - Ed25519 signing key extension
 """
 
-import base64
 import binascii
 import hashlib
 import time
diff --git a/test/settings.cfg b/test/settings.cfg
index c953773..1d011f9 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -194,7 +194,7 @@ test.unit_tests
 |test.unit.descriptor.networkstatus.document_v3.TestNetworkStatusDocument
 |test.unit.descriptor.networkstatus.bridge_document.TestBridgeNetworkStatusDocument
 |test.unit.descriptor.hidden_service_descriptor.TestHiddenServiceDescriptor
-|test.unit.descriptor.certificate.TestCertificate
+|test.unit.descriptor.certificate.TestEd25519Certificate
 |test.unit.exit_policy.rule.TestExitPolicyRule
 |test.unit.exit_policy.policy.TestExitPolicy
 |test.unit.version.TestVersion
diff --git a/test/unit/descriptor/certificate.py b/test/unit/descriptor/certificate.py
index 2f2f728..5297579 100644
--- a/test/unit/descriptor/certificate.py
+++ b/test/unit/descriptor/certificate.py
@@ -2,12 +2,79 @@
 Unit tests for stem.descriptor.certificate.
 """
 
+import base64
+import datetime
+import re
 import unittest
 
 import stem.descriptor.certificate
 import stem.prereq
 import test.runner
 
+from stem.descriptor.certificate import ED25519_SIGNATURE_LENGTH, CertType, Ed25519Certificate, Ed25519CertificateV1
+
+ED25519_CERT = """
+AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABnprVR
+ptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8sGG8lTjx1
+g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98Ljhdp2w4=
+""".strip()
+
+
+def certificate(version = 1, cert_type = 4):
+  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]))
+
+
+class TestEd25519Certificate(unittest.TestCase):
+  def assert_raises(self, parse_arg, exc_msg):
+    self.assertRaisesRegexp(ValueError, re.escape(exc_msg), Ed25519Certificate.parse, parse_arg)
+
+  def test_basic_parsing(self):
+    cert_bytes = certificate()
+    cert = Ed25519Certificate.parse(cert_bytes)
+
+    self.assertEqual(Ed25519CertificateV1, type(cert))
+    self.assertEqual(1, cert.version)
+    self.assertEqual(cert_bytes, cert.encoded)
+    self.assertEqual(CertType.SIGNING, cert.cert_type)
+    self.assertEqual(datetime.datetime(1970, 1, 1, 1, 0), cert.expiration)
+    self.assertEqual(1, cert.key_type)
+    self.assertEqual(b'\x03' * 32, cert.key)
+
+  def test_with_real_cert(self):
+    cert = Ed25519Certificate.parse(ED25519_CERT)
+
+    self.assertEqual(Ed25519CertificateV1, type(cert))
+    self.assertEqual(1, cert.version)
+    self.assertEqual(ED25519_CERT, cert.encoded)
+    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)
+
+  def test_non_base64(self):
+    self.assert_raises('\x02\x0323\x04', "Ed25519 certificate wasn't propoerly base64 encoded (Incorrect padding):")
+
+  def test_too_short(self):
+    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):
+    self.assert_raises(certificate(version = 2), 'Ed25519 certificate is version 2. Parser presently only supports version 1.')
+
+  def test_with_invalid_cert_type(self):
+    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.')
+
+
+
+
+
 
 class TestCertificate(unittest.TestCase):
   def test_with_invalid_version(self):





More information about the tor-commits mailing list