[tor-commits] [stem/master] Implementing Relay Descriptor verification

atagar at torproject.org atagar at torproject.org
Thu Nov 29 05:22:57 UTC 2012


commit e0095fbe54759c45cbf6d1b120d2b17b47a0ec21
Author: Eoin o Fearghail <eoin.o.fearghail at gmail.com>
Date:   Fri Nov 23 22:16:22 2012 +0000

    Implementing Relay Descriptor verification
    
    cf https://trac.torproject.org/projects/tor/ticket/5810
    
    1) Implemented relay descriptor verification using the python-crypto lib.
       Code is only run if python-crypto can be imported. [cf stem.prereq.is_crypto_available()]
       NOTE: constructing a RelayDescriptor will now raise an exception if invalid descriptor content is used.
    2) Refactored the digest() function in server_descriptor.py.
    3) Added a function to the mocking lib to sign a descriptor with an auto-generated key
    4) Add usage of new sign_descriptor_content() in unit tests where necessary.
    5) Updated the non-ascii-descriptor file to be correctly signed.
    6) Updated extra info descriptor test to use new fingerprint in non-ascii-descriptor file
    7) Removed server descriptor tests that do not make sense if data is being generated dynamically.
       e.g. Removed test fingerprint valid test, since data now dynamically generated.
---
 stem/descriptor/server_descriptor.py            |  144 +++++++++++++++++------
 stem/prereq.py                                  |   23 ++--
 test/integ/descriptor/data/non-ascii_descriptor |   14 +-
 test/integ/descriptor/server_descriptor.py      |    4 +-
 test/mocking.py                                 |   85 +++++++++++++
 test/unit/descriptor/server_descriptor.py       |   23 +---
 test/unit/tutorial.py                           |    8 +-
 7 files changed, 223 insertions(+), 78 deletions(-)

diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
index 046dfe1..9f3dbb3 100644
--- a/stem/descriptor/server_descriptor.py
+++ b/stem/descriptor/server_descriptor.py
@@ -39,6 +39,7 @@ import stem.exit_policy
 import stem.version
 import stem.util.connection
 import stem.util.tor_tools
