Removed proxying code; incoming message handling finished.

simplesantiago
==============

- Removed the unnecessary proxying message functions.
- Messages are now versioned.  
- Each Santiago now has supported (incoming/outgoing) protocol versions.
- Completed santiago.unpack_request.

test_santiago
=============

- Removed VerifySender and VerifyClient, they're no longer needed.
- Completed VerifyRequest, UnpackRequest.
- Started HandleRequest.
This commit is contained in:
Nick Daly 2012-05-12 15:40:08 -05:00
parent 568ae9f2f3
commit ab0986ed42
2 changed files with 202 additions and 234 deletions

View File

@ -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):

View File

@ -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)