kopia lustrzana https://gitlab.com/jaywink/federation
Merge pull request #48 from jaywink/new-magic-envelope
New style Diaspora Magic Envelope supportmerge-requests/130/head
commit
8e29497e91
|
@ -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 `<XML><post></post></XML>`. 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
|
||||
|
||||
|
|
|
@ -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 <XML><post></post></XML>.
|
||||
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")
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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="<status_message><foo>bar</foo></status_message>",
|
||||
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="<status_message><foo>bar</foo></status_message>",
|
||||
private_key="key",
|
||||
author_handle="foobar@example.com",
|
||||
wrap_payload=True,
|
||||
)
|
||||
payload = env.create_payload()
|
||||
assert payload == b"<XML><post><status_message><foo>bar</foo></status_message></post></XML>"
|
||||
|
||||
def test_create_payload(self):
|
||||
env = MagicEnvelope(
|
||||
message="<status_message><foo>bar</foo></status_message>",
|
||||
private_key="key",
|
||||
author_handle="foobar@example.com"
|
||||
)
|
||||
payload = env.create_payload()
|
||||
assert payload == b"<status_message><foo>bar</foo></status_message>"
|
||||
|
||||
def test_encode_payload(self):
|
||||
env = MagicEnvelope(
|
||||
message="<status_message><foo>bar</foo></status_message>",
|
||||
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="<status_message><foo>bar</foo></status_message>",
|
||||
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="<status_message><foo>bar</foo></status_message>",
|
||||
private_key=get_dummy_private_key(),
|
||||
author_handle="foobar@example.com"
|
||||
)
|
||||
env.build()
|
||||
output = env.render()
|
||||
assert output == '<me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">' \
|
||||
'<me:encoding>base64url</me:encoding><me:alg>RSA-SHA256</me:alg>' \
|
||||
'<me:data type="application/xml">PHN0YXR1c19tZXNzYWdlPjxmb28-Ym' \
|
||||
'FyPC9mb28-PC9zdGF0dXNfbWVzc2Fn\nZT4=\n</me:data>' \
|
||||
'<me:sig key_id="Zm9vYmFyQGV4YW1wbGUuY29t">RAfiBBrk0OzPbmh6xE7wMRe' \
|
||||
'7ir-qprZ7zk5VDGfopc6rfATFNbNB2FWHFdvJfoky9ORNvfUoiFmtbMG7kmmFHgp' \
|
||||
'QdUl_OU81lKb7NG6-aq2ZRVDQT46UYat1ssdqkkynqywo' \
|
||||
'wdyEGVUxxalFkOHWuYajmpc7ajt_G8xXjMDUCtt0VUFXepxshd24ZWRX' \
|
||||
'O1RQK4bFr7X9-d26Ho3kLuB1VB_pYYbxJQCZlm0EDlFj7vktl0zibs' \
|
||||
'wMFyRqiacwu8zec_HR4x8yMkF_zSNJsnnLq6ch4ad6r83LOVk3Yvdxin' \
|
||||
'b61spHEjr2zvPWExEgUt4Jcpc07aZRUKCJVfFXFYAGnA==</me:sig></me:env>'
|
||||
env2 = MagicEnvelope(
|
||||
message="<status_message><foo>bar</foo></status_message>",
|
||||
private_key=get_dummy_private_key(),
|
||||
author_handle="foobar@example.com"
|
||||
)
|
||||
output2 = env2.render()
|
||||
assert output2 == output
|
Ładowanie…
Reference in New Issue