[tor-commits] [stem/master] Check consensus signatures

atagar at torproject.org atagar at torproject.org
Mon May 15 00:22:35 UTC 2017


commit 8c84eea733f8be0d5f74265c7387230df3200f7b
Author: Tyler Parks <tparks5 at mail.csuchico.edu>
Date:   Sun May 14 17:18:09 2017 -0700

    Check consensus signatures
    
    We already validate crypto of most descriptor types but lacked this check for
    arguably the most important thing: the consensus.
    
    This requires key certificates so unlike other descriptors this isn't validated
    by default. Rather, callers need to call validate_signatures() with the
    authority certificates.
    
    This branch has been a collaboration between Tyler and Damian over a couple
    weeks of bouncing remotes back and forth. :P
    
      https://trac.torproject.org/projects/tor/ticket/11045
---
 docs/change_log.rst                    |  1 +
 stem/descriptor/__init__.py            |  6 +-----
 stem/descriptor/networkstatus.py       | 33 +++++++++++++++++++++++++++++++++
 stem/descriptor/remote.py              | 13 ++++++++++++-
 test/integ/descriptor/__init__.py      |  4 ++--
 test/integ/descriptor/networkstatus.py | 15 ++++++++++++++-
 6 files changed, 63 insertions(+), 9 deletions(-)

diff --git a/docs/change_log.rst b/docs/change_log.rst
index b99d0b4..cd5dcd5 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -55,6 +55,7 @@ The following are only available within Stem's `git repository
 
   * Supporting `descriptor creation <tutorials/mirror_mirror_on_the_wall.html#can-i-create-descriptors>`_ (:trac:`10227`)
   * Support and validation for `ed25519 certificates <api/descriptor/certificate.html>`_ (`spec <https://gitweb.torproject.org/torspec.git/tree/cert-spec.txt>`_, :trac:`21558`)
+  * Added :func:`~stem.descriptor.networkstatus.NetworkStatusDocumentV3.validate_signatures` to check our key certificate signatures (:trac:`11045`)
   * Moved from the deprecated `pycrypto <https://www.dlitz.net/software/pycrypto/>`_ module to `cryptography <https://pypi.python.org/pypi/cryptography>`_ for validating signatures (:trac:`21086`)
   * Sped descriptor reading by ~25% by deferring defaulting when validating
   * Added server descriptor's new extra_info_sha256_digest attribute (:spec:`0f03581`)
diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
index 1826b36..b9c31af 100644
--- a/stem/descriptor/__init__.py
+++ b/stem/descriptor/__init__.py
@@ -681,17 +681,14 @@ class Descriptor(object):
     from cryptography.hazmat.backends import default_backend
     from cryptography.hazmat.primitives.serialization import load_der_public_key
     from cryptography.utils import int_to_bytes, int_from_bytes
-
     key = load_der_public_key(_bytes_for_block(signing_key), default_backend())
     modulus = key.public_numbers().n
     public_exponent = key.public_numbers().e
-
     sig_as_bytes = _bytes_for_block(signature)
     sig_as_long = int_from_bytes(sig_as_bytes, byteorder='big')  # convert signature to an int
-    blocksize = 128  # block size will always be 128 for a 1024 bit key
+    blocksize = len(sig_as_bytes)  # 256B for NetworkStatusDocuments, 128B for others
 
     # use the public exponent[e] & the modulus[n] to decrypt the int
-
     decrypted_int = pow(sig_as_long, public_exponent, modulus)
 
     # convert the int to a byte array
@@ -708,7 +705,6 @@ class Descriptor(object):
     # More info here http://www.ietf.org/rfc/rfc2313.txt
     #                esp the Notes in section 8.1
     ############################################################################
-
     try:
       if decrypted_bytes.index(b'\x00\x01') != 0:
         raise ValueError('Verification failed, identifier missing')
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index ff9f105..1a155cd 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -1055,6 +1055,39 @@ class NetworkStatusDocumentV3(NetworkStatusDocument):
     self.routers = dict((desc.fingerprint, desc) for desc in router_iter)
     self._footer(document_file, validate)
 
