[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Implementing Relay Descriptor verification
commit e0095fbe54759c45cbf6d1b120d2b17b47a0ec21
Author: Eoin o Fearghail <eoin.o.fearghail@xxxxxxxxx>
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@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,
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits