Add support for new style Diaspora Salmon magic envelope in public payloads

Closes #75
merge-requests/130/head
Jason Robinson 2017-05-16 22:52:29 +03:00
rodzic 064d0fa366
commit 1a5cb4d163
6 zmienionych plików z 59 dodań i 34 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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