Add relayable signature verification for DiasporaComment

merge-requests/130/head
Jason Robinson 2017-04-29 22:07:28 +03:00
rodzic b24a3370f2
commit 268afb950b
6 zmienionych plików z 97 dodań i 9 usunięć

Wyświetl plik

@ -15,6 +15,7 @@ class BaseEntity(object):
_children = []
_allowed_children = ()
_source_protocol = ""
_source_object = None
def __init__(self, *args, **kwargs):
self._required = []
@ -33,6 +34,7 @@ class BaseEntity(object):
2) Make sure all attrs in required have a non-empty value
3) Loop through attributes and call their `validate_<attr>` methods, if any.
4) Validate allowed children
5) Validate signatures
"""
attributes = []
validates = []
@ -48,6 +50,7 @@ class BaseEntity(object):
self._validate_required(attributes)
self._validate_attributes(validates)
self._validate_children()
self._validate_signatures()
def _validate_required(self, attributes):
"""Ensure required attributes are present."""
@ -82,6 +85,11 @@ class BaseEntity(object):
)
)
def _validate_signatures(self):
"""Override in subclasses where necessary"""
pass
class GUIDMixin(BaseEntity):
guid = ""
@ -164,6 +172,7 @@ class SignedMixin(BaseEntity):
This will be needed to for example when relaying content.
"""
_sender_key = ""
signature = ""
def __init__(self, *args, **kwargs):

Wyświetl plik

@ -3,6 +3,8 @@ from lxml import etree
from federation.entities.base import Comment, Post, Reaction, Relationship, Profile, Retraction
from federation.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes
from federation.exceptions import SignatureVerificationError
from federation.protocols.diaspora.signatures import verify_relayable_signature
from federation.utils.diaspora import retrieve_and_parse_profile
@ -39,6 +41,13 @@ class DiasporaComment(DiasporaEntityMixin, Comment):
])
return element
def _validate_signatures(self):
super()._validate_signatures()
if not self._sender_key:
raise SignatureVerificationError("Cannot verify entity signature - no sender key available")
if not verify_relayable_signature(self._sender_key, self._source_object, self.signature):
raise SignatureVerificationError("Signature verification failed.")
class DiasporaPost(DiasporaEntityMixin, Post):
"""Diaspora post, ie status message."""

Wyświetl plik

@ -4,10 +4,10 @@ from datetime import datetime
from lxml import etree
from federation.entities.base import Image, Relationship, Post, Reaction, Comment, Profile, Retraction
from federation.entities.base import Image, Relationship, Post, Reaction, Comment, Profile, Retraction, SignedMixin
from federation.entities.diaspora.entities import (
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, DiasporaProfile, DiasporaRetraction)
from federation.utils.diaspora import retrieve_and_parse_profile
logger = logging.getLogger("federation")
@ -43,7 +43,7 @@ def xml_children_as_dict(node):
return dict((e.tag, e.text) for e in node)
def element_to_objects(tree):
def element_to_objects(tree, sender_key_fetcher=None):
"""Transform an Element tree to a list of entities recursively.
Possible child entities are added to each entity `_children` list.
@ -62,6 +62,18 @@ def element_to_objects(tree):
if hasattr(cls, "fill_extra_attributes"):
transformed = cls.fill_extra_attributes(transformed)
entity = cls(**transformed)
# Add protocol name
entity._source_protocol = "diaspora"
# Save element object to entity for possible later use
entity._source_object = element
# If signable, fetch sender key
if issubclass(cls, SignedMixin):
if sender_key_fetcher:
entity._sender_key = sender_key_fetcher(entity.handle)
else:
profile = retrieve_and_parse_profile(entity.handle)
if profile:
entity._sender_key = profile.public_key
try:
entity.validate()
except ValueError as ex:
@ -72,8 +84,6 @@ def element_to_objects(tree):
continue
# Do child elements
entity._children = element_to_objects(element)
# Add protocol name
entity._source_protocol = "diaspora"
# Add to entities list
entities.append(entity)
if cls == DiasporaRequest:
@ -84,18 +94,20 @@ def element_to_objects(tree):
return entities
def message_to_objects(message):
def message_to_objects(message, sender_key_fetcher=None):
"""Takes in a message extracted by a protocol and maps it to entities.
:param message: XML payload
:type message: str
:param sender_key_fetcher: Function to fetch sender public key. If not given, key will always be fetched
over network
:returns: list of entities
"""
doc = etree.fromstring(message)
if doc[0].tag == "post":
# Skip the top <post> element if it exists
doc = doc[0]
entities = element_to_objects(doc)
entities = element_to_objects(doc, sender_key_fetcher)
return entities

Wyświetl plik

@ -16,10 +16,29 @@ Example Comment payload
</XML>
TODO:
* Add method to verify author signature
* Add method to create author signature
https://diaspora.github.io/diaspora_federation/federation/relayable.html
parent_author_signature part can be skipped as per discussion with Diaspora protcol team.
"""
from base64 import urlsafe_b64decode
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
def verify_relayable_signature(public_key, doc, signature):
"""
Verify the signed XML elements to have confidence that the claimed
author did actually generate this message.
"""
props = []
for child in doc:
if child.tag not in ["author_signature", "parent_author_signature"]:
props.append(child.text)
content = ";".join(props)
sig_hash = SHA256.new(content.encode("ascii"))
cipher = PKCS1_v1_5.new(RSA.importKey(public_key))
return cipher.verify(sig_hash, urlsafe_b64decode(signature))

