[tor-commits] [stem/master] Parsing the directory-signature and unrecognized lines

atagar at torproject.org atagar at torproject.org
Sat Oct 13 18:35:45 UTC 2012


commit 1f868090e2d641ddcb49d02bd15b5894f5bf6923
Author: Damian Johnson <atagar at torproject.org>
Date:   Tue Sep 18 09:36:56 2012 -0700

    Parsing the directory-signature and unrecognized lines
    
    Finishing up with the footer. It doesn't make sense for the DirectorySignature
    or DirectoryAuthority to be Descriptor subclasses (cuz... well, they aren't
    descriptors). However, I like having this struct class rather than providing
    our callers with a tuple list. I should probably do this for other descriptor
    documents too...
---
 stem/descriptor/networkstatus.py               |  111 ++++++++++--------------
 test/integ/descriptor/networkstatus.py         |   16 ++--
 test/unit/descriptor/networkstatus/document.py |   59 ++++++++++---
 3 files changed, 98 insertions(+), 88 deletions(-)

diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index e142728..a7bca7a 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -36,7 +36,7 @@ The documents can be obtained from any of the following sources...
     +- MicrodescriptorConsensus - Microdescriptor flavoured consensus documents
   RouterStatusEntry - Router descriptor; contains information about a Tor relay
     +- RouterMicrodescriptor - Router microdescriptor; contains information that doesn't change frequently
-  DirectorySignature - Network status document's directory signature
+  DocumentSignature - Signature of a document by a directory authority
   DirectoryAuthority - Directory authority defined in a v3 network status document
 """
 
@@ -216,7 +216,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
   :var list known_flags: **\*** list of known router flags
   :var list params: **\*** dict of parameter(str) => value(int) mappings
   :var list directory_authorities: **\*** list of DirectoryAuthority objects that have generated this document
-  :var list directory_signatures: **\*** list of signatures this document has
+  :var list signatures: **\*** DocumentSignature of the authorities that have signed the document
   
   **Consensus Attributes:**
   :var int consensus_method: method version used to generate this consensus
@@ -243,7 +243,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
     super(NetworkStatusDocument, self).__init__(raw_content)
     
     self.directory_authorities = []
-    self.directory_signatures = []
+    self.signatures = []
     
     self.version = None
     self.is_consensus = True
@@ -262,6 +262,8 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
     self.params = dict(DEFAULT_PARAMS) if default_params else {}
     self.bandwidth_weights = {}
     
+    self._unrecognized_lines = []
+    
     document_file = StringIO(raw_content)
     header, footer, routers_end = _get_document_content(document_file, validate)
     
@@ -290,13 +292,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
     return bool(self.consensus_method >= method or filter(lambda x: x >= method, self.consensus_methods))
   
   def get_unrecognized_lines(self):
-    """
-    Returns any unrecognized trailing lines.
-    
-    :returns: a list of unrecognized trailing lines
-    """
-    
-    return self.unrecognized_lines
+    return list(self._unrecognized_lines)
   
   def _parse(self, header, footer, validate):
     """
@@ -445,6 +441,15 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
             actual_label = ', '.join(weight_keys)
             
             raise ValueError("A network status document's 'bandwidth-weights' entries should be '%s', got '%s'" % (expected_label, actual_label))
+      elif keyword == "directory-signature":
+        if not " " in value or not block_contents:
+          if not validate: continue
+          raise ValueError("Authority signatures in a network status document are expected to be of the form 'directory-signature FINGERPRINT KEY_DIGEST\\nSIGNATURE', got:\n%s" % line)
+        
+        fingerprint, key_digest = value.split(" ", 1)
+        self.signatures.append(DocumentSignature(fingerprint, key_digest, block_contents, validate))
+      else:
+        self._unrecognized_lines.append(line)
     
     # doing this validation afterward so we know our 'is_consensus' and
     # 'is_vote' attributes
@@ -483,17 +488,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
     
     _read_keyword_line("directory-footer", content, False, True)
     _read_keyword_line("bandwidth-weights", content, False, True)
-    
-    while _peek_keyword(content) == "directory-signature":
-      signature_data = _read_until_keywords("directory-signature", content, False, True)
-      self.directory_signatures.append(DirectorySignature("".join(signature_data)))
-    
-    remainder = content.read()
-    
-    if remainder:
-      self.unrecognized_lines = remainder.split("\n")
-    else:
-      self.unrecognized_lines = []
+    _read_keyword_line("directory-signature", content, False, True)
   
   def _check_for_missing_and_disallowed_fields(self, header_entries, footer_entries):
     """
@@ -706,60 +701,44 @@ class DirectoryAuthority(stem.descriptor.Descriptor):
     
     return self.unrecognized_lines
 
-class DirectorySignature(stem.descriptor.Descriptor):
+# TODO: microdescriptors have a slightly different format (including a
+# 'method') - should probably be a subclass
+class DocumentSignature(object):
   """
-  Contains directory signatures in a v3 network status document.
+  Directory signature of a v3 network status document.
   
-  :var str identity: signature identity
-  :var str key_digest: signature key digest
-  :var str method: method used to generate the signature
-  :var str signature: the signature data
-  """
+  :var str identity: fingerprint of the authority that made the signature
+  :var str key_digest: digest of the signing key
+  :var str signature: document signature
+  :param bool validate: checks validity if True
   
-  def __init__(self, raw_content, validate = True):
-    """
-    Parse a directory signature entry in a v3 network status document and
-    provide a DirectorySignature object.
-    
-    :param str raw_content: raw directory signature entry information
-    :param bool validate: True if the document is to be validated, False otherwise
-    
-    :raises: ValueError if the raw data is invalid
-    """
-    
-    super(DirectorySignature, self).__init__(raw_content)
-    self.identity, self.key_digest, self.method, self.signature = None, None, None, None
-    content = raw_content.splitlines()
-    
-    signature_line = _read_keyword_line_str("directory-signature", content, validate).split(" ")
-    
-    if len(signature_line) == 2:
-      self.identity, self.key_digest = signature_line
-    if len(signature_line) == 3:
-      # for microdescriptor consensuses
-      # This 'method' seems to be undocumented 8-8-12
-      self.method, self.identity, self.key_digest = signature_line
-    
-    self.signature = _get_pseudo_pgp_block(content)
-    self.unrecognized_lines = content
-    if self.unrecognized_lines and validate:
-      raise ValueError("Unrecognized trailing data in directory signature")
+  :raises: ValueError if a validity check fails
+  """
   
-  def get_unrecognized_lines(self):
-    """
-    Returns any unrecognized lines.
+  def __init__(self, identity, key_digest, signature, validate = True):
+    # Checking that these attributes are valid. Technically the key
+    # digest isn't a fingerprint, but it has the same characteristics.
     
-    :returns: a list of unrecognized lines
-    """
+    if validate:
+      if not stem.util.tor_tools.is_valid_fingerprint(identity):
+        raise ValueError("Malformed fingerprint (%s) in the document signature" % (identity))
+      
+      if not stem.util.tor_tools.is_valid_fingerprint(key_digest):
+        raise ValueError("Malformed key digest (%s) in the document signature" % (key_digest))
     
-    return self.unrecognized_lines
+    self.identity = identity
+    self.key_digest = key_digest
+    self.signature = signature
   
   def __cmp__(self, other):
-    if not isinstance(other, DirectorySignature):
+    if not isinstance(other, DocumentSignature):
       return 1
     
-    # attributes are all derived from content, so we can simply use that to check
-    return str(self) > str(other)
+    for attr in ("identity", "key_digest", "signature"):
+      if getattr(self, attr) > getattr(other, attr): return 1
+      elif getattr(self, attr) < getattr(other, attr): return -1
+    
+    return 0
 
 class RouterStatusEntry(stem.descriptor.Descriptor):
   """
@@ -1033,7 +1012,7 @@ class MicrodescriptorConsensus(NetworkStatusDocument):
   :var list params: dict of parameter(str) => value(int) mappings
   :var list directory_authorities: **\*** list of DirectoryAuthority objects that have generated this document
   :var dict bandwidth_weights: **~** dict of weight(str) => value(int) mappings
-  :var list directory_signatures: **\*** list of signatures this document has
+  :var list signatures: **\*** list of signatures this document has
   
   | **\*** attribute is either required when we're parsed with validation or has a default value, others are left as None if undefined
   | **~** attribute appears only in consensuses
diff --git a/test/integ/descriptor/networkstatus.py b/test/integ/descriptor/networkstatus.py
index d77a36f..aa91ab3 100644
--- a/test/integ/descriptor/networkstatus.py
+++ b/test/integ/descriptor/networkstatus.py
@@ -143,10 +143,10 @@ HFXB4497LzESysYJ/4jJY83E5vLjhv+igIxD9LU6lf6ftkGeF+lNmIAIEKaMts8H
 mfWcW0b+jsrXcJoCxV5IrwCDF3u1aC3diwZY6yiG186pwWbOwE41188XI2DeYPwE
 I/TJmV928na7RLZe2mGHCAW3VQOvV+QkCfj05VZ8CsY=
 -----END SIGNATURE-----"""
