commit 90834c9b437bf099f016f30a190f7eb8e125f013 Author: Damian Johnson atagar@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):