[tor-commits] [stem/master] Start using the cryptography package for verifying Ed25519 signatures

atagar at torproject.org atagar at torproject.org
Wed Apr 10 18:03:33 UTC 2019


commit f58577435c91b56f9381f07cb6486a17f9b7ad9a
Author: Illia Volochii <illia.volochii at gmail.com>
Date:   Wed Apr 10 00:23:34 2019 +0300

    Start using the cryptography package for verifying Ed25519 signatures
---
 docs/faq.rst                         |  4 +---
 requirements.txt                     |  1 -
 run_tests.py                         |  1 -
 stem/descriptor/certificate.py       | 29 ++++++++++++++---------------
 stem/descriptor/server_descriptor.py |  2 +-
 stem/prereq.py                       | 26 ++++++++++++++------------
 test/require.py                      |  3 +--
 test/settings.cfg                    |  2 --
 test/task.py                         |  2 --
 test/unit/descriptor/certificate.py  |  6 +++---
 10 files changed, 34 insertions(+), 42 deletions(-)

diff --git a/docs/faq.rst b/docs/faq.rst
index 23d0c3f1..ceaeaf78 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -53,8 +53,7 @@ Does Stem have any dependencies?
 **No.** All you need in order to use Stem is Python.
 
 When it is available Stem will use `cryptography
-<https://pypi.python.org/pypi/cryptography>`_ and `PyNaCl
-<https://pypi.python.org/pypi/PyNaCl/>`_ to validate descriptor signatures.
+<https://pypi.python.org/pypi/cryptography>`_ to validate descriptor signatures.
 However, there is no need to install cryptography unless you need this
 functionality.
 
@@ -81,7 +80,6 @@ Ubuntu you can install these with...
 
   % sudo apt-get install python-dev libffi-dev
   % sudo pip install cryptography
-  % sudo pip install pynacl
 
 .. _what_python_versions_is_stem_compatible_with:
 
diff --git a/requirements.txt b/requirements.txt
index 3cd71608..6dc054ca 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,4 +3,3 @@ pyflakes
 pycodestyle
 tox
 cryptography
-pynacl
diff --git a/run_tests.py b/run_tests.py
index e8dd1a82..c31e7b48 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -163,7 +163,6 @@ def main():
     test.task.PYTHON_VERSION,
     test.task.PLATFORM_VERSION,
     test.task.CRYPTO_VERSION,
-    test.task.PYNACL_VERSION,
     test.task.MOCK_VERSION,
     test.task.PYFLAKES_VERSION,
     test.task.PYCODESTYLE_VERSION,
diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py
index 37829bc4..2f62e889 100644
--- a/stem/descriptor/certificate.py
+++ b/stem/descriptor/certificate.py
@@ -224,34 +224,33 @@ class Ed25519CertificateV1(Ed25519Certificate):
 
     :raises:
       * **ValueError** if signing key or descriptor are invalid
