[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Check consensus signatures
commit 8c84eea733f8be0d5f74265c7387230df3200f7b
Author: Tyler Parks <tparks5@xxxxxxxxxxxxxxxxx>
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):
"""
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits