commit 682fdf18ae921bde1cebb3514d794ca6e18ab9d5
Author: Damian Johnson <atagar(a)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)