From 0b91e828d462a6b5556931b4e63d13b8ef0517be Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Mon, 12 Sep 2016 22:50:48 +0300 Subject: [PATCH] New style Diaspora Magic Envelope support Not used in actual federation yet. Offers a class to build the envelope separately. Closes #47 --- CHANGELOG.md | 8 +- .../protocols/diaspora/magic_envelope.py | 83 ++++++++++++++++ federation/protocols/diaspora/protocol.py | 5 +- federation/tests/fixtures/keys.py | 34 +++++++ .../tests/protocols/diaspora/test_diaspora.py | 1 - .../protocols/diaspora/test_magic_envelope.py | 96 +++++++++++++++++++ 6 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 federation/protocols/diaspora/magic_envelope.py create mode 100644 federation/tests/fixtures/keys.py create mode 100644 federation/tests/protocols/diaspora/test_magic_envelope.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ea62a8..32328e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ ## [unreleased] -## Changed -* Deprecate receiving user `key` attribute for Diaspora protocol. Instead correct attribute is now `private_key` for any user passed to `federation.inbound.handle_receive`. We already use `private_key` in the message creation code so this is just to unify the user related required attributes. There is a fallback with `key` for user objects in the receiving payload part of the Diaspora protocol until 0.8.0. +### Added +* New style Diaspora Magic Envelope support. The magic envelope can be created using the class `federation.protocols.diaspora.magic_envelope.MagicEnvelope`. By default this will not wrap the payload message in ``. To provide that functionality the class should be initialized with `wrap_payload=True`. No changes are made to the protocol send methods yet, if you need this new magic envelope you can initialize and render it directly. + +### Changed +* Deprecate receiving user `key` attribute for Diaspora protocol. Instead correct attribute is now `private_key` for any user passed to `federation.inbound.handle_receive`. We already use `private_key` in the message creation code so this is just to unify the user related required attributes. + * DEPRECATION: There is a fallback with `key` for user objects in the receiving payload part of the Diaspora protocol until 0.8.0. ## [0.5.0] - 2016-09-05 diff --git a/federation/protocols/diaspora/magic_envelope.py b/federation/protocols/diaspora/magic_envelope.py new file mode 100644 index 0000000..05fcc8c --- /dev/null +++ b/federation/protocols/diaspora/magic_envelope.py @@ -0,0 +1,83 @@ +from base64 import urlsafe_b64encode, b64encode + +from Crypto.Hash import SHA256 +from Crypto.Signature import PKCS1_v1_5 as PKCSSign +from lxml import etree + + +class MagicEnvelope(object): + """Diaspora protocol magic envelope. + + See: http://diaspora.github.io/diaspora_federation/federation/magicsig.html + """ + + nsmap = { + 'me': 'http://salmon-protocol.org/ns/magic-env' + } + + def __init__(self, message, private_key, author_handle, wrap_payload=False): + """ + Args: + wrap_payload (bool) - Whether to wrap the message in . + This is part of the legacy Diaspora protocol which will be removed in the future. (default False) + """ + self.message = message + self.private_key = private_key + self.author_handle = author_handle + self.wrap_payload = wrap_payload + self.doc = None + self.payload = None + + def _encode_payload(self): + """Encode the payload and wrap it to 60 char lines.""" + self.payload = urlsafe_b64encode(self.payload).decode("ascii") + self.payload = '\n'.join( + [self.payload[start:start + 60] for start in range(0, len(self.payload), 60)] + ) + self.payload += "\n" + return self.payload + + def create_payload(self): + """Create the payload doc. + + Returns: + bytes + """ + doc = etree.fromstring(self.message) + if self.wrap_payload: + wrap = etree.Element("XML") + post = etree.SubElement(wrap, "post") + post.append(doc) + doc = wrap + self.payload = etree.tostring(doc, encoding="utf-8") + return self.payload + + def _build_signature(self): + """Create the signature using the private key.""" + sig_contents = \ + self.payload + "." + \ + b64encode(b"application/xml").decode("ascii") + "." + \ + b64encode(b"base64url").decode("ascii") + "." + \ + b64encode(b"RSA-SHA256").decode("ascii") + sig_hash = SHA256.new(sig_contents.encode("ascii")) + cipher = PKCSSign.new(self.private_key) + sig = urlsafe_b64encode(cipher.sign(sig_hash)) + key_id = urlsafe_b64encode(bytes(self.author_handle, encoding="utf-8")) + return sig, key_id + + def build(self): + self.doc = etree.Element("{%s}env" % self.nsmap["me"], nsmap=self.nsmap) + etree.SubElement(self.doc, "{%s}encoding" % self.nsmap["me"]).text = 'base64url' + etree.SubElement(self.doc, "{%s}alg" % self.nsmap["me"]).text = 'RSA-SHA256' + self.create_payload() + self._encode_payload() + etree.SubElement(self.doc, "{%s}data" % self.nsmap["me"], + {"type": "application/xml"}).text = self.payload + signature, key_id = self._build_signature() + etree.SubElement(self.doc, "{%s}sig" % self.nsmap["me"], key_id=key_id).text = signature + return self.doc + + def render(self): + if self.doc is None: + self.build() + return etree.tostring(self.doc, encoding="unicode") diff --git a/federation/protocols/diaspora/protocol.py b/federation/protocols/diaspora/protocol.py index 26e87a2..301c6a3 100644 --- a/federation/protocols/diaspora/protocol.py +++ b/federation/protocols/diaspora/protocol.py @@ -335,8 +335,11 @@ class Protocol(BaseProtocol): encrypted header. Selected elements are signed by the author so that tampering can be detected. + Note, this corresponds to the old Diaspora protocol which will slowly be replaced by the + new version. See PR https://github.com/diaspora/diaspora_federation/issues/30 + Args: - recipient - Recipient object which must have public key as `key` + recipient - Recipient object which must have public key as `key` (private messages only) Returns: XML document as string diff --git a/federation/tests/fixtures/keys.py b/federation/tests/fixtures/keys.py new file mode 100644 index 0000000..ad699b6 --- /dev/null +++ b/federation/tests/fixtures/keys.py @@ -0,0 +1,34 @@ +from Crypto.PublicKey import RSA + + +PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n" \ + "MIIEogIBAAKCAQEAiY2JBgMV90ULt0btku198l6wGuzn3xCcHs+eBZHL2C+XWRA3\n" \ + "BVDThSBj19dKXehfDphQ5u/Omfm76ImajEPHGBiYtZT7AgcO15zvm+JCpbREbdOV\n" \ + "QkST3ANyqCzi+Fk0ZWRwXQTR9m64ML++42iK0BESUbbrVnKipZJ1tE73xs1XBM8J\n" \ + "DCOIdM2VBVdDArNJZHGzqugEbDzwh0SqEsKYLE7uzst+eY9vIAbyX80pNzC/d1J8\n" \ + "3Pia5WvRV0gtllkMXlGnTIortDJuEr496a8UqfPWDWNg4scCca6aSk/13Q8ClEbP\n" \ + "X1sdW4s9yW9OmGg0VMZj+Tca3Jls/3FJosH0yQIDAQABAoIBADVdDGihr9bjGX17\n" \ + "7dUPf8oUg/ueJwJ5/idR4ntEqbFwHSY3TTEpvzWpcDKfWkF+UcpmuxQsupkvsn+v\n" \ + "Sp7Z+JZXjH79kjeiJ1bskmSGbda9TcLRz9kKo9Y6HDQ0XcV9Tf977L+ZjB8vqxN2\n" \ + "gAbXWusHhHThIwHBrWnQnQtbi3K7SzVT3OK0WFfsoAZgYSzfS+4LE0Gs9+ZcK8q7\n" \ + "So4BE7/jSjf+Baux92Hes5spi73ltx/BsyEYR5XQVzWfIUg4sX3VDRbpBTW+DBqA\n" \ + "G0kUh3CjlsPkZeRSiPrAfk610hQr4HLInGxPkaK+8Fuui2ofM0qYwOeGkNXqlY4Z\n" \ + "huhXcFUCgYEAtX0/KoF9k52FbSJdl+2ekeBluU9fJyB3SpGyk5MTKeoAo9I82KyJ\n" \ + "tens+5ebj8rUZYHTQfjHsm0ihy4F3GH+huPw4B+RQ8h5BLkU5+KC6pT60M+eMj13\n" \ + "bJZkm9n4bInDx9f8Aj4XSG+P2g8h9dBSSm4Ewiqp4CtFTY58uujvMu8CgYEAwgaE\n" \ + "5vanfxfk08qvZ7WSxUGfZxp6R2sLjfyB2qL4XJk/8ZpLB17kpYdGhhpk5qWRNmlH\n" \ + "vetLp3RZoZRB0JJYq++IkiIq1gfnghgKcSbM8sMXvIT0icBXZU/XTzBVReeRYf9P\n" \ + "Sjc+zD/W6L2lXhdZ7z1rGHHvEH/bMQEj3vIQc8cCgYAN6awN9h9KUakI1LmYC/87\n" \ + "75fcvNjuhu6eKM0nwv6VF/s0k8lWUuO7rlMcdmLWgxYFMg6f4BJu+y7KbhzE6D46\n" \ + "2P5+L+1S5OtiEU4o+JRQp1sS5teZwlyFVoIf8HW63FTF3SjUgy4Fv4enj8Fqtq2Y\n" \ + "RxbWS676IFcPuvyU14Z+wQKBgARZWw9GRhjeMz3gFDBx7HlJcEZCXK1PI/Ipz8tT\n" \ + "zdddhAZpW/ctVFi1gIou+0YEPg4HLBmAtbBqNjwd85+2OBCajOghpe4oPTM4ULua\n" \ + "kAt8/gI2xLh1vD/EG2JmBfNMLoEQ1Pkn5dt0LuAGqDdEtLpdGRJyM1aeVw5xJRmx\n" \ + "OVcvAoGAO2keIaA0uB9SszdgovK22pzmkluCIB7ldcjuf/zkjt62nSOOa3mtEAue\n" \ + "t/b5Jw+yQVBqNkfJwOMykCxcYs4IEuJelbOYSCp3GmW014nDxYbe5y1Q40drdTro\n" \ + "w6Y5FnjFw022w+M3exyH6ZtxcmG6buDbp2F/SPD/FnYy5IFCDig=\n" \ + "-----END RSA PRIVATE KEY-----" + + +def get_dummy_private_key(): + return RSA.importKey(PRIVATE_KEY) diff --git a/federation/tests/protocols/diaspora/test_diaspora.py b/federation/tests/protocols/diaspora/test_diaspora.py index ecda298..3226e19 100644 --- a/federation/tests/protocols/diaspora/test_diaspora.py +++ b/federation/tests/protocols/diaspora/test_diaspora.py @@ -8,7 +8,6 @@ import pytest from federation.exceptions import EncryptedMessageError, NoSenderKeyFoundError, NoHeaderInMessageError from federation.protocols.diaspora.protocol import Protocol, identify_payload -from federation.tests.factories.entities import DiasporaPostFactory from federation.tests.fixtures.payloads import ENCRYPTED_DIASPORA_PAYLOAD, UNENCRYPTED_DIASPORA_PAYLOAD diff --git a/federation/tests/protocols/diaspora/test_magic_envelope.py b/federation/tests/protocols/diaspora/test_magic_envelope.py new file mode 100644 index 0000000..5aedbea --- /dev/null +++ b/federation/tests/protocols/diaspora/test_magic_envelope.py @@ -0,0 +1,96 @@ +from Crypto import Random +from Crypto.PublicKey import RSA +from lxml.etree import _Element + +from federation.protocols.diaspora.magic_envelope import MagicEnvelope +from federation.tests.fixtures.keys import get_dummy_private_key + + +class TestMagicEnvelope(object): + @staticmethod + def generate_rsa_private_key(): + """Generate a new RSA private key.""" + rand = Random.new().read + return RSA.generate(2048, rand) + + def test_build(self): + env = MagicEnvelope( + message="bar", + private_key=get_dummy_private_key(), + author_handle="foobar@example.com" + ) + doc = env.build() + assert isinstance(doc, _Element) + + def test_create_payload_wrapped(self): + env = MagicEnvelope( + message="bar", + private_key="key", + author_handle="foobar@example.com", + wrap_payload=True, + ) + payload = env.create_payload() + assert payload == b"bar" + + def test_create_payload(self): + env = MagicEnvelope( + message="bar", + private_key="key", + author_handle="foobar@example.com" + ) + payload = env.create_payload() + assert payload == b"bar" + + def test_encode_payload(self): + env = MagicEnvelope( + message="bar", + private_key="key", + author_handle="foobar@example.com" + ) + env.create_payload() + payload = env._encode_payload() + assert payload == "PHN0YXR1c19tZXNzYWdlPjxmb28-YmFyPC9mb28-PC9zdGF0dXNfbWVzc2Fn\nZT4=\n" + + def test_build_signature(self): + env = MagicEnvelope( + message="bar", + private_key=get_dummy_private_key(), + author_handle="foobar@example.com" + ) + env.create_payload() + env._encode_payload() + signature, key_id = env._build_signature() + assert signature == b"RAfiBBrk0OzPbmh6xE7wMRe7ir-qprZ7zk5VDGfopc6rfATFNbNB2FWH" \ + b"FdvJfoky9ORNvfUoiFmtbMG7kmmFHgpQdUl_OU81lKb7NG6-aq2ZRVDQ" \ + b"T46UYat1ssdqkkynqywowdyEGVUxxalFkOHWuYajmpc7ajt_G8xXjMDU" \ + b"Ctt0VUFXepxshd24ZWRXO1RQK4bFr7X9-d26Ho3kLuB1VB_pYYbxJQCZl" \ + b"m0EDlFj7vktl0zibswMFyRqiacwu8zec_HR4x8yMkF_zSNJsnnLq6ch4ad6" \ + b"r83LOVk3Yvdxinb61spHEjr2zvPWExEgUt4Jcpc07aZRUKCJVfFXFYAGnA==" + assert key_id == b"Zm9vYmFyQGV4YW1wbGUuY29t" + + def test_render(self): + env = MagicEnvelope( + message="bar", + private_key=get_dummy_private_key(), + author_handle="foobar@example.com" + ) + env.build() + output = env.render() + assert output == '' \ + 'base64urlRSA-SHA256' \ + 'PHN0YXR1c19tZXNzYWdlPjxmb28-Ym' \ + 'FyPC9mb28-PC9zdGF0dXNfbWVzc2Fn\nZT4=\n' \ + 'RAfiBBrk0OzPbmh6xE7wMRe' \ + '7ir-qprZ7zk5VDGfopc6rfATFNbNB2FWHFdvJfoky9ORNvfUoiFmtbMG7kmmFHgp' \ + 'QdUl_OU81lKb7NG6-aq2ZRVDQT46UYat1ssdqkkynqywo' \ + 'wdyEGVUxxalFkOHWuYajmpc7ajt_G8xXjMDUCtt0VUFXepxshd24ZWRX' \ + 'O1RQK4bFr7X9-d26Ho3kLuB1VB_pYYbxJQCZlm0EDlFj7vktl0zibs' \ + 'wMFyRqiacwu8zec_HR4x8yMkF_zSNJsnnLq6ch4ad6r83LOVk3Yvdxin' \ + 'b61spHEjr2zvPWExEgUt4Jcpc07aZRUKCJVfFXFYAGnA==' + env2 = MagicEnvelope( + message="bar", + private_key=get_dummy_private_key(), + author_handle="foobar@example.com" + ) + output2 = env2.render() + assert output2 == output