
commit e0eab2e3bce1dd404c7435b920485f2781146c90 Author: Patrick O'Doherty <p@trickod.com> Date: Sat Mar 11 19:02:45 2017 -0800 Fix Python 3 compatibility issues & broken unit test. Fix a number of exceptions caused by mixing str and bytes types, which while OK in Python 2 cause runtime exceptions in Python 3. Fix broken truncated extension parsing test. Not quite sure how this test ever failed, as python's slices (in both 2 and 3) always truncate instead of raising exceptions for out of bounds indices. --- stem/descriptor/certificate.py | 57 +++++++++++++++++----------- stem/descriptor/hidden_service_descriptor.py | 2 +- stem/descriptor/server_descriptor.py | 5 ++- test/unit/descriptor/certificate.py | 15 ++++---- test/unit/manual.py | 3 ++ 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py index 8904b58..09cf1cf 100644 --- a/stem/descriptor/certificate.py +++ b/stem/descriptor/certificate.py @@ -22,6 +22,7 @@ contain one SignedWithEd25519KeyCertificateExtension. """ import base64 +import binascii import hashlib import time @@ -37,11 +38,14 @@ except ImportError: SIGNATURE_LENGTH = 64 STANDARD_ATTRIBUTES_LENGTH = 40 CERTIFICATE_FLAGS_LENGTH = 4 -ED25519_ROUTER_SIGNATURE_PREFIX = 'Tor router descriptor signature v1' +ED25519_ROUTER_SIGNATURE_PREFIX = b'Tor router descriptor signature v1' def _bytes_to_long(b): - return long(b.encode('hex'), 16) + if stem.prereq.is_python_3(): + return int(binascii.hexlify(stem.util.str_tools._to_bytes(b)), 16) + else: + return long(binascii.hexlify(b), 16) def _parse_long_offset(offset, length): @@ -59,21 +63,22 @@ def _parse_offset(offset, length): def _parse_certificate(raw_contents, master_key_bytes, validate = False): - version, cert_type = raw_contents[0:2] + version = raw_contents[0:1] + cert_type = raw_contents[1:2] - if version == '\x01': - if cert_type == '\x04': + if version == b'\x01': + if cert_type == b'\x04': return Ed25519KeyCertificate(raw_contents, master_key_bytes, validate = validate) - elif cert_type == '\x05': + elif cert_type == b'\x05': # TLS link certificated signed with ed25519 signing key pass - elif cert_type == '\x06': + elif cert_type == b'\x06': # Ed25519 authentication signed with ed25519 signing key pass else: - raise ValueError("Unknown Certificate type %s" % cert_type.encode('hex')) + raise ValueError("Unknown Certificate type %s" % binascii.hexlify(cert_type)) else: - raise ValueError("Unknown Certificate version %s" % version.encode('hex')) + raise ValueError("Unknown Certificate version %s" % binascii.hexlify(version)) def _parse_extensions(raw_contents): @@ -83,18 +88,19 @@ def _parse_extensions(raw_contents): extensions = [] extension_bytes = raw_contents[STANDARD_ATTRIBUTES_LENGTH:-SIGNATURE_LENGTH] + while len(extension_bytes) > 0: - try: - ext_length = _bytes_to_long(extension_bytes[0:2]) - ext_type, ext_flags = extension_bytes[2:CERTIFICATE_FLAGS_LENGTH] - ext_data = extension_bytes[CERTIFICATE_FLAGS_LENGTH:(CERTIFICATE_FLAGS_LENGTH + ext_length)] - except: + ext_length = _bytes_to_long(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' % ext_type.encode('hex')) + raise ValueError('Invalid certificate extension type: %s' % binascii.hexlify(ext_type)) extensions.append(extension) extension_bytes = extension_bytes[CERTIFICATE_FLAGS_LENGTH + ext_length:] @@ -127,19 +133,23 @@ class Certificate(object): def __init__(self, raw_contents, identity_key, validate = False): self.certificate_bytes = raw_contents - self.identity_key = identity_key + + 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.iteritems(): + 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.iteritems(): + for key, value in entries.items(): setattr(self, key, value) @@ -167,7 +177,7 @@ class Ed25519KeyCertificate(Certificate): 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('router-sig-ed25519 ') + len('router-sig-ed25519 ')] + 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() @@ -181,10 +191,11 @@ class Ed25519KeyCertificate(Certificate): 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(base64.b64decode(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) @@ -195,7 +206,7 @@ class Ed25519KeyCertificate(Certificate): class CertificateExtension(object): - KNOWN_TYPES = ['\x04'] + KNOWN_TYPES = [b'\x04'] def __init__(self, ext_type, ext_flags, ext_data): self.ext_type = ext_type @@ -206,11 +217,11 @@ class CertificateExtension(object): return self.ext_type in CertificateExtension.KNOWN_TYPES def affects_validation(self): - return self.ext_flags == '\x01' + return self.ext_flags == b'\x01' class SignedWithEd25519KeyCertificateExtension(CertificateExtension): - TYPE = '\x04' + 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/stem/descriptor/hidden_service_descriptor.py b/stem/descriptor/hidden_service_descriptor.py index dc42286..9c87500 100644 --- a/stem/descriptor/hidden_service_descriptor.py +++ b/stem/descriptor/hidden_service_descriptor.py @@ -326,7 +326,7 @@ class HiddenServiceDescriptor(Descriptor): # try decrypting the session key - cipher = Cipher(algorithms.AES(authentication_cookie), modes.CTR(stem.util.str_tools._to_bytes('\x00' * len(iv))), default_backend()) + cipher = Cipher(algorithms.AES(authentication_cookie), modes.CTR(b'\x00' * len(iv)), default_backend()) decryptor = cipher.decryptor() session_key = decryptor.update(encrypted_session_key) + decryptor.finalize() diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py index a695966..9cd709b 100644 --- a/stem/descriptor/server_descriptor.py +++ b/stem/descriptor/server_descriptor.py @@ -35,6 +35,7 @@ import functools import hashlib import re import base64 +import binascii import stem.descriptor.extrainfo_descriptor import stem.exit_policy @@ -800,8 +801,8 @@ class RelayDescriptor(ServerDescriptor): """ signing_key_digest = hashlib.sha1(_bytes_for_block(self.signing_key)).digest() - data = signing_key_digest + base64.b64decode(self.ed25519_master_key + b'=') - return data.encode('hex').upper() + data = signing_key_digest + base64.b64decode(self.ed25519_master_key + '=') + return stem.util.str_tools._to_unicode(binascii.hexlify(data).upper()) def _compare(self, other, method): if not isinstance(other, RelayDescriptor): diff --git a/test/unit/descriptor/certificate.py b/test/unit/descriptor/certificate.py index 61f6068..4e71e84 100644 --- a/test/unit/descriptor/certificate.py +++ b/test/unit/descriptor/certificate.py @@ -11,7 +11,7 @@ import test.runner class TestCertificate(unittest.TestCase): def test_with_invalid_version(self): - cert_bytes = '\x02\x04' + cert_bytes = b'\x02\x04' self.assertRaisesRegexp( ValueError, 'Unknown Certificate version', @@ -21,7 +21,7 @@ class TestCertificate(unittest.TestCase): ) def test_with_invalid_type(self): - cert_bytes = '\x01\x07' + cert_bytes = b'\x01\x07' self.assertRaisesRegexp( ValueError, 'Unknown Certificate type', @@ -34,13 +34,14 @@ class TestCertificate(unittest.TestCase): cert_bytes = '\x00' * 39 # First 40 bytes are standard fields cert_bytes += '\x01' # n_extensions = 1 cert_bytes += '\x00\x08' # extension length = 8 bytes + cert_bytes += '\x04' # ext_type = 0x04 cert_bytes += stem.descriptor.certificate.SIGNATURE_LENGTH * '\x00' # pad empty signature block self.assertRaisesRegexp( ValueError, 'Certificate contained truncated extension', stem.descriptor.certificate._parse_extensions, - cert_bytes + stem.util.str_tools._to_bytes(cert_bytes) ) def test_parse_extensions_invalid_certificate_extension_type(self): @@ -54,7 +55,7 @@ class TestCertificate(unittest.TestCase): ValueError, 'Invalid certificate extension type:', stem.descriptor.certificate._parse_extensions, - cert_bytes + stem.util.str_tools._to_bytes(cert_bytes) ) def test_parse_extensions_invalid_n_extensions_count(self): @@ -69,7 +70,7 @@ class TestCertificate(unittest.TestCase): ValueError, 'n_extensions was 2 but parsed 1', stem.descriptor.certificate._parse_extensions, - cert_bytes + stem.util.str_tools._to_bytes(cert_bytes) ) def test_ed25519_key_certificate_without_extensions(self): @@ -81,7 +82,7 @@ class TestCertificate(unittest.TestCase): ValueError, 'Ed25519KeyCertificate missing SignedWithEd25519KeyCertificateExtension extension', stem.descriptor.certificate._parse_certificate, - cert_bytes, + stem.util.str_tools._to_bytes(cert_bytes), None, validate = True ) @@ -107,7 +108,7 @@ class TestCertificate(unittest.TestCase): ValueError, 'Ed25519KeyCertificate signature invalid', stem.descriptor.certificate._parse_certificate, - cert_bytes, + stem.util.str_tools._to_bytes(cert_bytes), master_key_base64, validate = True ) diff --git a/test/unit/manual.py b/test/unit/manual.py index 5f939c3..671ed93 100644 --- a/test/unit/manual.py +++ b/test/unit/manual.py @@ -173,6 +173,9 @@ class TestManual(unittest.TestCase): self.assertEqual(EXPECTED_CLI_OPTIONS, manual.commandline_options) self.assertEqual(EXPECTED_SIGNALS, manual.signals) self.assertEqual(EXPECTED_FILES, manual.files) + if EXPECTED_CONFIG_OPTIONS != manual.config_options: + print(EXPECTED_CONFIG_OPTIONS) + print(manual.config_options) self.assertEqual(EXPECTED_CONFIG_OPTIONS, manual.config_options) def test_parsing_with_unknown_options(self):