plugin pubsub signing: pubsub items signature implementation:

- this is based on a protoXEP, not yet an official XEP: https://github.com/xsf/xeps/pull/1228
- XEP-0470: `set` attachment handler can now be async

rel 381
This commit is contained in:
Goffi 2022-10-28 18:47:17 +02:00
commit 4f13a79f87
3 changed files with 353 additions and 12 deletions

View file

@ -81,7 +81,7 @@ class Events:
self._a = host.plugins["XEP-0470"]
# self._i = host.plugins.get("EMAIL_INVITATION")
host.registerNamespace("events", NS_EVENTS)
self._a.registerAttachmentHandler("rsvp", NS_EVENTS, self.rsvp_get, self.rsvp_set)
self._a.register_attachment_handler("rsvp", NS_EVENTS, self.rsvp_get, self.rsvp_set)
# host.plugins["PUBSUB_INVITATION"].register(NS_EVENTS, self)
host.bridge.addMethod(
"eventsGet",
@ -982,7 +982,7 @@ class Events:
service = client.jid.userhostJID()
if node is None:
node = NS_EVENTS
await self._a.setAttachments(client, {
await self._a.set_attachements(client, {
"service": service.full(),
"node": node,
"id": item,

View file

@ -0,0 +1,338 @@
#!/usr/bin/env python3
# Libervia plugin for Pubsub Items Signature
# Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import time
from typing import Any, Dict, List, Optional
from lxml import etree
import shortuuid
from twisted.internet import defer
from twisted.words.protocols.jabber import jid, xmlstream
from twisted.words.xish import domish
from wokkel import disco, iwokkel
from wokkel import pubsub
from zope.interface import implementer
from sat.core import exceptions
from sat.core.constants import Const as C
from sat.core.core_types import SatXMPPEntity
from sat.core.i18n import _
from sat.core.log import getLogger
from sat.tools import utils
from sat.tools.common import data_format
from .plugin_xep_0373 import get_gpg_provider, VerificationFailed
log = getLogger(__name__)
IMPORT_NAME = "pubsub-signing"
PLUGIN_INFO = {
C.PI_NAME: "Pubsub Signing",
C.PI_IMPORT_NAME: IMPORT_NAME,
C.PI_TYPE: C.PLUG_TYPE_XEP,
C.PI_MODES: C.PLUG_MODE_BOTH,
C.PI_PROTOCOLS: [],
C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0373", "XEP-0470"],
C.PI_MAIN: "PubsubSigning",
C.PI_HANDLER: "yes",
C.PI_DESCRIPTION: _(
"""Pubsub Signature can be used to strongly authenticate a pubsub item"""
),
}
NS_PUBSUB_SIGNING = "urn:xmpp:pubsub-signing:0"
NS_PUBSUB_SIGNING_OPENPGP = "urn:xmpp:pubsub-signing:openpgp:0"
class PubsubSigning:
namespace = NS_PUBSUB_SIGNING
def __init__(self, host):
log.info(_("Pubsub Signing plugin initialization"))
host.registerNamespace("pubsub-signing", NS_PUBSUB_SIGNING)
self.host = host
self._p = host.plugins["XEP-0060"]
self._ox = host.plugins["XEP-0373"]
self._a = host.plugins["XEP-0470"]
self._a.register_attachment_handler(
"signature", NS_PUBSUB_SIGNING, self.signature_get, self.signature_set
)
host.trigger.add("XEP-0060_publish", self._publish_trigger)
host.bridge.addMethod(
"psSignatureCheck",
".plugin",
in_sign="sssss",
out_sign="s",
method=self._check,
async_=True,
)
def getHandler(self, client):
return PubsubSigning_Handler()
async def profileConnecting(self, client):
self.gpg_provider = get_gpg_provider(self.host, client)
def get_data_to_sign(
self,
item_elt: domish.Element,
to_jid: jid.JID,
timestamp: float,
signer: str,
) -> bytes:
"""Generate the wrapper element, normalize, serialize and return it"""
# we remove values which must not be in the serialised data
item_id = item_elt.attributes.pop("id")
item_publisher = item_elt.attributes.pop("publisher", None)
item_parent = item_elt.parent
# we need to be sure that item element namespace is right
item_elt.uri = item_elt.defaultUri = pubsub.NS_PUBSUB
sign_data_elt = domish.Element((NS_PUBSUB_SIGNING, "sign-data"))
to_elt = sign_data_elt.addElement("to")
to_elt["jid"] = to_jid.userhost()
time_elt = sign_data_elt.addElement("time")
time_elt["stamp"] = utils.xmpp_date(timestamp)
sign_data_elt.addElement("signer", content=signer)
sign_data_elt.addChild(item_elt)
# FIXME: xml_tools.domish_elt_2_et_elt must be used once implementation is
# complete. For now serialisation/deserialisation is more secure.
# et_sign_data_elt = xml_tools.domish_elt_2_et_elt(sign_data_elt, True)
et_sign_data_elt = etree.fromstring(sign_data_elt.toXml())
to_sign = etree.tostring(
et_sign_data_elt,
method="c14n2",
with_comments=False,
strip_text=True
)
# the data to sign is serialised, we cna restore original values
item_elt["id"] = item_id
if item_publisher is not None:
item_elt["publisher"] = item_publisher
item_elt.parent = item_parent
return to_sign
def _check(
self,
service: str,
node: str,
item_id: str,
signature_data_s: str,
profile_key: str,
) -> defer.Deferred:
d = defer.ensureDeferred(
self.check(
self.host.getClient(profile_key),
jid.JID(service),
node,
item_id,
data_format.deserialise(signature_data_s)
)
)
d.addCallback(data_format.serialise)
return d
async def check(
self,
client: SatXMPPEntity,
service: jid.JID,
node: str,
item_id: str,
signature_data: Dict[str, Any],
) -> Dict[str, Any]:
items, __ = await self._p.getItems(
client, service, node, item_ids=[item_id]
)
if not items != 1:
raise exceptions.NotFound(
f"target item not found for {item_id!r} at {node!r} for {service}"
)
item_elt = items[0]
timestamp = signature_data["timestamp"]
signers = signature_data["signers"]
if not signers:
raise ValueError("we must have at least one signer to check the signature")
if len(signers) > 1:
raise NotImplemented("multiple signers are not supported yet")
signer = jid.JID(signers[0])
signature = base64.b64decode(signature_data["signature"])
verification_keys = {
k for k in await self._ox.import_all_public_keys(client, signer)
if self.gpg_provider.can_sign(k)
}
signed_data = self.get_data_to_sign(item_elt, service, timestamp, signer.full())
try:
self.gpg_provider.verify_detached(signed_data, signature, verification_keys)
except VerificationFailed:
validated = False
else:
validated = True
trusts = {
k.fingerprint: (await self._ox.get_trust(client, k, signer)).value.lower()
for k in verification_keys
}
return {
"signer": signer.full(),
"validated": validated,
"trusts": trusts,
}
def signature_get(
self,
client: SatXMPPEntity,
attachments_elt: domish.Element,
data: Dict[str, Any],
) -> None:
try:
signature_elt = next(
attachments_elt.elements(NS_PUBSUB_SIGNING, "signature")
)
except StopIteration:
pass
else:
time_elts = list(signature_elt.elements(NS_PUBSUB_SIGNING, "time"))
if len(time_elts) != 1:
raise exceptions.DataError("only a single <time/> element is allowed")
try:
timestamp = utils.parse_xmpp_date(time_elts[0]["stamp"])
except (KeyError, exceptions.ParsingError):
raise exceptions.DataError(
"invalid time element: {signature_elt.toXml()}"
)
signature_data: Dict[str, Any] = {
"timestamp": timestamp,
"signers": [
str(s) for s in signature_elt.elements(NS_PUBSUB_SIGNING, "signer")
]
}
# FIXME: only OpenPGP signature is available for now, to be updated if and
# when more algorithms are available.
sign_elt = next(
signature_elt.elements(NS_PUBSUB_SIGNING_OPENPGP, "sign"),
None
)
if sign_elt is None:
log.warning(
"no known signature profile element found, ignoring signature: "
f"{signature_elt.toXml()}"
)
return
else:
signature_data["signature"] = str(sign_elt)
data["signature"] = signature_data
async def signature_set(
self,
client: SatXMPPEntity,
attachments_data: Dict[str, Any],
former_elt: Optional[domish.Element]
) -> Optional[domish.Element]:
signature_data = attachments_data["extra"].get("signature")
if signature_data is None:
return former_elt
elif signature_data:
item_elt = signature_data.get("item_elt")
service = jid.JID(attachments_data["service"])
if item_elt is None:
node = attachments_data["node"]
item_id = attachments_data["id"]
items, __ = await self._p.getItems(
client, service, node, items_ids=[item_id]
)
if not items != 1:
raise exceptions.NotFound(
f"target item not found for {item_id!r} at {node!r} for {service}"
)
item_elt = items[0]
signer = signature_data["signer"]
timestamp = time.time()
timestamp_xmpp = utils.xmpp_date(timestamp)
to_sign = self.get_data_to_sign(item_elt, service, timestamp, signer)
signature_elt = domish.Element(
(NS_PUBSUB_SIGNING, "signature"),
)
time_elt = signature_elt.addElement("time")
time_elt["stamp"] = timestamp_xmpp
signature_elt.addElement("signer", content=signer)
sign_elt = signature_elt.addElement((NS_PUBSUB_SIGNING_OPENPGP, "sign"))
signing_keys = {
k for k in self._ox.list_secret_keys(client)
if self.gpg_provider.can_sign(k.public_key)
}
# the base64 encoded signature itself
sign_elt.addContent(
base64.b64encode(
self.gpg_provider.sign_detached(to_sign, signing_keys)
).decode()
)
return signature_elt
else:
return None
async def _publish_trigger(
self,
client: SatXMPPEntity,
service: jid.JID,
node: str,
items: Optional[List[domish.Element]],
options: Optional[dict],
sender: jid.JID,
extra: Dict[str, Any]
) -> bool:
if not items or not extra.get("signed"):
return True
for item_elt in items:
# we need an ID to find corresponding attachment node, and so to sign an item
if not item_elt.hasAttribute("id"):
item_elt["id"] = shortuuid.uuid()
await self._a.set_attachements(
client,
{
"service": service.full(),
"node": node,
"id": item_elt["id"],
"extra": {
"signature": {
"item_elt": item_elt,
"signer": sender.userhost(),
}
}
}
)
return True
@implementer(iwokkel.IDisco)
class PubsubSigning_Handler(xmlstream.XMPPHandler):
def getDiscoInfo(self, requestor, service, nodeIdentifier=""):
return [disco.DiscoFeature(NS_PUBSUB_SIGNING)]
def getDiscoItems(self, requestor, service, nodeIdentifier=""):
return []

View file

@ -30,7 +30,7 @@ from sat.core.log import getLogger
from sat.core.core_types import SatXMPPEntity
from sat.core import exceptions
from sat.tools.common import uri, data_format, date_utils
from sat.tools.utils import xmpp_date
from sat.tools.utils import asDeferred, xmpp_date
log = getLogger(__name__)
@ -63,10 +63,10 @@ class PubsubAttachments:
self._p = host.plugins["XEP-0060"]
self.handlers: Dict[Tuple[str, str], dict[str, Any]] = {}
host.trigger.add("XEP-0277_send", self.onMBSend)
self.registerAttachmentHandler(
self.register_attachment_handler(
"noticed", NS_PUBSUB_ATTACHMENTS, self.noticedGet, self.noticedSet
)
self.registerAttachmentHandler(
self.register_attachment_handler(
"reactions", NS_PUBSUB_ATTACHMENTS, self.reactionsGet, self.reactionsSet
)
host.bridge.addMethod(
@ -89,7 +89,7 @@ class PubsubAttachments:
def getHandler(self, client):
return PubsubAttachments_Handler()
def registerAttachmentHandler(
def register_attachment_handler(
self,
name: str,
namespace: str,
@ -113,6 +113,7 @@ class PubsubAttachments:
it will be called with (client, data, former_elt of None if there was no
former element). When suitable, ``operation`` should be used to check if we
request an ``update`` or a ``replace``.
The callback can be either a blocking method, a Deferred or a coroutine
"""
key = (name, namespace)
if key in self.handlers:
@ -299,7 +300,7 @@ class PubsubAttachments:
@return: A tuple with:
- the list of attachments data, one item per found sender. The attachments
data are dict containing attachment, no ``extra`` field is used here
(contrarily to attachments data used with ``setAttachments``).
(contrarily to attachments data used with ``set_attachements``).
- metadata returned by the call to ``getItems``
"""
if extra is None:
@ -320,9 +321,9 @@ class PubsubAttachments:
) -> None:
client = self.host.getClient(profile_key)
attachments = data_format.deserialise(attachments_s) or {}
return defer.ensureDeferred(self.setAttachments(client, attachments))
return defer.ensureDeferred(self.set_attachements(client, attachments))
def applySetHandler(
async def apply_set_handler(
self,
client: SatXMPPEntity,
attachments_data: dict,
@ -375,7 +376,9 @@ class PubsubAttachments:
former_elt = next(attachments_elt.elements(namespace, name))
except StopIteration:
former_elt = None
new_elt = handler["set"](client, attachments_data, former_elt)
new_elt = await asDeferred(
handler["set"], client, attachments_data, former_elt
)
if new_elt != former_elt:
if former_elt is not None:
attachments_elt.children.remove(former_elt)
@ -383,7 +386,7 @@ class PubsubAttachments:
attachments_elt.addChild(new_elt)
return item_elt
async def setAttachments(
async def set_attachements(
self,
client: SatXMPPEntity,
attachments_data: Dict[str, Any]
@ -425,7 +428,7 @@ class PubsubAttachments:
else:
item_elt = items[0]
item_elt = self.applySetHandler(
item_elt = await self.apply_set_handler(
client,
attachments_data,
item_elt=item_elt,