commit 000e48aa900e38994c3443773addd08017c3c91b Author: Damian Johnson atagar@torproject.org Date: Thu Mar 30 04:18:34 2017 +0200
Drop previous ed25519 certificate implementation
Now that we support everything it did time to drop the prior implementation. --- stem/descriptor/certificate.py | 256 ++++-------------------------------- test/unit/descriptor/certificate.py | 65 --------- 2 files changed, 24 insertions(+), 297 deletions(-)
diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py index d49df50..4a279a3 100644 --- a/stem/descriptor/certificate.py +++ b/stem/descriptor/certificate.py @@ -13,8 +13,14 @@ used to validate the key used to sign server descriptors. ::
Ed25519Certificate - Ed25519 signing key certificate + | +- Ed25519CertificateV1 - version 1 Ed25519 certificate + | |- is_expired - checks if certificate is presently expired + | +- validate - validates signature of a server descriptor + | +- parse - reads base64 encoded certificate data
+ Ed25519Extension - extension included within an Ed25519Certificate + .. data:: CertType (enum)
Purpose of Ed25519 certificate. As new certificate versions are added this @@ -53,16 +59,30 @@ used to validate the key used to sign server descriptors. import base64 import collections import datetime +import hashlib
-from stem.util import enum +import stem.prereq +import stem.util.enum +import stem.util.str_tools
ED25519_HEADER_LENGTH = 40 ED25519_SIGNATURE_LENGTH = 64 ED25519_ROUTER_SIGNATURE_PREFIX = b'Tor router descriptor signature v1'
-CertType = enum.UppercaseEnum('SIGNING', 'LINK_CERT', 'AUTH') -ExtensionType = enum.Enum(('HAS_SIGNING_KEY', 4),) -ExtensionFlag = enum.UppercaseEnum('AFFECTS_VALIDATION', 'UNKNOWN') +CertType = stem.util.enum.UppercaseEnum('SIGNING', 'LINK_CERT', 'AUTH') +ExtensionType = stem.util.enum.Enum(('HAS_SIGNING_KEY', 4),) +ExtensionFlag = stem.util.enum.UppercaseEnum('AFFECTS_VALIDATION', 'UNKNOWN') + + +class Ed25519Extension(collections.namedtuple('Ed25519Extension', ['type', 'flags', 'flag_int', 'data'])): + """ + Extension within an Ed25519 certificate. + + :var int type: extension type + :var list flags: extension attribute flags + :var int flag_int: integer encoding of the extension attribute flags + :var bytes data: data the extension concerns + """
class Ed25519Certificate(object): @@ -245,231 +265,3 @@ class Ed25519CertificateV1(Ed25519Certificate): verify_key.verify(descriptor_sha256_digest, signature_bytes) except BadSignatureError as exc: raise ValueError('Descriptor Ed25519 certificate signature invalid (%s)' % exc) - - -class Ed25519Extension(collections.namedtuple('Ed25519Extension', ['type', 'flags', 'flag_int', 'data'])): - """ - Extension within an Ed25519 certificate. - - :var int type: extension type - :var list flags: extension attribute flags - :var int flag_int: integer encoding of the extension attribute flags - :var bytes data: data the extension concerns - """ - - - - - - - -""" -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 - - CertificateExtension - Certificate extension - +- SignedWithEd25519KeyCertificateExtension - Ed25519 signing key extension -""" - -import binascii -import hashlib -import time - -import stem.prereq -import stem.util.str_tools - -try: - # added in python 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - -SIGNATURE_LENGTH = 64 -STANDARD_ATTRIBUTES_LENGTH = 40 -CERTIFICATE_FLAGS_LENGTH = 4 -ED25519_ROUTER_SIGNATURE_PREFIX = b'Tor router descriptor signature v1' - - -def _parse_long_offset(offset, length): - def _parse(raw_contents): - return stem.util.str_tools._to_int(raw_contents[offset:(offset + length)]) - - return _parse - - -def _parse_offset(offset, length): - def _parse(raw_contents): - return raw_contents[offset:(offset + length)] - - return _parse - - -def _parse_certificate(raw_contents, master_key_bytes, validate = False): - version = raw_contents[0:1] - cert_type = raw_contents[1:2] - - if version == b'\x01': - if cert_type == b'\x04': - return Ed25519KeyCertificate(raw_contents, master_key_bytes, validate = validate) - elif cert_type == b'\x05': - # TLS link certificated signed with ed25519 signing key - pass - elif cert_type == b'\x06': - # Ed25519 authentication signed with ed25519 signing key - pass - else: - raise ValueError('Unknown Certificate type %s' % binascii.hexlify(cert_type)) - else: - raise ValueError('Unknown Certificate version %s' % binascii.hexlify(version)) - - -def _parse_extensions(raw_contents): - n_extensions = stem.util.str_tools._to_int(raw_contents[39:40]) - - if n_extensions == 0: - return [] - - extensions = [] - extension_bytes = raw_contents[STANDARD_ATTRIBUTES_LENGTH:-SIGNATURE_LENGTH] - - while len(extension_bytes) > 0: - ext_length = stem.util.str_tools._to_int(extension_bytes[0:2]) - ext_type = extension_bytes[2:3] - ext_flags = extension_bytes[3:CERTIFICATE_FLAGS_LENGTH] - ext_data = extension_bytes[CERTIFICATE_FLAGS_LENGTH:(CERTIFICATE_FLAGS_LENGTH + ext_length)] - if len(ext_type) == 0 or len(ext_flags) == 0 or len(ext_data) == 0: - raise ValueError('Certificate contained truncated extension') - - if ext_type == SignedWithEd25519KeyCertificateExtension.TYPE: - extension = SignedWithEd25519KeyCertificateExtension(ext_type, ext_flags, ext_data) - else: - raise ValueError('Invalid certificate extension type: %s' % binascii.hexlify(ext_type)) - - extensions.append(extension) - extension_bytes = extension_bytes[CERTIFICATE_FLAGS_LENGTH + ext_length:] - - if len(extensions) != n_extensions: - raise ValueError('n_extensions was %d but parsed %d' % (n_extensions, len(extensions))) - - return extensions - - -def _parse_signature(cert): - return cert[-SIGNATURE_LENGTH:] - - -class Certificate(object): - """ - See proposal #220 https://gitweb.torproject.org/torspec.git/tree/proposals/220-ecc-id-keys.txt - """ - - ATTRIBUTES = { - 'version': _parse_offset(0, 1), - 'cert_type': _parse_offset(1, 1), - 'expiration_date': _parse_long_offset(2, 4), - 'cert_key_type': _parse_offset(6, 1), - 'certified_key': _parse_offset(7, 32), - 'n_extensions': _parse_long_offset(39, 1), - 'extensions': _parse_extensions, - 'signature': _parse_signature - } - - def __init__(self, raw_contents, identity_key, validate = False): - self.certificate_bytes = raw_contents - - if type(identity_key) == bytes: - self.identity_key = stem.util.str_tools._to_unicode(identity_key) - else: - self.identity_key = identity_key - - self.__set_certificate_entries(raw_contents) - - def __set_certificate_entries(self, raw_contents): - entries = OrderedDict() - for key, func in Certificate.ATTRIBUTES.items(): - try: - entries[key] = func(raw_contents) - except IndexError: - raise ValueError('Unable to get bytes for %s from certificate' % key) - - for key, value in entries.items(): - setattr(self, key, value) - - -class Ed25519KeyCertificate(Certificate): - def __init__(self, raw_contents, identity_key, validate = False): - super(Ed25519KeyCertificate, self).__init__(raw_contents, identity_key, validate = False) - - if validate: - if len(self.extensions) == 0: - raise ValueError('Ed25519KeyCertificate missing SignedWithEd25519KeyCertificateExtension extension') - - self._verify_signature() - - if (self.expiration_date * 3600) < int(time.time()): - raise ValueError('Expired Ed25519KeyCertificate') - - def verify_descriptor_signature(self, descriptor, signature): - if not stem.prereq._is_pynacl_available(): - raise ValueError('Certificate validation requires the pynacl module') - - import nacl.signing - from nacl.exceptions import BadSignatureError - - missing_padding = len(signature) % 4 - signature_bytes = base64.b64decode(stem.util.str_tools._to_bytes(signature) + b'=' * missing_padding) - verify_key = nacl.signing.VerifyKey(self.certified_key) - - signed_part = descriptor[:descriptor.index(b'router-sig-ed25519 ') + len('router-sig-ed25519 ')] - descriptor_with_prefix = ED25519_ROUTER_SIGNATURE_PREFIX + signed_part - descriptor_sha256_digest = hashlib.sha256(descriptor_with_prefix).digest() - - try: - verify_key.verify(descriptor_sha256_digest, signature_bytes) - except BadSignatureError: - raise ValueError('Descriptor Ed25519 certificate signature invalid') - - def _verify_signature(self): - if not stem.prereq._is_pynacl_available(): - raise ValueError('Certificate validation requires the pynacl module') - - import nacl.signing - import nacl.encoding - from nacl.exceptions import BadSignatureError - - if self.identity_key: - verify_key = nacl.signing.VerifyKey(self.identity_key + '=', encoder=nacl.encoding.Base64Encoder) - else: - verify_key = nacl.singing.VerifyKey(self.extensions[0].ext_data) - - try: - verify_key.verify(self.certificate_bytes[:-SIGNATURE_LENGTH], self.signature) - except BadSignatureError: - raise ValueError('Ed25519KeyCertificate signature invalid') - - -class CertificateExtension(object): - KNOWN_TYPES = [b'\x04'] - - def __init__(self, ext_type, ext_flags, ext_data): - self.ext_type = ext_type - self.ext_flags = ext_flags - self.ext_data = ext_data - - def is_known_type(self): - return self.ext_type in CertificateExtension.KNOWN_TYPES - - def affects_validation(self): - return self.ext_flags == b'\x01' - - -class SignedWithEd25519KeyCertificateExtension(CertificateExtension): - TYPE = b'\x04' - - def __init__(self, ext_type, ext_flags, ext_data): - super(SignedWithEd25519KeyCertificateExtension, self).__init__(ext_type, ext_flags, ext_data) diff --git a/test/unit/descriptor/certificate.py b/test/unit/descriptor/certificate.py index 9226f30..6cbc3b2 100644 --- a/test/unit/descriptor/certificate.py +++ b/test/unit/descriptor/certificate.py @@ -191,68 +191,3 @@ class TestEd25519Certificate(unittest.TestCase):
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): - def test_with_invalid_version(self): - cert_bytes = b'\x02\x04' - self.assertRaisesRegexp(ValueError, 'Unknown Certificate version', stem.descriptor.certificate._parse_certificate, cert_bytes, None) - - def test_with_invalid_type(self): - cert_bytes = b'\x01\x07' - self.assertRaisesRegexp(ValueError, 'Unknown Certificate type', stem.descriptor.certificate._parse_certificate, cert_bytes, None) - - def test_parse_extensions_truncated_extension(self): - cert_bytes = b'\x00' * 39 # First 40 bytes are standard fields - cert_bytes += b'\x01' # n_extensions = 1 - cert_bytes += b'\x00\x08' # extension length = 8 bytes - cert_bytes += b'\x04' # ext_type = 0x04 - cert_bytes += stem.descriptor.certificate.SIGNATURE_LENGTH * b'\x00' # pad empty signature block - - self.assertRaisesRegexp(ValueError, 'Certificate contained truncated extension', stem.descriptor.certificate._parse_extensions, cert_bytes) - - def test_parse_extensions_invalid_certificate_extension_type(self): - cert_bytes = b'\x00' * 39 # First 40 bytes are standard fields - cert_bytes += b'\x01' # n_extensions = 1 - cert_bytes += b'\x00\x08' # extension length = 8 bytes - cert_bytes += b'\x00' * 6 # pad out to 8 bytes - cert_bytes += stem.descriptor.certificate.SIGNATURE_LENGTH * b'\x00' # pad empty signature block - - self.assertRaisesRegexp(ValueError, 'Invalid certificate extension type:', stem.descriptor.certificate._parse_extensions, cert_bytes) - - def test_parse_extensions_invalid_n_extensions_count(self): - cert_bytes = b'\x00' * 39 # First 40 bytes are standard fields - cert_bytes += b'\x02' # n_extensions = 2 - cert_bytes += b'\x00\x08' # extension length = 8 bytes - cert_bytes += b'\x04' # certificate type - cert_bytes += b'\x00' * 5 # pad out to 8 bytes - cert_bytes += stem.descriptor.certificate.SIGNATURE_LENGTH * b'\x00' # pad empty signature block - - self.assertRaisesRegexp(ValueError, 'n_extensions was 2 but parsed 1', stem.descriptor.certificate._parse_extensions, cert_bytes) - - def test_ed25519_key_certificate_without_extensions(self): - cert_bytes = b'\x01\x04' + b'\x00' * 37 # First 40 bytes are standard fields - cert_bytes += b'\x00' # n_extensions = 0 - cert_bytes += stem.descriptor.certificate.SIGNATURE_LENGTH * b'\x00' # pad empty signature block - - exc_msg = 'Ed25519KeyCertificate missing SignedWithEd25519KeyCertificateExtension extension' - self.assertRaisesRegexp(ValueError, exc_msg, stem.descriptor.certificate._parse_certificate, cert_bytes, None, validate = True) - - def test_certificate_with_invalid_signature(self): - if not stem.prereq._is_pynacl_available(): - test.runner.skip(self, '(requires pynacl module)') - return - - import nacl.signing - import nacl.encoding - - master_key = nacl.signing.SigningKey.generate() - master_key_base64 = master_key.encode(nacl.encoding.Base64Encoder) - - cert_bytes = b'\x01\x04' + b'\x00' * 37 # 40 byte preamble of standard fields - cert_bytes += b'\x01' # n_extensions = 1 - cert_bytes += b'\x00\x08' # extentsion length = 8 bytes - cert_bytes += b'\x04' + b'\x00' * 5 # certificate type + padding out to 8 bytes - cert_bytes += stem.descriptor.certificate.SIGNATURE_LENGTH * b'\x00' # empty signature block - - self.assertRaisesRegexp(ValueError, 'Ed25519KeyCertificate signature invalid', stem.descriptor.certificate._parse_certificate, cert_bytes, master_key_base64, validate = True)