+  def validate_signatures(self, key_certs):
+    """
+    Validates we're properly signed by the signing certificates.
+
+    .. versionadded:: 1.6.0
+
+    :param list key_certs: :class:`~stem.descriptor.networkstatus.KeyCertificates`
+      to validate the consensus against
+
+    :raises: **ValueError** if an insufficient number of valid signatures are present.
+    """
+
+    # sha1 hash of the body and header
+
+    local_digest = self._digest_for_content(b'network-status-version', b'directory-signature ')
+
+    valid_digests, total_digests = 0, 0
+    required_digests = len(self.signatures) / 2.0
+    signing_keys = dict([(cert.fingerprint, cert.signing_key) for cert in key_certs])
+
+    for sig in self.signatures:
+      if sig.identity not in signing_keys:
+        continue
+
+      signed_digest = self._digest_for_signature(signing_keys[sig.identity], sig.signature)
+      total_digests += 1
+
+      if signed_digest == local_digest:
+        valid_digests += 1
+
+    if valid_digests < required_digests:
+      raise ValueError('Network Status Document has %i valid signatures out of %i total, needed %i' % (valid_digests, total_digests, required_digests))
+
   def get_unrecognized_lines(self):
     if self._lazy_loading:
       self._parse(self._header_entries, False, parser_for_line = self.HEADER_PARSER_FOR_LINE)
diff --git a/stem/descriptor/remote.py b/stem/descriptor/remote.py
index 79f3889..2f44504 100644
--- a/stem/descriptor/remote.py
+++ b/stem/descriptor/remote.py
@@ -98,6 +98,7 @@ except ImportError:
   import urllib2 as urllib
 
 import stem.descriptor
+import stem.prereq
 
 from stem import Flag
 from stem.util import _hash_attr, connection, log, str_tools, tor_tools
@@ -628,7 +629,17 @@ class DescriptorDownloader(object):
     if authority_v3ident:
       resource += '/%s' % authority_v3ident
 
-    return self.query(resource + '.z', **query_args)
+    consensus_query = self.query(resource + '.z', **query_args)
+
+    # if we're performing validation then check that it's signed by the
+    # authority key certificates
+
+    if consensus_query.validate and consensus_query.document_handler == stem.descriptor.DocumentHandler.DOCUMENT and stem.prereq.is_crypto_available():
+      consensus = list(consensus_query.run())[0]
+      key_certs = self.get_key_certificates(**query_args).run()
+      consensus.validate_signatures(key_certs)
+
+    return consensus_query
 
   def get_vote(self, authority, **query_args):
     """
diff --git a/test/integ/descriptor/__init__.py b/test/integ/descriptor/__init__.py
index b2f7121..331316a 100644
--- a/test/integ/descriptor/__init__.py
+++ b/test/integ/descriptor/__init__.py
@@ -5,7 +5,7 @@ Integration tests for stem.descriptor.* contents.
 __all__ = [
   'extrainfo_descriptor',
   'microdescriptor',
+  'networkstatus',
+  'remote'
   'server_descriptor',
-  'get_resource',
-  'open_desc',
 ]
diff --git a/test/integ/descriptor/networkstatus.py b/test/integ/descriptor/networkstatus.py
index 72d503a..59dca00 100644
--- a/test/integ/descriptor/networkstatus.py
+++ b/test/integ/descriptor/networkstatus.py
@@ -7,17 +7,30 @@ import unittest
 
 import stem
 import stem.descriptor
-import stem.descriptor.networkstatus
+import stem.descriptor.remote
 import stem.version
 import test.runner
 
 from test.util import (
   register_new_capability,
   only_run_once,
+  require_cryptography,
+  require_online,
 )
 
 
 class TestNetworkStatus(unittest.TestCase):
+  @require_online
+  @require_cryptography
+  @only_run_once
+  def test_signature_validation(self):
+    """
+    The full consensus is pretty sizable so rather than storing a copy of it
+    using the remote module. Chekcing the signature on the current consensus.
+    """
+
+    stem.descriptor.remote.get_consensus(document_handler = stem.descriptor.DocumentHandler.DOCUMENT, validate = True).run()
+
   @only_run_once
   def test_cached_consensus(self):
     """



More information about the tor-commits mailing list