diff --git a/ugly_hacks/santiago/simplesantiago.py b/ugly_hacks/santiago/simplesantiago.py index 47b7421c0..17e9a0e04 100644 --- a/ugly_hacks/santiago/simplesantiago.py +++ b/ugly_hacks/santiago/simplesantiago.py @@ -65,6 +65,8 @@ class Santiago(object): protocols. """ + supported_protocols = set([1]) + def __init__(self, listeners = None, senders = None, hosting = None, consuming = None, me = 0): """Create a Santiago with the specified parameters. @@ -239,190 +241,96 @@ class Santiago(object): attacker knows that the last request brought down a system. """ - logging.debug("Incoming request: ", str(request)) - # no matter what happens, the sender will never hear about it. try: - try: - unpacked = self.unpack_request(request) - except ValueError as e: + if not verify_message(request): return + unpacked = self.unpack_request(request) + if not unpacked: return - logging.debug("Unpacked request: ", str(unpacked)) - if unpacked["locations"]: - self.handle_reply(unpacked["from"], unpacked["to"], - unpacked["host"], unpacked["client"], - unpacked["service"], unpacked["locations"], - unpacked["reply_to"]) + self.handle_reply( + unpacked["from"], unpacked["to"], + unpacked["host"], unpacked["client"], + unpacked["service"], unpacked["locations"], + unpacked["reply_to"], + unpacked["request_version"], + unpacked["reply_version"]) else: - self.handle_request(unpacked["from"], unpacked["to"], - unpacked["host"], unpacked["client"], - unpacked["service"], unpacked["reply_to"]) + self.handle_request( + unpacked["from"], unpacked["to"], + unpacked["host"], unpacked["client"], + unpacked["service"], unpacked["reply_to"], + unpacked["request_version"], + unpacked["reply_version"]) + except Exception as e: logging.exception("Error: ", str(e)) + def verify_request(self, request): + """Make sure the request meets minimum criteria before we process it. + + - The request must contain required keys. + - The request and client must be of and support protocol versions I + understand. + + """ + if False in map(request.__contains__, + ("request", "request_version", "reply_versions")): + return False + + if not (Santiago.supported_protocols & set(request["reply_versions"])): + return False + + if not (Santiago.supported_protocols & + set([request["request_version"]])): + return False + + return True + def unpack_request(self, request): """Decrypt and verify the request. - Raise an (unhandled?) error if there're any inconsistencies in the - message. - - I realize the following is a bit complicated, but this is the only way - we've yet found to avoid bug (####, in Tor). - - The message is wrapped in up to three ways: - - 1. The outermost signature: This layer is applied to the message by the - message's sender. This allows for proxying signed messages between - clients. - - 2. The inner signature: This layer is applied to the message by the - original sender (the requesting client or replying host). The - message's destination is recorded in plain-text in this layer so - proxiers can deliver the message. - - 3. The encrypted message: This layer is used by the host and client to - coordinate the service, hidden from prying eyes. - - Yes, each host and client requires two verifications and one decryption - per message. Each proxier requires two verifications: the inner - signature must be valid, not necessarily trusted. The host and client - are the only folks who must trust the inner signature. Proxiers must - only verify that signature. - - :FIXME: If we duplicate any keys in the signed message (for addressing) - they must be ignored. - - :FIXME: Handle weird requests. what if the client isn't the encrypter?? - in that case, it must be ignored. + The request comes in encrypted and it's decrypted here. If I can't + decrypt it, it's not for me. If it has no signature, I don't want it. """ request = self.gpg.decrypt(request) - if not str(request): - raise InvalidSignatureError() - - if not request.keyid: - # an unsigned or invalid request! + # skip badly signed messages or ones for other folks. + if not (str(request) and request.fingerprint): return - request_body = dict(request) - reqeust_body["to"] = self.me - request_body["from"] = request.keyid - - return request_body - - def verify_sender(self, request): - """Verify the signature of the message's sender. - - This is part (A) in the message diagram. - - Raises an InvalidSignature error when the signature is incorrect. - - Raises an UnwillingHost error when the signer is not a client - authorized to send us Santiago messages. - - At this point (the initial unwrap) request.next() returns a signed - message body that contains, but isn't, the request's body. - - We're verifying the Santiago message sender to make sure the proxier is - allowed to send us messages. - - """ - gpg_data = request.next() - - if not gpg_data: - raise InvalidSignatureError() - - if not self.get_host_locations(gpg_data.fingerprint, "santiago"): - raise UnwillingHostError( - "{0} is not a Santiago client.".format(gpg_data.fingerprint)) - - return request - - def verify_client(self, request): - """Verify the signature of the message's source. - - This is part (B) in the message diagram. - - Raises an InvalidSignature error when the signature is incorrect. - - Raises an UnwillingHost error when the signer is not a client authorized - to send us Santiago messages. - - We shouldn't verify the Santiago client here, it the request goes to - somebody else. - - """ - self.verify_sender(request) - - adict = None + # copy required keys from dictionary + adict = ast.literal_eval(str(request)) + request_body = dict() try: - adict = dict(request.message) - except: + for key in ("host", "client", "service", "locations", "reply_to"): + request_body = adict[key] + except KeyError: return - if not self.i_am(adict["to"]): - self.proxy(adict["request"]) - return - - return request - - def decrypt_client(self, request_body): - """Decrypt the message and validates the encrypted signature. - - This is part (C) in the message diagram. - - Raises an InvalidSignature error when the signature is incorrect. - - Raises an UnwillingHost error when the signer is not a client authorized - to send us Santiago messages. - - """ - self.verify_client(request_body) - - if not self.i_am(request_body["host"]): - return + # set implied keys + request_body["from"] = request.fingerprint + reqeust_body["to"] = self.me return request_body - @staticmethod - def signed_contents(request): - """Return the contents of the signed message. - - TODO: complete. - - """ - if not request.readline() == "-----BEGIN PGP SIGNED MESSAGE-----": - return - - # skip the blank line - # contents = the thingie. - # contents end at "-----BEGIN PGP SIGNATURE-----" - # message ends at "-----END PGP SIGNATURE-----" - def handle_request(self, from_, to, host, client, service, reply_to): """Actually do the request processing. - #. Verify we're willing to host for both the client and proxy. If we - aren't, quit and return nothing. - - #. Forward the request if it's not for me. - - #. Learn new Santiagi if they were sent. - - #. Reply to the client on the appropriate protocol. + - Verify we're willing to host for both the client and proxy. If we + aren't, quit and return nothing. + - Forward the request if it's not for me. + - Learn new Santiagi if they were sent. + - Reply to the client on the appropriate protocol. """ - try: - self.hosting[from_] - self.hosting[client] - except KeyError as e: + if False in map(self.hosting.__contains__, (from_, client)): return if not self.i_am(host): diff --git a/ugly_hacks/santiago/test_santiago.py b/ugly_hacks/santiago/test_santiago.py index c73022a2c..ae26df802 100644 --- a/ugly_hacks/santiago/test_santiago.py +++ b/ugly_hacks/santiago/test_santiago.py @@ -45,7 +45,6 @@ import os import sys import unittest -from pprint import pprint import gnupg import logging from errors import InvalidSignatureError, UnwillingHostError @@ -457,106 +456,167 @@ import pgpprocessor # # pass -class VerifySender(test_pgpprocessor.MessageWrapper): - """Santiago.verify_sender performs as expected. - It must unwrap the message and return the message's (decrypted) body. If - stuff is weird about the message, raise errors: +class VerifyRequest(unittest.TestCase): - - Raise an InvalidSignature error when the signature is incorrect. + """Are incoming requests handled correctly? - - Raise an UnwillingHost error when the signer is not a client authorized to - send us Santiago messages. + - Messages come with a request. + - Each message identifies the Santiago protocol version it uses. + - Messages come with a range of Santiago protocol versions I can reply with. + - Messages that don't share any of my versions are ignored (either the + client or I won't be able to understand the message). + + Test this in a fairly hacky way. """ - def setUp(self): - super(VerifySender, self).setUp() + self.santiago = santiago.Santiago() + self.request = { "request_version": 1, + "reply_versions": [1], + "request": None } - self.santiago = santiago.Santiago( - hosting = { self.keyid: {"santiago": ["1"] }}, - me = self.keyid) + def test_pass_acceptable_request(self): + """A known good request passes.""" - self.method = "verify_sender" + self.assertTrue(self.santiago.verify_request(self.request)) - self.unwrapper = pgpprocessor.Unwrapper(str(self.messages[2])) + def test_required_keys_are_required(self): + """Messages without required keys fail. + + The following keys are required in the un-encrypted part of the message: + + - request + - request_version + - reply_versions + + """ + for key in ("request", "request_version", "reply_versions"): + del self.request[key] + + self.assertFalse(self.santiago.verify_request(self.request)) + + def test_require_protocol_version_overlap(self): + """Clients that can't accept protocols I can send are ignored.""" + + santiago.Santiago.supported_protocols, unsupported = \ + set(["e"]), santiago.Santiago.supported_protocols + + self.assertFalse(self.santiago.verify_request(self.request)) + + santiago.Santiago.supported_protocols, unsupported = \ + unsupported, santiago.Santiago.supported_protocols + + def test_require_protocol_version_understanding(self): + """I must ignore any protocol versions I can't understand.""" + + self.request["request_version"] = "e" + + self.assertFalse(self.santiago.verify_request(self.request)) + + +class UnpackRequest(test_pgpprocessor.MessageWrapper): + + """Are requests unpacked as expected? + + - Messages that aren't for me (that I can't decrypt) are ignored. + - Messages with invalid signatures are rejected. + - The request keys are unpacked correctly: + + - client + - host + - service + - locations + - reply_to + + - Only passing messages return the dictionary. + + """ + def setUp(self): + """Create a request.""" + + self.gpg = gnupg.GPG(use_agent = True) + + self.request = { "host": None, "client": None, + "service": None, "reply_to": None, + "locations": None } + + config = configparser.ConfigParser( + {"KEYID": + "D95C32042EE54FFDB25EC3489F2733F40928D23A"}) + config.read(["test.cfg"]) + self.keyid = config.get("pgpprocessor", "keyid") + + self.santiago = santiago.Santiago() + + def test_requred_keys_are_required(self): + """If any required keys are missing, the message is skipped.""" + + for key in ("host", "client", "service", "reply_to", "locations"): + del self.request[key] + + encrypted_data = self.gpg.encrypt(str(self.request), + recipients=[self.keyid], + sign=self.keyid) + + self.assertEqual( + self.santiago.unpack_request(str(encrypted_data)), + None) + + def test_skip_undecryptable_messages(self): + """Mesasges that I can't decrypt (for other folks) are skipped. + + I don't know how I'll encrypt to a key that isn't there though. + + """ + pass + + def test_skip_invalid_signatures(self): + """Messages with invalid signatures are skipped.""" + + self.request = str(self.gpg.sign(str(self.request), keyid=self.keyid)) + + # delete the 7th line for the fun of it. + mangled = self.request.splitlines(True) + del mangled[7] + self.request = "".join(mangled) + + self.assertEqual(self.santiago.unpack_request(self.request), None) + +class HandleRequest(unittest.TestCase): + """Process an incoming request, from a client, for to host services. + + - Verify we're willing to host for both the client and proxy. If we + aren't, quit and return nothing. + - Forward the request if it's not for me. + - Learn new Santiagi if they were sent. + - Reply to the client on the appropriate protocol. + + """ + def setUp(self): + self.santiago = santiago.Santiago(hosting = {}) + self.santiago.outgoing_request = (lambda **x: self.call_request()) + self.santiago.requested = False + + def call_request(self): + self.santiago.requested = True def test_valid_message(self): - """A valid message (correctly signed and from a trusted host) passes.""" - gpg_data = getattr(self.santiago, self.method)(self.unwrapper) + self.assertTrue(self.santiago.requested) - self.assertEqual(self.messages[1], gpg_data.message) + def test_unwilling_client(self): + """Don't handle the request if the cilent isn't trusted.""" - def test_fail_invalid_signature(self): - """A message with an invalid signature fails + self.santiago.handle_request() - It raises an InvalidSignature error. + self.assertFalse(self.santiago.requested) - """ - message = self.unwrapper.message.splitlines(True) - message[7] += "q" - self.unwrapper.message = "".join(message) + def test_unwilling_proxy(self): + """Don't handle the request if the proxy isn't trusted.""" - self.assertRaises(InvalidSignatureError, - getattr(self.santiago, self.method), self.unwrapper) + self.assertFalse(self.santiago.requested) - def test_fail_invalid_signer(self): - """A message with a valid signature from an untrusted signer fails. - - It raises an UntrustedClient error. - - """ - self.santiago.hosting = { 1: { "santiago": ["1"] }} - - self.assertRaises(UnwillingHostError, - getattr(self.santiago, self.method), self.unwrapper) - -class VerifyClient(VerifySender): - """Santiago.verify_client performs as expected. - - It must unwrap the message and return the message's (decrypted) body. If - stuff is weird about the message, raise errors: - - - Raise an InvalidSignature error when the signature is incorrect. - - - Raise an UnwillingHost error when the signer is not a client authorized to - send us Santiago messages. - - Is this just unnecessarily fucking complicating all of this? Yes. Screw - proxying, just get it out the door by sending the encrypted bits directly. - - """ - def setUp(self): - super(VerifyClient, self).__init__() - - self.method = "verify_client" - - def test_proxy_request(self): - """When the message is for somebody else, it gets proxied.""" - - pass - - def test_return_only_valid_message(self): - """Invalid messages (without "to" and "request" keys) return nothing.""" - - pass - - def test_dont_verify_source(self): - """If the message is being proxied, we don't care who sent the message. - - """ - pass - -def show(name, item, iterations=1): - print "#" * iterations, name, "#" * iterations - if hasattr(item, "__dict__"): - for k, v in item.__dict__.iteritems(): - show(k, v, iterations + 1) - elif type(item) in (str, unicode): - print item - else: - pprint(item) if __name__ == "__main__": logging.disable(logging.CRITICAL)