Wyświetl plik

@ -73,7 +73,8 @@ class TestDiasporaEntityMappersReceive(object):
assert photo.public == False
assert photo.created_at == datetime(2011, 7, 20, 1, 36, 7)
def test_message_to_objects_comment(self):
@patch("federation.entities.diaspora.mappers.DiasporaComment._validate_signatures")
def test_message_to_objects_comment(self, mock_validate):
entities = message_to_objects(DIASPORA_POST_COMMENT)
assert len(entities) == 1
comment = entities[0]
@ -85,6 +86,7 @@ class TestDiasporaEntityMappersReceive(object):
assert comment.participation == "comment"
assert comment.raw_content == "((text))"
assert comment.signature == "((signature))"
mock_validate.assert_called_once_with()
def test_message_to_objects_like(self):
entities = message_to_objects(DIASPORA_POST_LIKE)

Wyświetl plik

@ -0,0 +1,37 @@
from lxml import etree
from federation.protocols.diaspora.signatures import verify_relayable_signature
xml = "<XML><post><comment><guid>0dd40d800db1013514416c626dd55703</guid><parent_guid>69ab2b83-aa69-4456-ad0a-dd669" \
"7f54714</parent_guid><text>Woop Woop</text><diaspora_handle>jaywink@iliketoast.net</diaspora_handle>" \
"<author_signature>A/vVRxM3V1ceEH1JrnPOaIZGM3gMjw/fnT9TgUh3poI4q9eH95AIoig+3eTA8XFuGvuo0tivxci4e0NJ1VLVkl/aqp8" \
"rvBNrRI1RQkn2WVF6zk15Gq6KSia/wyzyiJHGxNGM8oFY4qPfNp6K+8ydUti22J11tVBEvQn+7FPAoloF2Xz1waK48ZZCFs8Rxzj+4jlz1Pmu" \
"XCnTj7v7GYS1Rb6sdFz4nBSuVk5X8tGOSXIRYxPgmtsDRMRrvDeEK+v3OY6VnT8dLTckS0qCwTRUULub1CGwkz/2mReZk/M1W4EbUnugF5pts" \
"lmFqYDYJZM8PA/g89EKVpkx2gaFbsC4KXocWnxHNiue18rrFQ5hMnDuDRiRybLnQkxXbE/HDuLdnognt2S5wRshPoZmhe95v3qq/5nH/GX1D7" \
"VmxEEIG9fX+XX+Vh9kzO9bLbwoJZwm50zXxCvrLlye/2JU5Vd2Hbm4aMuAyRAZiLS/EQcBlsts4DaFu4txe60HbXSh6nqNofGkusuzZnCd0VO" \
"bOpXizrI8xNQzZpjJEB5QqE2gbCC2YZNdOS0eBGXw42dAXa/QV3jZXGES7DdQlqPqqT3YjcMFLiRrWQR8cl4hJIBRpV5piGyLmMMKYrWu7hQS" \
"rdRAEL3K6mNZZU6/yoG879LjtQbVwaFGPeT29B4zBE97FIo=</author_signature><parent_author_signature/></comment>" \
"</post></XML>"
signature = "A/vVRxM3V1ceEH1JrnPOaIZGM3gMjw/fnT9TgUh3poI4q9eH95AIoig+3eTA8XFuGvuo0tivxci4e0NJ1VLVkl/aqp8rvBNrRI1RQk" \
"n2WVF6zk15Gq6KSia/wyzyiJHGxNGM8oFY4qPfNp6K+8ydUti22J11tVBEvQn+7FPAoloF2Xz1waK48ZZCFs8Rxzj+4jlz1PmuXCnT" \
"j7v7GYS1Rb6sdFz4nBSuVk5X8tGOSXIRYxPgmtsDRMRrvDeEK+v3OY6VnT8dLTckS0qCwTRUULub1CGwkz/2mReZk/M1W4EbUnugF5" \
"ptslmFqYDYJZM8PA/g89EKVpkx2gaFbsC4KXocWnxHNiue18rrFQ5hMnDuDRiRybLnQkxXbE/HDuLdnognt2S5wRshPoZmhe95v3qq" \
"/5nH/GX1D7VmxEEIG9fX+XX+Vh9kzO9bLbwoJZwm50zXxCvrLlye/2JU5Vd2Hbm4aMuAyRAZiLS/EQcBlsts4DaFu4txe60HbXSh6n" \
"qNofGkusuzZnCd0VObOpXizrI8xNQzZpjJEB5QqE2gbCC2YZNdOS0eBGXw42dAXa/QV3jZXGES7DdQlqPqqT3YjcMFLiRrWQR8cl4h" \
"JIBRpV5piGyLmMMKYrWu7hQSrdRAEL3K6mNZZU6/yoG879LjtQbVwaFGPeT29B4zBE97FIo="
pubkey = "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuCfU1G5X+3O6vPdSz6QY\nSFbgdbv3KPv" \
"xHi8tRmlyOLdLt5i1eqsy2WCW1iYNijiCL7OfbrvymBQxe3GA9S64\nVuavwzQ8nO7nzpNMqxY5tBXsBM1lECCHDOvm5dzINXWT9Sg7P1" \
"8iIxE/2wQEgMUL\nAeVbJtAriXM4zydL7c91agFMJu1aHp0lxzoH8I13xzUetGMutR1tbcfWvoQvPAoU\n89uAz5j/DFMhWrkVEKGeWt1" \
"YtHMmJqpYqR6961GDlwRuUsOBsLgLLVohzlBsTBSn\n3580o2E6G3DEaX0Az9WB9ylhNeV/L/PP3c5htpEyoPZSy1pgtut6TRYQwC8wns" \
"qO\nbVIbFBkrKoaRDyVCnpMuKdDNLZqOOfhzas+SWRAby6D8VsXpPi/DpeS9XkX0o/uH\nJ9N49GuYMSUGC8gKtaddD13pUqS/9rpSvLD" \
"rrDQe5Lhuyusgd28wgEAPCTmM3pEt\nQnlxEeEmFMIn3OBLbEDw5TFE7iED0z7a4dAkqqz8KCGEt12e1Kz7ujuOVMxJxzk6\nNtwt40Sq" \
"EOPcdsGHAA+hqzJnXUihXfmtmFkropaCxM2f+Ha0bOQdDDui5crcV3sX\njShmcqN6YqFzmoPK0XM9P1qC+lfL2Mz6bHC5p9M8/FtcM46" \
"hCj1TF/tl8zaZxtHP\nOrMuFJy4j4yAsyVy3ddO69ECAwEAAQ==\n-----END PUBLIC KEY-----\n"
def test_verify_relayable_signature():
doc = etree.XML(xml)
root = doc.find(".//comment")
assert verify_relayable_signature(pubkey, root, signature)