commit f58577435c91b56f9381f07cb6486a17f9b7ad9a Author: Illia Volochii illia.volochii@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.