mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-20 10:34:30 +00:00
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:
parent
568ae9f2f3
commit
ab0986ed42
@ -65,6 +65,8 @@ class Santiago(object):
|
|||||||
protocols.
|
protocols.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
supported_protocols = set([1])
|
||||||
|
|
||||||
def __init__(self, listeners = None, senders = None,
|
def __init__(self, listeners = None, senders = None,
|
||||||
hosting = None, consuming = None, me = 0):
|
hosting = None, consuming = None, me = 0):
|
||||||
"""Create a Santiago with the specified parameters.
|
"""Create a Santiago with the specified parameters.
|
||||||
@ -239,190 +241,96 @@ class Santiago(object):
|
|||||||
attacker knows that the last request brought down a system.
|
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.
|
# no matter what happens, the sender will never hear about it.
|
||||||
try:
|
try:
|
||||||
try:
|
if not verify_message(request):
|
||||||
unpacked = self.unpack_request(request)
|
|
||||||
except ValueError as e:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
unpacked = self.unpack_request(request)
|
||||||
|
|
||||||
if not unpacked:
|
if not unpacked:
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.debug("Unpacked request: ", str(unpacked))
|
|
||||||
|
|
||||||
if unpacked["locations"]:
|
if unpacked["locations"]:
|
||||||
self.handle_reply(unpacked["from"], unpacked["to"],
|
self.handle_reply(
|
||||||
unpacked["host"], unpacked["client"],
|
unpacked["from"], unpacked["to"],
|
||||||
unpacked["service"], unpacked["locations"],
|
unpacked["host"], unpacked["client"],
|
||||||
unpacked["reply_to"])
|
unpacked["service"], unpacked["locations"],
|
||||||
|
unpacked["reply_to"],
|
||||||
|
unpacked["request_version"],
|
||||||
|
unpacked["reply_version"])
|
||||||
else:
|
else:
|
||||||
self.handle_request(unpacked["from"], unpacked["to"],
|
self.handle_request(
|
||||||
unpacked["host"], unpacked["client"],
|
unpacked["from"], unpacked["to"],
|
||||||
unpacked["service"], unpacked["reply_to"])
|
unpacked["host"], unpacked["client"],
|
||||||
|
unpacked["service"], unpacked["reply_to"],
|
||||||
|
unpacked["request_version"],
|
||||||
|
unpacked["reply_version"])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("Error: ", str(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):
|
def unpack_request(self, request):
|
||||||
"""Decrypt and verify the request.
|
"""Decrypt and verify the request.
|
||||||
|
|
||||||
Raise an (unhandled?) error if there're any inconsistencies in the
|
The request comes in encrypted and it's decrypted here. If I can't
|
||||||
message.
|
decrypt it, it's not for me. If it has no signature, I don't want it.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
request = self.gpg.decrypt(request)
|
request = self.gpg.decrypt(request)
|
||||||
|
|
||||||
if not str(request):
|
# skip badly signed messages or ones for other folks.
|
||||||
raise InvalidSignatureError()
|
if not (str(request) and request.fingerprint):
|
||||||
|
|
||||||
if not request.keyid:
|
|
||||||
# an unsigned or invalid request!
|
|
||||||
return
|
return
|
||||||
|
|
||||||
request_body = dict(request)
|
# copy required keys from dictionary
|
||||||
reqeust_body["to"] = self.me
|
adict = ast.literal_eval(str(request))
|
||||||
request_body["from"] = request.keyid
|
request_body = dict()
|
||||||
|
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
adict = dict(request.message)
|
for key in ("host", "client", "service", "locations", "reply_to"):
|
||||||
except:
|
request_body = adict[key]
|
||||||
|
except KeyError:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.i_am(adict["to"]):
|
# set implied keys
|
||||||
self.proxy(adict["request"])
|
request_body["from"] = request.fingerprint
|
||||||
return
|
reqeust_body["to"] = self.me
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
return request_body
|
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,
|
def handle_request(self, from_, to, host, client,
|
||||||
service, reply_to):
|
service, reply_to):
|
||||||
"""Actually do the request processing.
|
"""Actually do the request processing.
|
||||||
|
|
||||||
#. Verify we're willing to host for both the client and proxy. If we
|
- Verify we're willing to host for both the client and proxy. If we
|
||||||
aren't, quit and return nothing.
|
aren't, quit and return nothing.
|
||||||
|
- Forward the request if it's not for me.
|
||||||
#. Forward the request if it's not for me.
|
- Learn new Santiagi if they were sent.
|
||||||
|
- Reply to the client on the appropriate protocol.
|
||||||
#. Learn new Santiagi if they were sent.
|
|
||||||
|
|
||||||
#. Reply to the client on the appropriate protocol.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
if False in map(self.hosting.__contains__, (from_, client)):
|
||||||
self.hosting[from_]
|
|
||||||
self.hosting[client]
|
|
||||||
except KeyError as e:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.i_am(host):
|
if not self.i_am(host):
|
||||||
|
|||||||
@ -45,7 +45,6 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from pprint import pprint
|
|
||||||
import gnupg
|
import gnupg
|
||||||
import logging
|
import logging
|
||||||
from errors import InvalidSignatureError, UnwillingHostError
|
from errors import InvalidSignatureError, UnwillingHostError
|
||||||
@ -457,106 +456,167 @@ import pgpprocessor
|
|||||||
#
|
#
|
||||||
# pass
|
# 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
|
class VerifyRequest(unittest.TestCase):
|
||||||
stuff is weird about the message, raise errors:
|
|
||||||
|
|
||||||
- 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
|
- Messages come with a request.
|
||||||
send us Santiago messages.
|
- 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):
|
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(
|
def test_pass_acceptable_request(self):
|
||||||
hosting = { self.keyid: {"santiago": ["1"] }},
|
"""A known good request passes."""
|
||||||
me = self.keyid)
|
|
||||||
|
|
||||||
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):
|
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):
|
self.santiago.handle_request()
|
||||||
"""A message with an invalid signature fails
|
|
||||||
|
|
||||||
It raises an InvalidSignature error.
|
self.assertFalse(self.santiago.requested)
|
||||||
|
|
||||||
"""
|
def test_unwilling_proxy(self):
|
||||||
message = self.unwrapper.message.splitlines(True)
|
"""Don't handle the request if the proxy isn't trusted."""
|
||||||
message[7] += "q"
|
|
||||||
self.unwrapper.message = "".join(message)
|
|
||||||
|
|
||||||
self.assertRaises(InvalidSignatureError,
|
self.assertFalse(self.santiago.requested)
|
||||||
getattr(self.santiago, self.method), self.unwrapper)
|
|
||||||
|
|
||||||
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__":
|
if __name__ == "__main__":
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user