commit 682fdf18ae921bde1cebb3514d794ca6e18ab9d5 Author: Damian Johnson atagar@torproject.org Date: Tue Feb 18 15:54:48 2020 -0800
Consistently document when cryptography is required
Audited our library's cryptography imports for exception handling when unavailable, and consistently document when it's required. --- stem/client/__init__.py | 2 ++ stem/descriptor/certificate.py | 7 +++-- stem/descriptor/hidden_service.py | 50 ++++++++++++++++++------------- stem/util/__init__.py | 2 +- test/unit/descriptor/hidden_service_v2.py | 4 +-- test/unit/descriptor/remote.py | 9 ++++++ test/unit/descriptor/server_descriptor.py | 40 +++++++++++++++++++++---- 7 files changed, 82 insertions(+), 32 deletions(-)
diff --git a/stem/client/__init__.py b/stem/client/__init__.py index 7456726a..57cd3457 100644 --- a/stem/client/__init__.py +++ b/stem/client/__init__.py @@ -300,6 +300,8 @@ class Circuit(object): :var hashlib.sha1 backward_digest: digest for backward integrity check :var bytes forward_key: forward encryption key :var bytes backward_key: backward encryption key + + :raises: **ImportError** if the cryptography module is unavailable """
def __init__(self, relay, circ_id, kdf): diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py index ae2d6636..0522b883 100644 --- a/stem/descriptor/certificate.py +++ b/stem/descriptor/certificate.py @@ -247,6 +247,9 @@ class Ed25519CertificateV1(Ed25519Certificate):
:param bytes signature: pre-calculated certificate signature :param cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey signing_key: certificate signing key + + :raises: **ImportError** if key is a cryptographic type and ed25519 support + is unavailable """
def __init__(self, cert_type = None, expiration = None, key_type = None, key = None, extensions = None, signature = None, signing_key = None): @@ -364,7 +367,7 @@ class Ed25519CertificateV1(Ed25519Certificate): :raises: * **ValueError** if signing key or descriptor are invalid * **TypeError** if descriptor type is unsupported - * **ImportError** if cryptography module or ed25519 support unavailable + * **ImportError** if cryptography module with ed25519 support is unavailable """
import stem.descriptor.server_descriptor @@ -373,7 +376,7 @@ class Ed25519CertificateV1(Ed25519Certificate): from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.exceptions import InvalidSignature except ImportError: - raise ImportError('Certificate validation requires the cryptography module and ed25519 support') + raise ImportError('Certificate validation requires cryptography 2.6 or later')
if isinstance(descriptor, stem.descriptor.server_descriptor.RelayDescriptor): signed_content = hashlib.sha256(Ed25519CertificateV1._signed_content(descriptor)).digest() diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py index 980222a3..75a78d2e 100644 --- a/stem/descriptor/hidden_service.py +++ b/stem/descriptor/hidden_service.py @@ -214,7 +214,9 @@ class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_s
:returns: :class:`~stem.descriptor.hidden_service.IntroductionPointV3` with these attributes
- :raises: **ValueError** if the address, port, or keys are malformed + :raises: + * **ValueError** if the address, port, or keys are malformed + * **ImportError** if cryptography module with ed25519 support is unavailable """
if not stem.util.connection.is_valid_port(port): @@ -244,14 +246,16 @@ class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_s
:returns: :class:`~stem.descriptor.hidden_service.IntroductionPointV3` with these attributes
- :raises: **ValueError** if the address, port, or keys are malformed + :raises: + * **ValueError** if the address, port, or keys are malformed + * **ImportError** if cryptography module with ed25519 support is unavailable """
try: from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey except ImportError: - raise ImportError('Introduction point creation requires the cryptography module ed25519 support') + raise ImportError('Introduction point creation requires cryptography 2.6 or later')
if expiration is None: expiration = datetime.datetime.utcnow() + datetime.timedelta(hours = stem.descriptor.certificate.DEFAULT_EXPIRATION_HOURS) @@ -302,7 +306,7 @@ class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_s :returns: ntor :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
:raises: - * **ImportError** if required the cryptography module is unavailable + * **ImportError** if cryptography module with ed25519 support is unavailable * **EnvironmentError** if OpenSSL x25519 unsupported """
@@ -315,7 +319,7 @@ class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_s :returns: :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey`
:raises: - * **ImportError** if required the cryptography module is unavailable + * **ImportError** if cryptography module with ed25519 support is unavailable * **EnvironmentError** if OpenSSL x25519 unsupported """
@@ -328,7 +332,7 @@ class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_s :returns: encryption :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
:raises: - * **ImportError** if required the cryptography module is unavailable + * **ImportError** if cryptography module with ed25519 support is unavailable * **EnvironmentError** if OpenSSL x25519 unsupported """
@@ -341,7 +345,7 @@ class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_s :returns: legacy :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
:raises: - * **ImportError** if required the cryptography module is unavailable + * **ImportError** if cryptography module with ed25519 support is unavailable * **EnvironmentError** if OpenSSL x25519 unsupported """
@@ -356,7 +360,7 @@ class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_s from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey except ImportError: - raise ImportError('cryptography module unavailable') + raise ImportError('Key parsing requires cryptography 2.6 or later')
if x25519: if not X25519_AVAILABLE: @@ -507,8 +511,11 @@ def _encrypt_layer(plaintext, constant, revision_counter, subcredential, blinded
def _layer_cipher(constant, revision_counter, subcredential, blinded_key, salt): - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.backends import default_backend + try: + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.backends import default_backend + except ImportError: + raise ImportError('Layer encryption/decryption requires the cryptography module')
kdf = hashlib.shake_256(blinded_key + subcredential + struct.pack('>Q', revision_counter) + salt + constant) keys = kdf.digest(S_KEY_LEN + S_IV_LEN + MAC_LEN) @@ -964,13 +971,13 @@ class HiddenServiceDescriptorV3(HiddenServiceDescriptor):
:raises: * **ValueError** if parameters are malformed - * **ImportError** if cryptography is unavailable + * **ImportError** if cryptography module with ed25519 support is unavailable """
try: from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey except ImportError: - raise ImportError('Hidden service descriptor creation requires cryptography version 2.6') + raise ImportError('Hidden service descriptor creation requires cryptography 2.6 or later')
if blinding_nonce and len(blinding_nonce) != 32: raise ValueError('Blinding nonce must be 32 bytes, but was %i' % len(blinding_nonce)) @@ -1058,9 +1065,7 @@ class HiddenServiceDescriptorV3(HiddenServiceDescriptor): :returns: :class:`~stem.descriptor.hidden_service.InnerLayer` with our decrypted content
- :raises: - * **ImportError** if required cryptography or sha3 module is unavailable - * **ValueError** if unable to decrypt or validation fails + :raises: **ValueError** if unable to decrypt or validation fails """
if self._inner_layer is None: @@ -1091,7 +1096,8 @@ class HiddenServiceDescriptorV3(HiddenServiceDescriptor):
:returns: **unicode** hidden service address
- :raises: **ImportError** if sha3 unsupported + :raises: **ImportError** if key is a cryptographic type and ed25519 support + is unavailable """
key = stem.util._pubkey_bytes(key) # normalize key into bytes @@ -1111,9 +1117,7 @@ class HiddenServiceDescriptorV3(HiddenServiceDescriptor):
:returns: **bytes** for the hidden service's public identity key
- :raises: - * **ImportError** if sha3 unsupported - * **ValueError** if address malformed or checksum is invalid + :raises: **ValueError** if address malformed or checksum is invalid """
if onion_address.endswith('.onion'): @@ -1202,7 +1206,7 @@ class OuterLayer(Descriptor): from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey except ImportError: - raise ImportError('Hidden service layer creation requires cryptography version 2.6') + raise ImportError('Hidden service layer creation requires cryptography 2.6 or later')
if authorized_clients and 'auth-client' in attr: raise ValueError('Authorized clients cannot be specified through both attr and authorized_clients') @@ -1336,7 +1340,11 @@ def _blinded_pubkey(identity_key, blinding_nonce):
def _blinded_sign(msg, identity_key, blinded_key, blinding_nonce): - from cryptography.hazmat.primitives import serialization + try: + from cryptography.hazmat.primitives import serialization + except ImportError: + raise ImportError('Key signing requires the cryptography module') + from stem.util import ed25519
identity_key_bytes = identity_key.private_bytes( diff --git a/stem/util/__init__.py b/stem/util/__init__.py index cde49de7..e4e08174 100644 --- a/stem/util/__init__.py +++ b/stem/util/__init__.py @@ -91,7 +91,7 @@ def _pubkey_bytes(key): from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey except ImportError: - raise ImportError('Key normalization requires the cryptography module with ed25519 support') + raise ImportError('Key normalization requires cryptography 2.6 or later')
if isinstance(key, (X25519PrivateKey, Ed25519PrivateKey)): return key.public_key().public_bytes( diff --git a/test/unit/descriptor/hidden_service_v2.py b/test/unit/descriptor/hidden_service_v2.py index 7112648b..ee2a9111 100644 --- a/test/unit/descriptor/hidden_service_v2.py +++ b/test/unit/descriptor/hidden_service_v2.py @@ -250,7 +250,7 @@ class TestHiddenServiceDescriptorV2(unittest.TestCase): """
with open(get_resource('hidden_service_duckduckgo'), 'rb') as descriptor_file: - desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True)) + desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True, skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE)) self._assert_matches_duckduckgo(desc)
def test_for_duckduckgo_without_validation(self): @@ -268,7 +268,7 @@ class TestHiddenServiceDescriptorV2(unittest.TestCase): """
with open(get_resource('hidden_service_facebook'), 'rb') as descriptor_file: - desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True)) + desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor 1.0', validate = True, skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE))
self.assertEqual('utjk4arxqg6s6zzo7n6cjnq6ot34udhr', desc.descriptor_id) self.assertEqual(2, desc.version) diff --git a/test/unit/descriptor/remote.py b/test/unit/descriptor/remote.py index 70f88b52..d3f43995 100644 --- a/test/unit/descriptor/remote.py +++ b/test/unit/descriptor/remote.py @@ -12,6 +12,7 @@ import stem import stem.descriptor import stem.descriptor.remote import stem.util.str_tools +import test.require
from unittest.mock import patch, Mock, MagicMock
@@ -114,6 +115,7 @@ class TestDescriptorDownloader(unittest.TestCase): reply = stem.descriptor.remote.their_server_descriptor( endpoints = [stem.ORPort('12.34.56.78', 1100)], validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, )
self.assertEqual(1, len(list(reply))) @@ -137,12 +139,14 @@ class TestDescriptorDownloader(unittest.TestCase): stem.descriptor.remote.their_server_descriptor( endpoints = [stem.ORPort('12.34.56.78', 1100)], validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, ).run()
with patch('stem.client.Relay.connect', _orport_mock(TEST_DESCRIPTOR, response_code_header = b'HTTP/1.0 500 Kaboom\r\n')): request = stem.descriptor.remote.their_server_descriptor( endpoints = [stem.ORPort('12.34.56.78', 1100)], validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, )
self.assertRaisesRegexp(stem.ProtocolError, "^Response should begin with HTTP success, but was 'HTTP/1.0 500 Kaboom'", request.run) @@ -156,6 +160,7 @@ class TestDescriptorDownloader(unittest.TestCase): reply = stem.descriptor.remote.their_server_descriptor( endpoints = [stem.DirPort('12.34.56.78', 1100)], validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, )
self.assertEqual(1, len(list(reply))) @@ -177,6 +182,7 @@ class TestDescriptorDownloader(unittest.TestCase): '9695DFC35FFEB861329B9F1AB04C46397020CE31', compression = Compression.PLAINTEXT, validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, ))
self.assertEqual(1, len(descriptors)) @@ -192,6 +198,7 @@ class TestDescriptorDownloader(unittest.TestCase): '9695DFC35FFEB861329B9F1AB04C46397020CE31', compression = Compression.GZIP, validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, ))
self.assertEqual(1, len(descriptors)) @@ -285,6 +292,7 @@ class TestDescriptorDownloader(unittest.TestCase): endpoints = [stem.DirPort('128.31.0.39', 9131)], compression = Compression.PLAINTEXT, validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, )
self.assertEqual(stem.DirPort('128.31.0.39', 9131), query._pick_endpoint()) @@ -366,6 +374,7 @@ class TestDescriptorDownloader(unittest.TestCase): endpoints = [stem.DirPort('128.31.0.39', 9131)], compression = Compression.PLAINTEXT, validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, )
# check that iterating over the query provides the descriptors each time diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py index 07f50013..867f98ba 100644 --- a/test/unit/descriptor/server_descriptor.py +++ b/test/unit/descriptor/server_descriptor.py @@ -170,7 +170,12 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4= """
with open(get_resource('old_descriptor'), 'rb') as descriptor_file: - desc = next(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0', validate = True)) + desc = next(stem.descriptor.parse_file( + descriptor_file, + 'server-descriptor 1.0', + validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, + ))
self.assertEqual('krypton', desc.nickname) self.assertEqual('3E2F63E2356F52318B536A12B6445373808A5D6C', desc.fingerprint) @@ -219,7 +224,12 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4= """
with open(get_resource('non-ascii_descriptor'), 'rb') as descriptor_file: - desc = next(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0', validate = True)) + desc = next(stem.descriptor.parse_file( + descriptor_file, + 'server-descriptor 1.0', + validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, + ))
self.assertEqual('Coruscant', desc.nickname) self.assertEqual('0B9821545C48E496AEED9ECC0DB506C49FF8158D', desc.fingerprint) @@ -307,7 +317,11 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4= """
with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file: - desc = next(stem.descriptor.parse_file(descriptor_file, validate = True)).make_router_status_entry() + desc = next(stem.descriptor.parse_file( + descriptor_file, + validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, + )).make_router_status_entry()
self.assertEqual(stem.descriptor.router_status_entry.RouterStatusEntryV3, type(desc)) self.assertEqual('destiny', desc.nickname) @@ -339,7 +353,11 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4= """
with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file: - desc = next(stem.descriptor.parse_file(descriptor_file, validate = True)) + desc = next(stem.descriptor.parse_file( + descriptor_file, + validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, + ))
family = set([ '$379FB450010D17078B3766C2273303C358C3A442', @@ -434,7 +452,12 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4= """
with open(get_resource('cr_in_contact_line'), 'rb') as descriptor_file: - desc = next(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0', validate = True)) + desc = next(stem.descriptor.parse_file( + descriptor_file, + 'server-descriptor 1.0', + validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, + ))
self.assertEqual('pogonip', desc.nickname) self.assertEqual('6DABD62BC65D4E6FE620293157FC76968DAB9C9B', desc.fingerprint) @@ -456,7 +479,12 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4= """
with open(get_resource('negative_uptime'), 'rb') as descriptor_file: - desc = next(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0', validate = True)) + desc = next(stem.descriptor.parse_file( + descriptor_file, + 'server-descriptor 1.0', + validate = True, + skip_crypto_validation = not test.require.CRYPTOGRAPHY_AVAILABLE, + ))
self.assertEqual('TipTor', desc.nickname) self.assertEqual('137962D4931DBF08A24E843288B8A155D6D2AEDD', desc.fingerprint)