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:
parent
6255985262
commit
4f13a79f87
3 changed files with 353 additions and 12 deletions
|
|
@ -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,
|
||||
|
|
|
|||
338
sat/plugins/plugin_sec_pubsub_signing.py
Normal file
338
sat/plugins/plugin_sec_pubsub_signing.py
Normal 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 []
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue