kopia lustrzana https://gitlab.com/jaywink/federation
Add relayable signature verification for DiasporaComment
rodzic
b24a3370f2
commit
268afb950b
|
@ -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):
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
Ładowanie…
Reference in New Issue