kopia lustrzana https://gitlab.com/jaywink/federation
Merge pull request #81 from jaywink/salmon-receive-refactoring
Add support for new style Diaspora Salmon magic envelope in public payloadsmerge-requests/130/head
commit
e0dd39d518
|
@ -1,5 +1,13 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [unreleased]
|
||||||
|
|
||||||
|
### Backwards incompatible changes
|
||||||
|
* Removed exception class `NoHeaderInMessageError`. New style Diaspora protocol does not have a custom header in the Salmon magic envelope and thus there is no need to raise this anywhere.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* New style Diaspora public payloads are now supported (see [here](https://github.com/diaspora/diaspora_federation/issues/30)). Old style payloads are still supported. Payloads are also still sent out old style.
|
||||||
|
|
||||||
## [0.11.0] - 2017-05-08
|
## [0.11.0] - 2017-05-08
|
||||||
|
|
||||||
### Backwards incompatible changes
|
### Backwards incompatible changes
|
||||||
|
|
|
@ -3,11 +3,6 @@ class EncryptedMessageError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NoHeaderInMessageError(Exception):
|
|
||||||
"""Message payload is missing required header."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NoSenderKeyFoundError(Exception):
|
class NoSenderKeyFoundError(Exception):
|
||||||
"""Sender private key was not available to sign a payload message."""
|
"""Sender private key was not available to sign a payload message."""
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
from base64 import urlsafe_b64encode, b64encode
|
from base64 import urlsafe_b64encode, b64encode, urlsafe_b64decode
|
||||||
|
|
||||||
from Crypto.Hash import SHA256
|
from Crypto.Hash import SHA256
|
||||||
from Crypto.Signature import PKCS1_v1_5 as PKCSSign
|
from Crypto.Signature import PKCS1_v1_5 as PKCSSign
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
class MagicEnvelope(object):
|
NAMESPACE = "http://salmon-protocol.org/ns/magic-env"
|
||||||
|
|
||||||
|
|
||||||
|
class MagicEnvelope():
|
||||||
"""Diaspora protocol magic envelope.
|
"""Diaspora protocol magic envelope.
|
||||||
|
|
||||||
See: http://diaspora.github.io/diaspora_federation/federation/magicsig.html
|
See: http://diaspora.github.io/diaspora_federation/federation/magicsig.html
|
||||||
"""
|
"""
|
||||||
|
|
||||||
nsmap = {
|
nsmap = {
|
||||||
'me': 'http://salmon-protocol.org/ns/magic-env'
|
"me": NAMESPACE,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, message, private_key, author_handle, wrap_payload=False):
|
def __init__(self, message, private_key, author_handle, wrap_payload=False):
|
||||||
|
@ -28,6 +31,16 @@ class MagicEnvelope(object):
|
||||||
self.doc = None
|
self.doc = None
|
||||||
self.payload = None
|
self.payload = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_sender(doc):
|
||||||
|
"""Get the key_id from the `sig` element which contains urlsafe_b64encoded Diaspora handle.
|
||||||
|
|
||||||
|
:param doc: ElementTree document
|
||||||
|
:returns: Diaspora handle
|
||||||
|
"""
|
||||||
|
key_id = doc.find(".//{%s}sig" % NAMESPACE).get("key_id")
|
||||||
|
return urlsafe_b64decode(key_id).decode("utf-8")
|
||||||
|
|
||||||
def create_payload(self):
|
def create_payload(self):
|
||||||
"""Create the payload doc.
|
"""Create the payload doc.
|
||||||
|
|
||||||
|
@ -58,14 +71,13 @@ class MagicEnvelope(object):
|
||||||
return sig, key_id
|
return sig, key_id
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
self.doc = etree.Element("{%s}env" % self.nsmap["me"], nsmap=self.nsmap)
|
self.doc = etree.Element("{%s}env" % NAMESPACE, nsmap=self.nsmap)
|
||||||
etree.SubElement(self.doc, "{%s}encoding" % self.nsmap["me"]).text = 'base64url'
|
etree.SubElement(self.doc, "{%s}encoding" % NAMESPACE).text = 'base64url'
|
||||||
etree.SubElement(self.doc, "{%s}alg" % self.nsmap["me"]).text = 'RSA-SHA256'
|
etree.SubElement(self.doc, "{%s}alg" % NAMESPACE).text = 'RSA-SHA256'
|
||||||
self.create_payload()
|
self.create_payload()
|
||||||
etree.SubElement(self.doc, "{%s}data" % self.nsmap["me"],
|
etree.SubElement(self.doc, "{%s}data" % NAMESPACE, {"type": "application/xml"}).text = self.payload
|
||||||
{"type": "application/xml"}).text = self.payload
|
|
||||||
signature, key_id = self._build_signature()
|
signature, key_id = self._build_signature()
|
||||||
etree.SubElement(self.doc, "{%s}sig" % self.nsmap["me"], key_id=key_id).text = signature
|
etree.SubElement(self.doc, "{%s}sig" % NAMESPACE, key_id=key_id).text = signature
|
||||||
return self.doc
|
return self.doc
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
|
|
|
@ -11,10 +11,9 @@ from Crypto.Random import get_random_bytes
|
||||||
from Crypto.Signature import PKCS1_v1_5 as PKCSSign
|
from Crypto.Signature import PKCS1_v1_5 as PKCSSign
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from federation.exceptions import (
|
from federation.exceptions import EncryptedMessageError, NoSenderKeyFoundError, SignatureVerificationError
|
||||||
EncryptedMessageError, NoHeaderInMessageError, NoSenderKeyFoundError, SignatureVerificationError,
|
|
||||||
)
|
|
||||||
from federation.protocols.base import BaseProtocol
|
from federation.protocols.base import BaseProtocol
|
||||||
|
from federation.protocols.diaspora.magic_envelope import MagicEnvelope
|
||||||
|
|
||||||
logger = logging.getLogger("federation")
|
logger = logging.getLogger("federation")
|
||||||
|
|
||||||
|
@ -67,6 +66,7 @@ class Protocol(BaseProtocol):
|
||||||
xml = xml.lstrip().encode("utf-8")
|
xml = xml.lstrip().encode("utf-8")
|
||||||
logger.debug("diaspora.protocol.receive: xml content: %s", xml)
|
logger.debug("diaspora.protocol.receive: xml content: %s", xml)
|
||||||
self.doc = etree.fromstring(xml)
|
self.doc = etree.fromstring(xml)
|
||||||
|
# Check for a legacy header
|
||||||
self.find_header()
|
self.find_header()
|
||||||
# Open payload and get actual message
|
# Open payload and get actual message
|
||||||
self.content = self.get_message_content()
|
self.content = self.get_message_content()
|
||||||
|
@ -88,12 +88,16 @@ class Protocol(BaseProtocol):
|
||||||
return self.user.private_key
|
return self.user.private_key
|
||||||
|
|
||||||
def find_header(self):
|
def find_header(self):
|
||||||
|
self.encrypted = self.legacy = False
|
||||||
self.header = self.doc.find(".//{"+PROTOCOL_NS+"}header")
|
self.header = self.doc.find(".//{"+PROTOCOL_NS+"}header")
|
||||||
if self.header != None:
|
if self.header != None:
|
||||||
self.encrypted = False
|
# Legacy public header found
|
||||||
|
self.legacy = True
|
||||||
return
|
return
|
||||||
if self.doc.find(".//{" + PROTOCOL_NS + "}encrypted_header") == None:
|
if self.doc.find(".//{" + PROTOCOL_NS + "}encrypted_header") == None:
|
||||||
raise NoHeaderInMessageError("Could not find header in message")
|
# No legacy encrypted header found
|
||||||
|
return
|
||||||
|
self.legacy = True
|
||||||
if not self.user:
|
if not self.user:
|
||||||
raise EncryptedMessageError("Cannot decrypt private message without user object")
|
raise EncryptedMessageError("Cannot decrypt private message without user object")
|
||||||
user_private_key = self._get_user_key(self.user)
|
user_private_key = self._get_user_key(self.user)
|
||||||
|
@ -104,6 +108,11 @@ class Protocol(BaseProtocol):
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_sender(self):
|
def get_sender(self):
|
||||||
|
if self.legacy:
|
||||||
|
return self.get_sender_legacy()
|
||||||
|
return MagicEnvelope.get_sender(self.doc)
|
||||||
|
|
||||||
|
def get_sender_legacy(self):
|
||||||
try:
|
try:
|
||||||
return self.header.find(".//{"+PROTOCOL_NS+"}author_id").text
|
return self.header.find(".//{"+PROTOCOL_NS+"}author_id").text
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
from Crypto import Random
|
from Crypto import Random
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from lxml.etree import _Element
|
from lxml.etree import _Element
|
||||||
|
|
||||||
from federation.protocols.diaspora.magic_envelope import MagicEnvelope
|
from federation.protocols.diaspora.magic_envelope import MagicEnvelope
|
||||||
from federation.tests.fixtures.keys import get_dummy_private_key
|
from federation.tests.fixtures.keys import get_dummy_private_key
|
||||||
|
from federation.tests.fixtures.payloads import DIASPORA_PUBLIC_PAYLOAD
|
||||||
|
|
||||||
|
|
||||||
class TestMagicEnvelope(object):
|
class TestMagicEnvelope():
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_rsa_private_key():
|
def generate_rsa_private_key():
|
||||||
"""Generate a new RSA private key."""
|
"""Generate a new RSA private key."""
|
||||||
|
@ -80,3 +83,7 @@ class TestMagicEnvelope(object):
|
||||||
)
|
)
|
||||||
output2 = env2.render()
|
output2 = env2.render()
|
||||||
assert output2 == output
|
assert output2 == output
|
||||||
|
|
||||||
|
def test_get_sender(self):
|
||||||
|
doc = etree.fromstring(bytes(DIASPORA_PUBLIC_PAYLOAD, encoding="utf-8"))
|
||||||
|
assert MagicEnvelope.get_sender(doc) == "foobar@example.com"
|
||||||
|
|
|
@ -5,7 +5,7 @@ from xml.etree.ElementTree import ElementTree
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from federation.exceptions import EncryptedMessageError, NoSenderKeyFoundError, NoHeaderInMessageError
|
from federation.exceptions import EncryptedMessageError, NoSenderKeyFoundError
|
||||||
from federation.protocols.diaspora.protocol import Protocol, identify_payload
|
from federation.protocols.diaspora.protocol import Protocol, identify_payload
|
||||||
from federation.tests.fixtures.payloads import (
|
from federation.tests.fixtures.payloads import (
|
||||||
ENCRYPTED_LEGACY_DIASPORA_PAYLOAD, UNENCRYPTED_LEGACY_DIASPORA_PAYLOAD,
|
ENCRYPTED_LEGACY_DIASPORA_PAYLOAD, UNENCRYPTED_LEGACY_DIASPORA_PAYLOAD,
|
||||||
|
@ -105,12 +105,6 @@ class TestDiasporaProtocol(DiasporaTestBase):
|
||||||
with pytest.raises(NoSenderKeyFoundError):
|
with pytest.raises(NoSenderKeyFoundError):
|
||||||
protocol.receive(UNENCRYPTED_LEGACY_DIASPORA_PAYLOAD, user, mock_not_found_get_contact_key)
|
protocol.receive(UNENCRYPTED_LEGACY_DIASPORA_PAYLOAD, user, mock_not_found_get_contact_key)
|
||||||
|
|
||||||
def test_find_header_raises_if_header_cannot_be_found(self):
|
|
||||||
protocol = self.init_protocol()
|
|
||||||
protocol.doc = etree.fromstring("<foo>bar</foo>")
|
|
||||||
with pytest.raises(NoHeaderInMessageError):
|
|
||||||
protocol.find_header()
|
|
||||||
|
|
||||||
def test_get_message_content(self):
|
def test_get_message_content(self):
|
||||||
protocol = self.init_protocol()
|
protocol = self.init_protocol()
|
||||||
protocol.doc = self.get_unencrypted_doc()
|
protocol.doc = self.get_unencrypted_doc()
|
||||||
|
@ -130,25 +124,25 @@ class TestDiasporaProtocol(DiasporaTestBase):
|
||||||
def test_identify_payload_with_other_payload(self):
|
def test_identify_payload_with_other_payload(self):
|
||||||
assert identify_payload("foobar not a diaspora protocol") == False
|
assert identify_payload("foobar not a diaspora protocol") == False
|
||||||
|
|
||||||
def test_get_sender_returns_sender_in_header(self):
|
def test_get_sender_legacy_returns_sender_in_header(self):
|
||||||
protocol = self.init_protocol()
|
protocol = self.init_protocol()
|
||||||
protocol.doc = self.get_unencrypted_doc()
|
protocol.doc = self.get_unencrypted_doc()
|
||||||
protocol.find_header()
|
protocol.find_header()
|
||||||
assert protocol.get_sender() == "bob@example.com"
|
assert protocol.get_sender_legacy() == "bob@example.com"
|
||||||
|
|
||||||
def test_get_sender_returns_sender_in_content(self):
|
def test_get_sender_legacy_returns_sender_in_content(self):
|
||||||
protocol = self.init_protocol()
|
protocol = self.init_protocol()
|
||||||
protocol.header = ElementTree()
|
protocol.header = ElementTree()
|
||||||
protocol.content = "<content><diaspora_handle>bob@example.com</diaspora_handle></content>"
|
protocol.content = "<content><diaspora_handle>bob@example.com</diaspora_handle></content>"
|
||||||
assert protocol.get_sender() == "bob@example.com"
|
assert protocol.get_sender_legacy() == "bob@example.com"
|
||||||
protocol.content = "<content><sender_handle>bob@example.com</sender_handle></content>"
|
protocol.content = "<content><sender_handle>bob@example.com</sender_handle></content>"
|
||||||
assert protocol.get_sender() == "bob@example.com"
|
assert protocol.get_sender_legacy() == "bob@example.com"
|
||||||
|
|
||||||
def test_get_sender_returns_none_if_no_sender_found(self):
|
def test_get_sender_legacy_returns_none_if_no_sender_found(self):
|
||||||
protocol = self.init_protocol()
|
protocol = self.init_protocol()
|
||||||
protocol.header = ElementTree()
|
protocol.header = ElementTree()
|
||||||
protocol.content = "<content><handle>bob@example.com</handle></content>"
|
protocol.content = "<content><handle>bob@example.com</handle></content>"
|
||||||
assert protocol.get_sender() == None
|
assert protocol.get_sender_legacy() is None
|
||||||
|
|
||||||
@patch.object(Protocol, "init_message")
|
@patch.object(Protocol, "init_message")
|
||||||
@patch.object(Protocol, "create_salmon_envelope")
|
@patch.object(Protocol, "create_salmon_envelope")
|
||||||
|
|
Ładowanie…
Reference in New Issue