-    self.assertEquals(8, len(desc.directory_signatures))
-    self.assertEquals("14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4", desc.directory_signatures[0].identity)
-    self.assertEquals("BF112F1C6D5543CFD0A32215ACABD4197B5279AD", desc.directory_signatures[0].key_digest)
-    self.assertEquals(expected_signature, desc.directory_signatures[0].signature)
+    self.assertEquals(8, len(desc.signatures))
+    self.assertEquals("14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4", desc.signatures[0].identity)
+    self.assertEquals("BF112F1C6D5543CFD0A32215ACABD4197B5279AD", desc.signatures[0].key_digest)
+    self.assertEquals(expected_signature, desc.signatures[0].signature)
   
   def test_metrics_vote(self):
     """
@@ -261,10 +261,10 @@ fskXN84wB3mXfo+yKGSt0AcDaaPuU3NwMR3ROxWgLN0KjAaVi2eV9PkPCsQkcgw3
 JZ/1HL9sHyZfo6bwaC6YSM9PNiiY6L7rnGpS7UkHiFI+M96VCMorvjm5YPs3FioJ
 DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w=
 -----END SIGNATURE-----"""
-    self.assertEquals(1, len(desc.directory_signatures))
-    self.assertEquals("27B6B5996C426270A5C95488AA5BCEB6BCC86956", desc.directory_signatures[0].identity)
-    self.assertEquals("D5C30C15BB3F1DA27669C2D88439939E8F418FCF", desc.directory_signatures[0].key_digest)
-    self.assertEquals(expected_signature, desc.directory_signatures[0].signature)
+    self.assertEquals(1, len(desc.signatures))
+    self.assertEquals("27B6B5996C426270A5C95488AA5BCEB6BCC86956", desc.signatures[0].identity)
+    self.assertEquals("D5C30C15BB3F1DA27669C2D88439939E8F418FCF", desc.signatures[0].key_digest)
+    self.assertEquals(expected_signature, desc.signatures[0].signature)
   
   def test_cached_microdesc_consensus(self):
     """
diff --git a/test/unit/descriptor/networkstatus/document.py b/test/unit/descriptor/networkstatus/document.py
index 8ed1f0e..a0fe3a8 100644
--- a/test/unit/descriptor/networkstatus/document.py
+++ b/test/unit/descriptor/networkstatus/document.py
@@ -7,7 +7,16 @@ import unittest
 
 import stem.version
 from stem.descriptor import Flag
-from stem.descriptor.networkstatus import HEADER_STATUS_DOCUMENT_FIELDS, FOOTER_STATUS_DOCUMENT_FIELDS, DEFAULT_PARAMS, BANDWIDTH_WEIGHT_ENTRIES, NetworkStatusDocument, DirectorySignature
+from stem.descriptor.networkstatus import HEADER_STATUS_DOCUMENT_FIELDS, FOOTER_STATUS_DOCUMENT_FIELDS, DEFAULT_PARAMS, BANDWIDTH_WEIGHT_ENTRIES, NetworkStatusDocument, DocumentSignature
+
+sig_block = """\
+-----BEGIN SIGNATURE-----
+e1XH33ITaUYzXu+dK04F2dZwR4PhcOQgIuK859KGpU77/6lRuggiX/INk/4FJanJ
+ysCTE1K4xk4fH3N1Tzcv/x/gS4LUlIZz3yKfBnj+Xh3w12Enn9V1Gm1Vrhl+/YWH
+eweONYRZTTvgsB+aYsCoBuoBBpbr4Swlu64+85F44o4=
+-----END SIGNATURE-----"""
+
+SIG = DocumentSignature("14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4", "BF112F1C6D5543CFD0A32215ACABD4197B5279AD", sig_block)
 
 NETWORK_STATUS_DOCUMENT_ATTR = {
   "network-status-version": "3",
@@ -21,16 +30,9 @@ NETWORK_STATUS_DOCUMENT_ATTR = {
   "voting-delay": "300 300",
   "known-flags": "Authority BadExit Exit Fast Guard HSDir Named Running Stable Unnamed V2Dir Valid",
   "directory-footer": "",
-  "directory-signature": "\n".join((
-    "14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4 BF112F1C6D5543CFD0A32215ACABD4197B5279AD",
-    "-----BEGIN SIGNATURE-----",
-    "e1XH33ITaUYzXu+dK04F2dZwR4PhcOQgIuK859KGpU77/6lRuggiX/INk/4FJanJ",
-    "ysCTE1K4xk4fH3N1Tzcv/x/gS4LUlIZz3yKfBnj+Xh3w12Enn9V1Gm1Vrhl+/YWH",
-    "eweONYRZTTvgsB+aYsCoBuoBBpbr4Swlu64+85F44o4=",
-    "-----END SIGNATURE-----")),
+  "directory-signature": "%s %s\n%s" % (SIG.identity, SIG.key_digest, SIG.signature),
 }
 
-SIG = DirectorySignature("directory-signature " + NETWORK_STATUS_DOCUMENT_ATTR["directory-signature"])
 
 def get_network_status_document(attr = None, exclude = None, routers = None):
   """