-      * **ImportError** if pynacl module is unavailable
+      * **ImportError** if cryptography module is unavailable or ed25519 is not supported
     """
 
-    if not stem.prereq._is_pynacl_available():
-      raise ImportError('Certificate validation requires the pynacl module')
+    if not stem.prereq._is_crypto_ed25519_supported():
+      raise ImportError('Certificate validation requires the cryptography module and support of ed25519')
 
-    import nacl.signing
-    import nacl.encoding
-    from nacl.exceptions import BadSignatureError
+    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+    from cryptography.exceptions import InvalidSignature
 
     descriptor_content = server_descriptor.get_bytes()
     signing_key = None
 
     if server_descriptor.ed25519_master_key:
-      signing_key = nacl.signing.VerifyKey(stem.util.str_tools._to_bytes(server_descriptor.ed25519_master_key) + b'=', encoder = nacl.encoding.Base64Encoder)
+      signing_key = Ed25519PublicKey.from_public_bytes(base64.b64decode(stem.util.str_tools._to_bytes(server_descriptor.ed25519_master_key) + b'='))
     else:
       for extension in self.extensions:
         if extension.type == ExtensionType.HAS_SIGNING_KEY:
-          signing_key = nacl.signing.VerifyKey(extension.data)
+          signing_key = Ed25519PublicKey.from_public_bytes(extension.data)
           break
 
     if not signing_key:
       raise ValueError('Server descriptor missing an ed25519 signing key')
 
     try:
-      signing_key.verify(base64.b64decode(stem.util.str_tools._to_bytes(self.encoded))[:-ED25519_SIGNATURE_LENGTH], self.signature)
-    except BadSignatureError as exc:
-      raise ValueError('Ed25519KeyCertificate signing key is invalid (%s)' % exc)
+      signing_key.verify(self.signature, base64.b64decode(stem.util.str_tools._to_bytes(self.encoded))[:-ED25519_SIGNATURE_LENGTH])
+    except InvalidSignature:
+      raise ValueError('Ed25519KeyCertificate signing key is invalid (Signature was forged or corrupt)')
 
     # ed25519 signature validates descriptor content up until the signature itself
 
@@ -265,7 +264,7 @@ class Ed25519CertificateV1(Ed25519Certificate):
     signature_bytes = base64.b64decode(stem.util.str_tools._to_bytes(server_descriptor.ed25519_signature) + b'=' * missing_padding)
 
     try:
-      verify_key = nacl.signing.VerifyKey(self.key)
-      verify_key.verify(descriptor_sha256_digest, signature_bytes)
-    except BadSignatureError as exc:
-      raise ValueError('Descriptor Ed25519 certificate signature invalid (%s)' % exc)
+      verify_key = Ed25519PublicKey.from_public_bytes(self.key)
+      verify_key.verify(signature_bytes, descriptor_sha256_digest)
+    except InvalidSignature:
+      raise ValueError('Descriptor Ed25519 certificate signature invalid (Signature was forged or corrupt)')
diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
index 6cc0e775..fc03a711 100644
--- a/stem/descriptor/server_descriptor.py
+++ b/stem/descriptor/server_descriptor.py
@@ -865,7 +865,7 @@ class RelayDescriptor(ServerDescriptor):
           if onion_key_crosscert_digest != self._onion_key_crosscert_digest():
             raise ValueError('Decrypted onion-key-crosscert digest does not match local digest (calculated: %s, local: %s)' % (onion_key_crosscert_digest, self._onion_key_crosscert_digest()))
 
-      if stem.prereq._is_pynacl_available() and self.certificate:
+      if stem.prereq._is_crypto_ed25519_supported() and self.certificate:
         self.certificate.validate(self)
 
   @classmethod
diff --git a/stem/prereq.py b/stem/prereq.py
index 594e4239..c2b546dc 100644
--- a/stem/prereq.py
+++ b/stem/prereq.py
@@ -29,7 +29,7 @@ import sys
 CRYPTO_UNAVAILABLE = "Unable to import the cryptography module. Because of this we'll be unable to verify descriptor signature integrity. You can get cryptography from: https://pypi.python.org/pypi/cryptography"
 ZSTD_UNAVAILABLE = 'ZSTD compression requires the zstandard module (https://pypi.python.org/pypi/zstandard)'
 LZMA_UNAVAILABLE = 'LZMA compression requires the lzma module (https://docs.python.org/3/library/lzma.html)'
-PYNACL_UNAVAILABLE = "Unable to import the pynacl module. Because of this we'll be unable to verify descriptor ed25519 certificate integrity. You can get pynacl from https://pypi.python.org/pypi/PyNaCl/"
+ED25519_UNSUPPORTED = "Unable to verify descriptor ed25519 certificate integrity. ed25519 is not supported by installed versions of OpenSSL and/or cryptography"
 
 
 def check_requirements():
@@ -126,6 +126,7 @@ def is_crypto_available():
   try:
     from cryptography.utils import int_from_bytes, int_to_bytes
     from cryptography.hazmat.backends import default_backend
+    from cryptography.hazmat.backends.openssl.backend import backend
     from cryptography.hazmat.primitives.asymmetric import rsa
     from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
     from cryptography.hazmat.primitives.serialization import load_der_public_key
@@ -239,20 +240,21 @@ def _is_lru_cache_available():
     return hasattr(functools, 'lru_cache')
 
 
-def _is_pynacl_available():
+def _is_crypto_ed25519_supported():
   """
-  Checks if the pynacl functions we use are available. This is used for
-  verifying ed25519 certificates in relay descriptor signatures.
+  Checks if ed25519 is supported by current versions of the cryptography
+  package and OpenSSL. This is used for verifying ed25519 certificates in relay
+  descriptor signatures.
 
