[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Revised basic Ed25519Certificate parsing
commit 90834c9b437bf099f016f30a190f7eb8e125f013
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
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):
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits