commit 05f0dc8763e18f7aa0c3aca8d209c1a901ffaf94 Author: Damian Johnson atagar@torproject.org Date: Wed Jun 28 09:24:46 2017 -0700
Sign created extrainfo descriptors
Extrainfo descriptors are signed the same way as server descriptors so simple to add support. Good opportunity too for generalizing this. --- stem/descriptor/__init__.py | 81 ++++++++++++++++++++++++++++ stem/descriptor/extrainfo_descriptor.py | 17 ++++-- stem/descriptor/server_descriptor.py | 57 +++++--------------- test/unit/descriptor/extrainfo_descriptor.py | 2 +- 4 files changed, 108 insertions(+), 49 deletions(-)
diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py index 80901af..21e7bd7 100644 --- a/stem/descriptor/__init__.py +++ b/stem/descriptor/__init__.py @@ -39,6 +39,7 @@ Package for parsing and processing descriptor data.
import base64 import codecs +import collections import copy import hashlib import os @@ -99,6 +100,18 @@ DocumentHandler = stem.util.enum.UppercaseEnum( )
+class SigningKey(collections.namedtuple('SigningKey', ['private', 'public', 'public_digest'])): + """ + Key used by relays to sign their server and extrainfo descriptors. + + .. versionadded:: 1.6.0 + + :var cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey private: private key + :var cryptography.hazmat.backends.openssl.rsa._RSAPublicKey public: public key + :var bytes public_digest: block that can be used for the a server descrptor's 'signing-key' field + """ + + def parse_file(descriptor_file, descriptor_type = None, validate = False, document_handler = DocumentHandler.ENTRIES, normalize_newlines = None, **kwargs): """ Simple function to read the descriptor contents from a file, providing an @@ -953,6 +966,74 @@ def _get_pseudo_pgp_block(remaining_contents): return None
+def _signing_key(private_key = None): + """ + Serializes a signing key if we have one. Otherwise this creates a new signing + key we can use to create descriptors. + + :param cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey private_key: private key + + :returns: :class:`~stem.descriptor.__init__.SigningKey` that can be used to + create descriptors + """ + + if not stem.prereq.is_crypto_available(): + raise ImportError('Signing requires the cryptography module') + + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + + if private_key is None: + private_key = rsa.generate_private_key( + public_exponent = 65537, + key_size = 1024, + backend = default_backend(), + ) + + # When signing the cryptography module includes a constant indicating + # the hash algorithm used. Tor doesn't. This causes signature + # validation failures and unfortunately cryptography have no nice way + # of excluding these so we need to mock out part of their internals... + # + # https://github.com/pyca/cryptography/issues/3713 + + def no_op(*args, **kwargs): + return 1 + + private_key._backend._lib.EVP_PKEY_CTX_set_signature_md = no_op + private_key._backend.openssl_assert = no_op + + public_key = private_key.public_key() + public_digest = b'\n' + public_key.public_bytes( + encoding = serialization.Encoding.PEM, + format = serialization.PublicFormat.PKCS1, + ).strip() + + return SigningKey(private_key, public_key, public_digest) + + +def _append_router_signature(content, private_key): + """ + Appends a router signature to a server or extrainfo descriptor. + + :param bytes content: descriptor content up through 'router-signature\n' + :param cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey private_key: + private relay signing key + + :returns: **bytes** with the signed descriptor content + """ + + if not stem.prereq.is_crypto_available(): + raise ImportError('Signing requires the cryptography module') + + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import padding + + signature = base64.b64encode(private_key.sign(content, padding.PKCS1v15(), hashes.SHA1())) + return content + b'\n'.join([b'-----BEGIN SIGNATURE-----'] + stem.util.str_tools._split_by_length(signature, 64) + [b'-----END SIGNATURE-----\n']) + + def _random_ipv4_address(): return '%i.%i.%i.%i' % (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
diff --git a/stem/descriptor/extrainfo_descriptor.py b/stem/descriptor/extrainfo_descriptor.py index c0484ad..3b4e9a4 100644 --- a/stem/descriptor/extrainfo_descriptor.py +++ b/stem/descriptor/extrainfo_descriptor.py @@ -89,6 +89,8 @@ from stem.descriptor import ( _parse_timestamp_line, _parse_forty_character_hex, _parse_key_block, + _signing_key, + _append_router_signature, )
try: @@ -973,11 +975,20 @@ class RelayExtraInfoDescriptor(ExtraInfoDescriptor): })
@classmethod - def content(cls, attr = None, exclude = (), sign = False): + def content(cls, attr = None, exclude = (), sign = False, private_signing_key = None): if sign: - raise NotImplementedError('Signing of %s not implemented' % cls.__name__) + if attr and 'router-signature' in attr: + raise ValueError('Cannot sign the descriptor if a router-signature has been provided')
- return _descriptor_content(attr, exclude, sign, RELAY_EXTRAINFO_HEADER, RELAY_EXTRAINFO_FOOTER) + signing_key = _signing_key(private_signing_key) + content = _descriptor_content(attr, exclude, sign, RELAY_EXTRAINFO_HEADER) + b'\nrouter-signature\n' + return _append_router_signature(content, signing_key.private) + else: + return _descriptor_content(attr, exclude, sign, RELAY_EXTRAINFO_HEADER, RELAY_EXTRAINFO_FOOTER) + + @classmethod + def create(cls, attr = None, exclude = (), validate = True, sign = False, private_signing_key = None): + return cls(cls.content(attr, exclude, sign, private_signing_key), validate = validate)
@lru_cache() def digest(self): diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py index 6ffe914..580b3d3 100644 --- a/stem/descriptor/server_descriptor.py +++ b/stem/descriptor/server_descriptor.py @@ -66,6 +66,8 @@ from stem.descriptor import ( _parse_forty_character_hex, _parse_protocol_line, _parse_key_block, + _signing_key, + _append_router_signature, _random_ipv4_address, _random_date, _random_crypto_blob, @@ -801,6 +803,9 @@ class RelayDescriptor(ServerDescriptor):
@classmethod def content(cls, attr = None, exclude = (), sign = False, private_signing_key = None): + if attr is None: + attr = {} + base_header = ( ('router', 'Unnamed%i %s 9001 0 0' % (random.randint(0, sys.maxint), _random_ipv4_address())), ('published', _random_date()), @@ -810,59 +815,21 @@ class RelayDescriptor(ServerDescriptor): ('signing-key', _random_crypto_blob('RSA PUBLIC KEY')), )
- base_footer = ( - ('router-signature', _random_crypto_blob('SIGNATURE')), - ) - if sign: - if not stem.prereq.is_crypto_available(): - raise ImportError('Signing requires the cryptography module') - elif attr and 'signing-key' in attr: + if attr and 'signing-key' in attr: raise ValueError('Cannot sign the descriptor if a signing-key has been provided') elif attr and 'router-signature' in attr: raise ValueError('Cannot sign the descriptor if a router-signature has been provided')
- from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes, serialization - from cryptography.hazmat.primitives.asymmetric import padding, rsa - - if attr is None: - attr = {} - - if private_signing_key is None: - private_signing_key = rsa.generate_private_key( - public_exponent = 65537, - key_size = 1024, - backend = default_backend(), - ) - - # When signing the cryptography module includes a constant indicating - # the hash algorithm used. Tor doesn't. This causes signature - # validation failures and unfortunately cryptography have no nice way - # of excluding these so we need to mock out part of their internals... - # - # https://github.com/pyca/cryptography/issues/3713 - - def no_op(*args, **kwargs): - return 1 - - private_signing_key._backend._lib.EVP_PKEY_CTX_set_signature_md = no_op - private_signing_key._backend.openssl_assert = no_op - - # create descriptor content without the router-signature, then - # appending the content signature - - attr['signing-key'] = b'\n' + private_signing_key.public_key().public_bytes( - encoding = serialization.Encoding.PEM, - format = serialization.PublicFormat.PKCS1, - ).strip() + signing_key = _signing_key(private_signing_key) + attr['signing-key'] = signing_key.public_digest
content = _descriptor_content(attr, exclude, sign, base_header) + b'\nrouter-signature\n' - signature = base64.b64encode(private_signing_key.sign(content, padding.PKCS1v15(), hashes.SHA1())) - - return content + b'\n'.join([b'-----BEGIN SIGNATURE-----'] + stem.util.str_tools._split_by_length(signature, 64) + [b'-----END SIGNATURE-----\n']) + return _append_router_signature(content, signing_key.private) else: - return _descriptor_content(attr, exclude, sign, base_header, base_footer) + return _descriptor_content(attr, exclude, sign, base_header, ( + ('router-signature', _random_crypto_blob('SIGNATURE')), + ))
@classmethod def create(cls, attr = None, exclude = (), validate = True, sign = False, private_signing_key = None): diff --git a/test/unit/descriptor/extrainfo_descriptor.py b/test/unit/descriptor/extrainfo_descriptor.py index d41f68b..1f91e72 100644 --- a/test/unit/descriptor/extrainfo_descriptor.py +++ b/test/unit/descriptor/extrainfo_descriptor.py @@ -136,7 +136,7 @@ k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
@test.require.cryptography def test_descriptor_signing(self): - self.assertRaisesRegexp(NotImplementedError, 'Signing of RelayExtraInfoDescriptor not implemented', RelayExtraInfoDescriptor.create, sign = True) + RelayExtraInfoDescriptor.create(sign = True) self.assertRaisesRegexp(NotImplementedError, 'Signing of BridgeExtraInfoDescriptor not implemented', BridgeExtraInfoDescriptor.create, sign = True)
def test_multiple_metrics_bridge_descriptors(self):