-  :returns: **True** if we can use pynacl and **False** otherwise
+  :returns: **True** if ed25519 is supported and **False** otherwise
   """
-
   from stem.util import log
 
-  try:
-    from nacl import encoding
-    from nacl import signing
-    return True
-  except ImportError:
-    log.log_once('stem.prereq._is_pynacl_available', log.INFO, PYNACL_UNAVAILABLE)
+  if not is_crypto_available():
     return False
+
+  from cryptography.hazmat.backends.openssl.backend import backend
+  supported = hasattr(backend, 'ed25519_supported') and backend.ed25519_supported()
+  if not supported:
+    log.log_once('stem.prereq._is_crypto_ed25519_supported', log.INFO, ED25519_UNSUPPORTED)
+  return supported
diff --git a/test/require.py b/test/require.py
index e6a0bbcc..1117ea1e 100644
--- a/test/require.py
+++ b/test/require.py
@@ -12,7 +12,6 @@ run.
   |- needs - skips the test unless a requirement is met
   |
   |- cryptography - skips test unless the cryptography module is present
-  |- pynacl - skips test unless the pynacl module is present
   |- command - requires a command to be on the path
   |- proc - requires the platform to have recognized /proc contents
   |
@@ -99,7 +98,7 @@ def version(req_version):
 
 
 cryptography = needs(stem.prereq.is_crypto_available, 'requires cryptography')
-pynacl = needs(stem.prereq._is_pynacl_available, 'requires pynacl module')
+ed25519_support = needs(stem.prereq._is_crypto_ed25519_supported, 'requires ed25519 support')
 proc = needs(stem.util.proc.is_available, 'proc unavailable')
 controller = needs(_can_access_controller, 'no connection')
 ptrace = needs(_can_ptrace, 'DisableDebuggerAttachment is set')
diff --git a/test/settings.cfg b/test/settings.cfg
index d463cac9..6bdf9394 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -177,8 +177,6 @@ pyflakes.ignore stem/prereq.py => 'cryptography.hazmat.primitives.ciphers.modes'
 pyflakes.ignore stem/prereq.py => 'cryptography.hazmat.primitives.ciphers.Cipher' imported but unused
 pyflakes.ignore stem/prereq.py => 'cryptography.hazmat.primitives.ciphers.algorithms' imported but unused
 pyflakes.ignore stem/prereq.py => 'lzma' imported but unused
-pyflakes.ignore stem/prereq.py => 'nacl.encoding' imported but unused
-pyflakes.ignore stem/prereq.py => 'nacl.signing' imported but unused
 pyflakes.ignore stem/response/events.py => undefined name 'long'
 pyflakes.ignore stem/util/__init__.py => undefined name 'long'
 pyflakes.ignore stem/util/__init__.py => undefined name 'unicode'
diff --git a/test/task.py b/test/task.py
index c527a0c7..7b75aa47 100644
--- a/test/task.py
+++ b/test/task.py
@@ -14,7 +14,6 @@
   |- PYTHON_VERSION - checks our python version
   |- PLATFORM_VERSION - checks our operating system version
   |- CRYPTO_VERSION - checks our version of cryptography
-  |- PYNACL_VERSION - checks our version of pynacl
   |- MOCK_VERSION - checks our version of mock
   |- PYFLAKES_VERSION - checks our version of pyflakes
   |- PYCODESTYLE_VERSION - checks our version of pycodestyle
@@ -313,7 +312,6 @@ TOR_VERSION = Task('tor version', _check_tor_version)
 PYTHON_VERSION = Task('python version', _check_python_version)
 PLATFORM_VERSION = Task('operating system', _check_platform_version)
 CRYPTO_VERSION = ModuleVersion('cryptography version', 'cryptography', stem.prereq.is_crypto_available)
-PYNACL_VERSION = ModuleVersion('pynacl version', 'nacl', stem.prereq._is_pynacl_available)
 MOCK_VERSION = ModuleVersion('mock version', ['unittest.mock', 'mock'], stem.prereq.is_mock_available)
 PYFLAKES_VERSION = ModuleVersion('pyflakes version', 'pyflakes')
 PYCODESTYLE_VERSION = ModuleVersion('pycodestyle version', ['pycodestyle', 'pep8'])
diff --git a/test/unit/descriptor/certificate.py b/test/unit/descriptor/certificate.py
index 28e0d20b..ca0a626e 100644
--- a/test/unit/descriptor/certificate.py
+++ b/test/unit/descriptor/certificate.py
@@ -157,7 +157,7 @@ class TestEd25519Certificate(unittest.TestCase):
     exc_msg = 'Ed25519 HAS_SIGNING_KEY extension must be 32 bytes, but was 2.'
     self.assertRaisesWith(ValueError, exc_msg, Ed25519Certificate.parse, certificate(extension_data = [b'\x00\x02\x04\x07\11\12']))
 
-  @test.require.pynacl
+  @test.require.ed25519_support
   def test_validation_with_descriptor_key(self):
     """
     Validate a descriptor signature using the ed25519 master key within the
@@ -169,7 +169,7 @@ class TestEd25519Certificate(unittest.TestCase):
 
     desc.certificate.validate(desc)
 
-  @test.require.pynacl
+  @test.require.ed25519_support
   def test_validation_with_embedded_key(self):
     """
     Validate a descriptor signature using the signing key within the ed25519
@@ -182,7 +182,7 @@ class TestEd25519Certificate(unittest.TestCase):
     desc.ed25519_master_key = None
     desc.certificate.validate(desc)
 
-  @test.require.pynacl
+  @test.require.ed25519_support
   def test_validation_with_invalid_descriptor(self):
     """
     Validate a descriptor without a valid signature.





More information about the tor-commits mailing list