[tor-commits] [stem/master] Consistently document when cryptography is required

atagar at torproject.org atagar at torproject.org
Wed Feb 26 23:02:10 UTC 2020


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





More information about the tor-commits mailing list