+import stem.util.log as log
 
 # relay descriptors must have exactly one of the following
 REQUIRED_FIELDS = (
@@ -593,52 +594,123 @@ class RelayDescriptor(ServerDescriptor):
     
     super(RelayDescriptor, self).__init__(raw_contents, validate, annotations)
     
-    # if we have a fingerprint then checks that our fingerprint is a hash of
-    # our signing key
+    # validate the descriptor if required
+    if validate:
+      # ensure the digest of the descriptor has been calculated
+      self.digest()
+      self._validate_content()
+  
+  def digest(self):
+    # Digest is calculated from everything in the
+    # descriptor except the router-signature.
+    raw_descriptor = str(self)
+    start_token = "router "
+    sig_token = "\nrouter-signature\n"
+    start = raw_descriptor.find(start_token)
+    sig_start = raw_descriptor.find(sig_token)
+    end = sig_start + len(sig_token)
+    if start >= 0 and sig_start > 0 and end > start:
+      for_digest = raw_descriptor[start:end]
+      digest_hash = hashlib.sha1(for_digest)
+      self._digest = digest_hash.hexdigest()
+    else:
+      log.warn("unable to calculate digest for descriptor")
+      raise ValueError("unable to calculate digest for descriptor")
     
-    if validate and self.fingerprint and stem.prereq.is_rsa_available():
-      import rsa
-      pubkey = rsa.PublicKey.load_pkcs1(self.signing_key)
-      der_encoded = pubkey.save_pkcs1(format = "DER")
-      key_hash = hashlib.sha1(der_encoded).hexdigest()
-      
-      if key_hash != self.fingerprint.lower():
-        raise ValueError("Hash of our signing key doesn't match our fingerprint. Signing key hash: %s, fingerprint: %s" % (key_hash, self.fingerprint.lower()))
+    return self._digest
   
-  def is_valid(self):
+  def _validate_content(self):
     """
     Validates that our content matches our signature.
     
-    **Method implementation is incomplete, and will raise a NotImplementedError**
-    
-    :returns: **True** if our signature matches our content, **False** otherwise
+    :raises a ValueError if signature does not match content,
     """
     
-    raise NotImplementedError # TODO: finish implementing
-    
-    # without validation we may be missing our signature
-    if not self.signature: return False
-    
-    # gets base64 encoded bytes of our signature without newlines nor the
-    # "-----[BEGIN|END] SIGNATURE-----" header/footer
+    if not self.signature:
+      log.warn("Signature missing")
+      raise ValueError("Signature missing")
     
-    sig_content = self.signature.replace("\n", "")[25:-23]
-    sig_bytes = base64.b64decode(sig_content)
+    # strips off the '-----BEGIN RSA PUBLIC KEY-----' header and corresponding footer
+    key_as_string = ''.join(self.signing_key.split('\n')[1:4])
     
-    # TODO: Decrypt the signature bytes with the signing key and remove
-    # the PKCS1 padding to get the original message, and encode the message
-    # in hex and compare it to the digest of the descriptor.
+    # calculate the signing key hash
+    key_as_der = base64.b64decode(key_as_string)
+    key_der_as_hash = hashlib.sha1(key_as_der).hexdigest()
     
-    return True
-  
-  def digest(self):
-    if self._digest is None:
-      # our digest is calculated from everything except our signature
-      raw_content, ending = str(self), "\nrouter-signature\n"
-      raw_content = raw_content[:raw_content.find(ending) + len(ending)]
-      self._digest = hashlib.sha1(raw_content).hexdigest().upper()
-    
-    return self._digest
+    # if we have a fingerprint then check that our fingerprint is a hash of
+    # our signing key
+    if self.fingerprint:
+      if key_der_as_hash != self.fingerprint.lower():
+        log.warn("Hash of our signing key doesn't match our fingerprint. Signing key hash: %s, fingerprint: %s" % (key_der_as_hash, self.fingerprint.lower()))
+        raise ValueError("Fingerprint does not match hash")
+    else:
+      log.notice("No fingerprint for this descriptor")
+    
+    try:
+      self._verify_descriptor(key_as_der)
+      log.info("Descriptor verified.")
+    except ValueError, e:
+      log.warn("Failed to verify descriptor: %s" % e)
+      raise e
+  
+  def _verify_descriptor(self, key_as_der):
+    if not stem.prereq.is_crypto_available():
+      return
+    else:
+      from Crypto.Util import asn1
+      from Crypto.Util.number import bytes_to_long, long_to_bytes
+      
+      # get the ASN.1 sequence
+      seq = asn1.DerSequence()
+      seq.decode(key_as_der)
+      modulus = seq[0]
+      public_exponent = seq[1] #should always be 65537
+      
+      # convert the descriptor signature to an int before decrypting it
+      sig_as_string = ''.join(self.signature.split('\n')[1:4])
+      sig_as_bytes = base64.b64decode(sig_as_string)
+      sig_as_long = bytes_to_long(sig_as_bytes)
+      
+      # use the public exponent[e] & the modulus[n] to decrypt the int
+      decrypted_int = pow(sig_as_long, public_exponent ,modulus)
+      # block size will always be 128 for a 1024 bit key
+      blocksize = 128
+      # convert the int to a byte array.
+      decrypted_bytes = long_to_bytes(decrypted_int, blocksize)
+      
+      ############################################################################
+      ## The decrypted bytes should have a structure exactly along these lines.
+      ## 1 byte  - [null '\x00']
+      ## 1 byte  - [block type identifier '\x01'] - Should always be 1
+      ## N bytes - [padding '\xFF' ]
+      ## 1 byte  - [separator '\x00' ]
+      ## M bytes - [message]
+      ## Total   - 128 bytes
+      ## More info here http://www.ietf.org/rfc/rfc2313.txt
+      ##                esp the Notes in section 8.1
+      ############################################################################
+      try:
+        if decrypted_bytes.index('\x00\x01') != 0:
+          log.warn("Verification failed, identifier missing")
+          raise ValueError("Verification failed, identifier missing")
+      except ValueError:
+        log.warn("Verification failed, Malformed data")
+        raise ValueError("Verification failed, Malformed data")
+      
+      try:
+        identifier_offset = 2
+        # Find the separator
+        seperator_index = decrypted_bytes.index('\x00', identifier_offset)
+      except ValueError:
+        log.warn("Verification failed, seperator not found")
+        raise ValueError("Verification failed, seperator not found")
+      
+      digest = decrypted_bytes[seperator_index+1:]
+      # The local digest is stored in hex so need to encode the decrypted digest
+      digest_hex = digest.encode('hex')
+      if digest_hex != self._digest:
+        log.warn("Decrypted digest does not match local digest")
+        raise ValueError("Decrypted digest does not match local digest")
   
   def _parse(self, entries, validate):
     entries = dict(entries) # shallow copy since we're destructive
diff --git a/stem/prereq.py b/stem/prereq.py
index 67611fd..d205954 100644
--- a/stem/prereq.py
+++ b/stem/prereq.py
@@ -24,7 +24,7 @@ import sys
 
 import stem.util.log as log
 
-IS_RSA_AVAILABLE = None
+IS_CRYPTO_AVAILABLE = None
 
 def check_requirements():
   """
@@ -59,20 +59,23 @@ def is_python_27():
   
   return _check_version(7)
 
-def is_rsa_available():
-  global IS_RSA_AVAILABLE
+def is_crypto_available():
+  global IS_CRYPTO_AVAILABLE
   
-  if IS_RSA_AVAILABLE == None:
+  if IS_CRYPTO_AVAILABLE == None:
     try:
-      import rsa
-      IS_RSA_AVAILABLE = True
+      from Crypto.PublicKey import RSA
+      from Crypto.Util import asn1
+      from Crypto.Util.number import long_to_bytes
+      IS_CRYPTO_AVAILABLE = True
     except ImportError:
-      IS_RSA_AVAILABLE = False
+      IS_CRYPTO_AVAILABLE = False
       
-      msg = "Unable to import the rsa module. Because of this we'll be unable to verify descriptor signature integrity."
-      log.log_once("stem.prereq.is_rsa_available", log.INFO, msg)
+      # the code that verifies relay descriptor signatures uses the python-crypto library
+      msg = "Unable to import the crypto module. Because of this we'll be unable to verify descriptor signature integrity."
+      log.log_once("stem.prereq.is_crypto_available", log.INFO, msg)
   
-  return IS_RSA_AVAILABLE
+  return IS_CRYPTO_AVAILABLE
 
 def _check_version(minor_req):
   major_version, minor_version = sys.version_info[0:2]
diff --git a/test/integ/descriptor/data/non-ascii_descriptor b/test/integ/descriptor/data/non-ascii_descriptor
index 2cd2a6b..eb3e31a 100644
--- a/test/integ/descriptor/data/non-ascii_descriptor
+++ b/test/integ/descriptor/data/non-ascii_descriptor
@@ -3,7 +3,7 @@ router torrelay389752132 130.243.230.116 9001 0 0
 platform Tor 0.2.2.35 (git-4f42b0a93422f70e) on Linux x86_64
 opt protocols Link 1 2 Circuit 1
 published 2012-03-21 16:28:14
-opt fingerprint FEBC 7F99 2AC4 18BB E42B C13F E94E FCFE 6549 197E
+opt fingerprint 5D47 E91A 1F74 21A4 E325 5F4D 04E5 34E9 A214 07BB
 uptime 3103848
 bandwidth 81920 102400 84275
 opt extra-info-digest 51E9FD0DA7C235D8C0250BAFB6E1ABB5F1EF9F04
@@ -15,16 +15,16 @@ k3Rx75up+wsuBzhfwSYr7W+T+WkDQvz49RFPpns6Ef0qFpQ1TlHxAgMBAAE=
 -----END RSA PUBLIC KEY-----
 signing-key
 -----BEGIN RSA PUBLIC KEY-----
-MIGJAoGBAMSmtutGlXVdvh/IC4TyhQpgSajxrZItC2lS5/70Vr4uLevryPlBgVrW
-35CHxKYaj0MAOfkJQ0/OvTaXe7hlaCLrDDXScaH/XEDurcWrynsdzomsCvn/6VJ+
-xZFszt2Dn5myXKMvYy3j1oevC4iDaZXwxgpwx/UMJsFn7GOUPFYbAgMBAAE=
+MIGJAoGBALRHQWXGjGLNROY8At3dMnrcSxw4PF/9oLYuqCsXNAq0Gju+EBA5qfM4
+AMpeOk+7ZsZ6AsjdBPAPaOf7hm+z6Kr3Am/gC43dci+iuNHf2wYLR8TnW/C5Q6ZQ
+iXpSAGrOHnIptyPHa0j9ayM4WmHWrPBKnC0QA91CGrxnnNc6DHehAgMBAAE=
 -----END RSA PUBLIC KEY-----
 opt hidden-service-dir
 contact 2048R/F171EC1F Johan Blåbäck こんにちは
 reject *:*
 router-signature
 -----BEGIN SIGNATURE-----
-q3Tw41+miuycnXowX/k6CCOcHMjw0BCDjW56Wh/eHoICmVb/hBJdtuzTaorWHLWp
-OoTa4Sy4OrGFL+ldzajGC8+oqMvrYudiIxbJWmH3NXFyd7ZeEdnHzHxNOU8p1+X+
-hFwdOCEvzvvTbOuS2DwDt+TU8rljZunZfcMWgXktAD0=
+WqBgiomhJ+XewpbOGg1r+6KXlAkdxHRhgCB/D980yJVzXWbOCrRhwyyAH9Lx+yrK
+1EFXAtfQBBx2hmsw8CSYuUT6ckjXyUBAKEdABC25yRdi+fN3NfSQd56U9MvArjo9
+Y8oz244gH4BSVp4CScL8dK0EUsUrAxjs+OU7bnV5saA=
 -----END SIGNATURE-----
diff --git a/test/integ/descriptor/server_descriptor.py b/test/integ/descriptor/server_descriptor.py
index 55e6545..bfe13a7 100644
--- a/test/integ/descriptor/server_descriptor.py
+++ b/test/integ/descriptor/server_descriptor.py
@@ -86,7 +86,7 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
     self.assertEquals(expected_signing_key, desc.signing_key)
     self.assertEquals(expected_signature, desc.signature)
     self.assertEquals([], desc.get_unrecognized_lines())
-    self.assertEquals("2C7B27BEAB04B4E2459D89CA6D5CD1CC5F95A689", desc.digest())
+    self.assertEquals("2C7B27BEAB04B4E2459D89CA6D5CD1CC5F95A689", desc.digest().upper())
   
   def test_old_descriptor(self):
     """
@@ -190,7 +190,7 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
     
     desc = stem.descriptor.server_descriptor.RelayDescriptor(descriptor_contents)
     self.assertEquals("torrelay389752132", desc.nickname)
-    self.assertEquals("FEBC7F992AC418BBE42BC13FE94EFCFE6549197E", desc.fingerprint)
+    self.assertEquals("5D47E91A1F7421A4E3255F4D04E534E9A21407BB", desc.fingerprint)
     self.assertEquals("130.243.230.116", desc.address)
     self.assertEquals(9001, desc.or_port)
     self.assertEquals(None, desc.socks_port)
diff --git a/test/mocking.py b/test/mocking.py
index a3d72b1..4c4ea25 100644
--- a/test/mocking.py
+++ b/test/mocking.py
@@ -46,6 +46,9 @@ calling :func:`test.mocking.revert_mocking`.
       get_router_status_entry_micro_v3 - RouterStatusEntryMicroV3
 """
 
+
+import base64
+import hashlib
 import inspect
 import itertools
 import StringIO
@@ -541,6 +544,7 @@ def get_relay_server_descriptor(attr = None, exclude = (), content = False):
   if content:
     return desc_content
   else:
+    desc_content = sign_descriptor_content(desc_content)
     return stem.descriptor.server_descriptor.RelayDescriptor(desc_content, validate = True)
 
 def get_bridge_server_descriptor(attr = None, exclude = (), content = False):
@@ -783,3 +787,84 @@ def get_network_status_document_v3(attr = None, exclude = (), authorities = None
   else:
     return stem.descriptor.networkstatus.NetworkStatusDocumentV3(desc_content, validate = True)
 
+def sign_descriptor_content(desc_content):
+  
+  if not stem.prereq.is_crypto_available():
+    return desc_content
+  else:
+    from Crypto.PublicKey import RSA
+    from Crypto.Util import asn1
+    from Crypto.Util.number import long_to_bytes
+    
+    # generate a key
+    private_key = RSA.generate(1024)
+    
+    # get a string representation of the public key
+    seq = asn1.DerSequence()
+    seq.append(private_key.n)
+    seq.append(private_key.e)
+    seq_as_string = seq.encode()
+    public_key_string = base64.b64encode(seq_as_string)
+    
+    # split public key into lines 64 characters long
+    public_key_string =  public_key_string [:64] + "\n" +public_key_string[64:128] +"\n" +public_key_string[128:]
+    
+    # generate the new signing key string
+    signing_key_token = "\nsigning-key\n" #note the trailing '\n' is important here so as not to match the string elsewhere
+    signing_key_token_start = "-----BEGIN RSA PUBLIC KEY-----\n"
+    signing_key_token_end = "\n-----END RSA PUBLIC KEY-----\n"
+    new_sk = signing_key_token+ signing_key_token_start+public_key_string+signing_key_token_end
+    
+    # update the descriptor string with the new signing key
+    skt_start = desc_content.find(signing_key_token)
+    skt_end = desc_content.find(signing_key_token_end, skt_start)
+    desc_content = desc_content[:skt_start]+new_sk+ desc_content[skt_end+len(signing_key_token_end):]
+    
+    # generate the new fingerprint string
+    key_hash = hashlib.sha1(seq_as_string).hexdigest().upper()
+    grouped_fingerprint = ""
+    for x in range(0, len(key_hash), 4):
+      grouped_fingerprint += " " + key_hash[x:x+4]
+      fingerprint_token = "\nfingerprint"
+      new_fp = fingerprint_token + grouped_fingerprint
+      
+    # update the descriptor string with the new fingerprint
+    ft_start = desc_content.find(fingerprint_token)
+    if ft_start < 0:
+      fingerprint_token = "\nopt fingerprint"
+      ft_start = desc_content.find(fingerprint_token)
+    
+    # if the descriptor does not already contain a fingerprint do not add one
+    if ft_start >= 0:
+      ft_end = desc_content.find("\n", ft_start+1)
+      desc_content = desc_content[:ft_start]+new_fp+desc_content[ft_end:]
+    
+    # calculate the new digest for the descriptor
+    tempDesc = stem.descriptor.server_descriptor.RelayDescriptor(desc_content, validate=False)
+    new_digest_hex = tempDesc.digest()
+    # remove the hex encoding
+    new_digest = new_digest_hex.decode('hex')
+    
+    # Generate the digest buffer.
+    #  block is 128 bytes in size
+    #  2 bytes for the type info
+    #  1 byte for the separator
+    padding = ""
+    for x in range(125 - len(new_digest)):
+      padding += '\xFF'
+      digestBuffer = '\x00\x01' + padding + '\x00' + new_digest
+    
+    # generate a new signature by signing the digest buffer with the private key
+    (signature, ) = private_key.sign(digestBuffer, None)
+    signature_as_bytes = long_to_bytes(signature, 128)
+    signature_base64 = base64.b64encode(signature_as_bytes)
+    signature_base64 =  signature_base64 [:64] + "\n" +signature_base64[64:128] +"\n" +signature_base64[128:]
+    
+    # update the descriptor string with the new signature
+    router_signature_token = "\nrouter-signature\n"
+    router_signature_start = "-----BEGIN SIGNATURE-----\n"
+    router_signature_end = "\n-----END SIGNATURE-----\n"
+    rst_start = desc_content.find(router_signature_token)
+    desc_content = desc_content[:rst_start] + router_signature_token + router_signature_start + signature_base64 + router_signature_end
+    
+    return desc_content
diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py
index fc81851..2e54e44 100644
--- a/test/unit/descriptor/server_descriptor.py
+++ b/test/unit/descriptor/server_descriptor.py
@@ -10,7 +10,7 @@ import stem.prereq
 import stem.descriptor.server_descriptor
 from stem.descriptor.server_descriptor import RelayDescriptor, BridgeDescriptor
 import test.runner
-from test.mocking import get_relay_server_descriptor, get_bridge_server_descriptor, CRYPTO_BLOB
+from test.mocking import get_relay_server_descriptor, get_bridge_server_descriptor, CRYPTO_BLOB, sign_descriptor_content
 
 class TestServerDescriptor(unittest.TestCase):
   def test_minimal_relay_descriptor(self):
@@ -25,8 +25,6 @@ class TestServerDescriptor(unittest.TestCase):
     self.assertEquals("71.35.133.197", desc.address)
     self.assertEquals(None, desc.fingerprint)
     self.assertTrue(CRYPTO_BLOB in desc.onion_key)
-    self.assertTrue(CRYPTO_BLOB in desc.signing_key)
-    self.assertTrue(CRYPTO_BLOB in desc.signature)
   
   def test_with_opt(self):
     """
@@ -148,6 +146,7 @@ class TestServerDescriptor(unittest.TestCase):
     self._expect_invalid_attr(desc_text, "published")
     
     desc_text = get_relay_server_descriptor({"published": "2012-02-29 04:03:19"}, content = True)
+    desc_text = sign_descriptor_content(desc_text)
     expected_published = datetime.datetime(2012, 2, 29, 4, 3, 19)
     self.assertEquals(expected_published, RelayDescriptor(desc_text).published)
   
@@ -200,6 +199,7 @@ class TestServerDescriptor(unittest.TestCase):
     
     desc_text = "@pepperjack very tasty\n at mushrooms not so much\n"
     desc_text += get_relay_server_descriptor(content = True)
+    desc_text = sign_descriptor_content(desc_text)
     desc_text += "\ntrailing text that should be ignored, ho hum"
     
     # running parse_file should provide an iterator with a single descriptor
@@ -243,29 +243,12 @@ class TestServerDescriptor(unittest.TestCase):
         self.assertEquals(None, desc.socks_port)
         self.assertEquals(None, desc.dir_port)
   
-  def test_fingerprint_valid(self):
-    """
-    Checks that a fingerprint matching the hash of our signing key will validate.
-    """
-    
-    if not stem.prereq.is_rsa_available():
-      test.runner.skip(self, "(rsa module unavailable)")
-      return
-    
-    fingerprint = "4F0C 867D F0EF 6816 0568 C826 838F 482C EA7C FE44"
-    desc = get_relay_server_descriptor({"opt fingerprint": fingerprint})
-    self.assertEquals(fingerprint.replace(" ", ""), desc.fingerprint)
-  
   def test_fingerprint_invalid(self):
     """
     Checks that, with a correctly formed fingerprint, we'll fail validation if
     it doesn't match the hash of our signing key.
     """
     
-    if not stem.prereq.is_rsa_available():
-      test.runner.skip(self, "(rsa module unavailable)")
-      return
-    
     fingerprint = "4F0C 867D F0EF 6816 0568 C826 838F 482C EA7C FE45"
     desc_text = get_relay_server_descriptor({"opt fingerprint": fingerprint}, content = True)
     self._expect_invalid_attr(desc_text, "fingerprint", fingerprint.replace(" ", ""))
diff --git a/test/unit/tutorial.py b/test/unit/tutorial.py
index f8a8d09..ae1a829 100644
--- a/test/unit/tutorial.py
+++ b/test/unit/tutorial.py
@@ -39,9 +39,11 @@ class TestTutorial(unittest.TestCase):
     from stem.descriptor.reader import DescriptorReader
     from stem.util import str_tools
     
-    exit_descriptor = RelayDescriptor(mocking.get_relay_server_descriptor({
-      'router': 'speedyexit 149.255.97.109 9001 0 0'
-    }, content = True).replace('reject *:*', 'accept *:*'))
+    exit_descriptor = mocking.get_relay_server_descriptor({
+     'router': 'speedyexit 149.255.97.109 9001 0 0'
+    }, content = True).replace('reject *:*', 'accept *:*')
+    exit_descriptor = mocking.sign_descriptor_content(exit_descriptor)
+    exit_descriptor = RelayDescriptor(exit_descriptor)
     
     reader_wrapper = mocking.get_object(DescriptorReader, {
       '__enter__': lambda x: x,





More information about the tor-commits mailing list