[tor-commits] [stem/master] Sign created extrainfo descriptors

atagar at torproject.org atagar at torproject.org
Wed Jun 28 16:24:35 UTC 2017


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



More information about the tor-commits mailing list