@@ -119,7 +121,7 @@ class TestNetworkStatusDocument(unittest.TestCase):
     self.assertEqual(DEFAULT_PARAMS, document.params)
     self.assertEqual([], document.directory_authorities)
     self.assertEqual({}, document.bandwidth_weights)
-    self.assertEqual([SIG], document.directory_signatures)
+    self.assertEqual([SIG], document.signatures)
     self.assertEqual([], document.get_unrecognized_lines())
   
   def test_minimal_vote(self):
@@ -151,7 +153,7 @@ class TestNetworkStatusDocument(unittest.TestCase):
     self.assertEqual(DEFAULT_PARAMS, document.params)
     self.assertEqual([], document.directory_authorities)
     self.assertEqual({}, document.bandwidth_weights)
-    self.assertEqual([SIG], document.directory_signatures)
+    self.assertEqual([SIG], document.signatures)
     self.assertEqual([], document.get_unrecognized_lines())
   
   def test_missing_fields(self):
@@ -170,6 +172,15 @@ class TestNetworkStatusDocument(unittest.TestCase):
             self.assertRaises(ValueError, NetworkStatusDocument, content)
             NetworkStatusDocument(content, False) # constructs without validation
   
+  def test_unrecognized_line(self):
+    """
+    Includes unrecognized content in the document.
+    """
+    
+    content = get_network_status_document({"pepperjack": "is oh so tasty!"})
+    document = NetworkStatusDocument(content)
+    self.assertEquals(["pepperjack is oh so tasty!"], document.get_unrecognized_lines())
+  
   def test_misordered_fields(self):
     """
     Rearranges our descriptor fields.
@@ -533,14 +544,14 @@ class TestNetworkStatusDocument(unittest.TestCase):
     self.assertRaises(ValueError, NetworkStatusDocument, content)
     
     document = NetworkStatusDocument(content, False)
-    self.assertEqual([SIG], document.directory_signatures)
+    self.assertEqual([SIG], document.signatures)
     self.assertEqual([], document.get_unrecognized_lines())
     
     # excludes a footer from a version that shouldn't have it
     
     content = get_network_status_document({"consensus-method": "8"}, ("directory-footer", "directory-signature"))
     document = NetworkStatusDocument(content)
-    self.assertEqual([], document.directory_signatures)
+    self.assertEqual([], document.signatures)
     self.assertEqual([], document.get_unrecognized_lines())
   
   def test_footer_with_value(self):
@@ -552,7 +563,7 @@ class TestNetworkStatusDocument(unittest.TestCase):
     self.assertRaises(ValueError, NetworkStatusDocument, content)
     
     document = NetworkStatusDocument(content, False)
-    self.assertEqual([SIG], document.directory_signatures)
+    self.assertEqual([SIG], document.signatures)
     self.assertEqual([], document.get_unrecognized_lines())
   
   def test_bandwidth_wights_ok(self):
@@ -649,4 +660,24 @@ class TestNetworkStatusDocument(unittest.TestCase):
       
       document = NetworkStatusDocument(content, False)
       self.assertEquals(expected, document.bandwidth_weights)
+  
+  def test_malformed_signature(self):
+    """
+    Provides malformed or missing content in the 'directory-signature' line.
+    """
+    
+    test_values = (
+      "",
+      "\n",
+      "blarg",
+    )
+    
+    for test_value in test_values:
+      for test_attr in xrange(3):
+        attrs = [SIG.identity, SIG.key_digest, SIG.signature]
+        attrs[test_attr] = test_value
+        
+        content = get_network_status_document({"directory-signature": "%s %s\n%s" % tuple(attrs)})
+        self.assertRaises(ValueError, NetworkStatusDocument, content)
+        NetworkStatusDocument(content, False) # checks that it's still parseable without validation
 





More information about the tor-commits mailing list