Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_sec_pubsub_signing.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/plugins/plugin_sec_pubsub_signing.py@524856bd7b19 |
children | 0d7bb4df2343 |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # Libervia plugin for Pubsub Items Signature | |
4 # Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 import base64 | |
20 import time | |
21 from typing import Any, Dict, List, Optional | |
22 | |
23 from lxml import etree | |
24 import shortuuid | |
25 from twisted.internet import defer | |
26 from twisted.words.protocols.jabber import jid, xmlstream | |
27 from twisted.words.xish import domish | |
28 from wokkel import disco, iwokkel | |
29 from wokkel import pubsub | |
30 from zope.interface import implementer | |
31 | |
32 from libervia.backend.core import exceptions | |
33 from libervia.backend.core.constants import Const as C | |
34 from libervia.backend.core.core_types import SatXMPPEntity | |
35 from libervia.backend.core.i18n import _ | |
36 from libervia.backend.core.log import getLogger | |
37 from libervia.backend.tools import utils | |
38 from libervia.backend.tools.common import data_format | |
39 | |
40 from .plugin_xep_0373 import VerificationFailed | |
41 | |
42 | |
43 log = getLogger(__name__) | |
44 | |
45 IMPORT_NAME = "pubsub-signing" | |
46 | |
47 PLUGIN_INFO = { | |
48 C.PI_NAME: "Pubsub Signing", | |
49 C.PI_IMPORT_NAME: IMPORT_NAME, | |
50 C.PI_TYPE: C.PLUG_TYPE_XEP, | |
51 C.PI_MODES: C.PLUG_MODE_BOTH, | |
52 C.PI_PROTOCOLS: [], | |
53 C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0373", "XEP-0470"], | |
54 C.PI_MAIN: "PubsubSigning", | |
55 C.PI_HANDLER: "yes", | |
56 C.PI_DESCRIPTION: _( | |
57 """Pubsub Signature can be used to strongly authenticate a pubsub item""" | |
58 ), | |
59 } | |
60 NS_PUBSUB_SIGNING = "urn:xmpp:pubsub-signing:0" | |
61 NS_PUBSUB_SIGNING_OPENPGP = "urn:xmpp:pubsub-signing:openpgp:0" | |
62 | |
63 | |
64 class PubsubSigning: | |
65 namespace = NS_PUBSUB_SIGNING | |
66 | |
67 def __init__(self, host): | |
68 log.info(_("Pubsub Signing plugin initialization")) | |
69 host.register_namespace("pubsub-signing", NS_PUBSUB_SIGNING) | |
70 self.host = host | |
71 self._p = host.plugins["XEP-0060"] | |
72 self._ox = host.plugins["XEP-0373"] | |
73 self._a = host.plugins["XEP-0470"] | |
74 self._a.register_attachment_handler( | |
75 "signature", NS_PUBSUB_SIGNING, self.signature_get, self.signature_set | |
76 ) | |
77 host.trigger.add("XEP-0060_publish", self._publish_trigger) | |
78 host.bridge.add_method( | |
79 "ps_signature_check", | |
80 ".plugin", | |
81 in_sign="sssss", | |
82 out_sign="s", | |
83 method=self._check, | |
84 async_=True, | |
85 ) | |
86 | |
87 def get_handler(self, client): | |
88 return PubsubSigning_Handler() | |
89 | |
90 def get_data_to_sign( | |
91 self, | |
92 item_elt: domish.Element, | |
93 to_jid: jid.JID, | |
94 timestamp: float, | |
95 signer: str, | |
96 ) -> bytes: | |
97 """Generate the wrapper element, normalize, serialize and return it""" | |
98 # we remove values which must not be in the serialised data | |
99 item_id = item_elt.attributes.pop("id") | |
100 item_publisher = item_elt.attributes.pop("publisher", None) | |
101 item_parent = item_elt.parent | |
102 | |
103 # we need to be sure that item element namespace is right | |
104 item_elt.uri = item_elt.defaultUri = pubsub.NS_PUBSUB | |
105 | |
106 sign_data_elt = domish.Element((NS_PUBSUB_SIGNING, "sign-data")) | |
107 to_elt = sign_data_elt.addElement("to") | |
108 to_elt["jid"] = to_jid.userhost() | |
109 time_elt = sign_data_elt.addElement("time") | |
110 time_elt["stamp"] = utils.xmpp_date(timestamp) | |
111 sign_data_elt.addElement("signer", content=signer) | |
112 sign_data_elt.addChild(item_elt) | |
113 # FIXME: xml_tools.domish_elt_2_et_elt must be used once implementation is | |
114 # complete. For now serialisation/deserialisation is more secure. | |
115 # et_sign_data_elt = xml_tools.domish_elt_2_et_elt(sign_data_elt, True) | |
116 et_sign_data_elt = etree.fromstring(sign_data_elt.toXml()) | |
117 to_sign = etree.tostring( | |
118 et_sign_data_elt, | |
119 method="c14n2", | |
120 with_comments=False, | |
121 strip_text=True | |
122 ) | |
123 # the data to sign is serialised, we cna restore original values | |
124 item_elt["id"] = item_id | |
125 if item_publisher is not None: | |
126 item_elt["publisher"] = item_publisher | |
127 item_elt.parent = item_parent | |
128 return to_sign | |
129 | |
130 def _check( | |
131 self, | |
132 service: str, | |
133 node: str, | |
134 item_id: str, | |
135 signature_data_s: str, | |
136 profile_key: str, | |
137 ) -> defer.Deferred: | |
138 d = defer.ensureDeferred( | |
139 self.check( | |
140 self.host.get_client(profile_key), | |
141 jid.JID(service), | |
142 node, | |
143 item_id, | |
144 data_format.deserialise(signature_data_s) | |
145 ) | |
146 ) | |
147 d.addCallback(data_format.serialise) | |
148 return d | |
149 | |
150 async def check( | |
151 self, | |
152 client: SatXMPPEntity, | |
153 service: jid.JID, | |
154 node: str, | |
155 item_id: str, | |
156 signature_data: Dict[str, Any], | |
157 ) -> Dict[str, Any]: | |
158 items, __ = await self._p.get_items( | |
159 client, service, node, item_ids=[item_id] | |
160 ) | |
161 if not items != 1: | |
162 raise exceptions.NotFound( | |
163 f"target item not found for {item_id!r} at {node!r} for {service}" | |
164 ) | |
165 item_elt = items[0] | |
166 timestamp = signature_data["timestamp"] | |
167 signers = signature_data["signers"] | |
168 if not signers: | |
169 raise ValueError("we must have at least one signer to check the signature") | |
170 if len(signers) > 1: | |
171 raise NotImplemented("multiple signers are not supported yet") | |
172 signer = jid.JID(signers[0]) | |
173 signature = base64.b64decode(signature_data["signature"]) | |
174 verification_keys = { | |
175 k for k in await self._ox.import_all_public_keys(client, signer) | |
176 if client.gpg_provider.can_sign(k) | |
177 } | |
178 signed_data = self.get_data_to_sign(item_elt, service, timestamp, signer.full()) | |
179 try: | |
180 client.gpg_provider.verify_detached(signed_data, signature, verification_keys) | |
181 except VerificationFailed: | |
182 validated = False | |
183 else: | |
184 validated = True | |
185 | |
186 trusts = { | |
187 k.fingerprint: (await self._ox.get_trust(client, k, signer)).value.lower() | |
188 for k in verification_keys | |
189 } | |
190 return { | |
191 "signer": signer.full(), | |
192 "validated": validated, | |
193 "trusts": trusts, | |
194 } | |
195 | |
196 def signature_get( | |
197 self, | |
198 client: SatXMPPEntity, | |
199 attachments_elt: domish.Element, | |
200 data: Dict[str, Any], | |
201 ) -> None: | |
202 try: | |
203 signature_elt = next( | |
204 attachments_elt.elements(NS_PUBSUB_SIGNING, "signature") | |
205 ) | |
206 except StopIteration: | |
207 pass | |
208 else: | |
209 time_elts = list(signature_elt.elements(NS_PUBSUB_SIGNING, "time")) | |
210 if len(time_elts) != 1: | |
211 raise exceptions.DataError("only a single <time/> element is allowed") | |
212 try: | |
213 timestamp = utils.parse_xmpp_date(time_elts[0]["stamp"]) | |
214 except (KeyError, exceptions.ParsingError): | |
215 raise exceptions.DataError( | |
216 "invalid time element: {signature_elt.toXml()}" | |
217 ) | |
218 | |
219 signature_data: Dict[str, Any] = { | |
220 "timestamp": timestamp, | |
221 "signers": [ | |
222 str(s) for s in signature_elt.elements(NS_PUBSUB_SIGNING, "signer") | |
223 ] | |
224 } | |
225 # FIXME: only OpenPGP signature is available for now, to be updated if and | |
226 # when more algorithms are available. | |
227 sign_elt = next( | |
228 signature_elt.elements(NS_PUBSUB_SIGNING_OPENPGP, "sign"), | |
229 None | |
230 ) | |
231 if sign_elt is None: | |
232 log.warning( | |
233 "no known signature profile element found, ignoring signature: " | |
234 f"{signature_elt.toXml()}" | |
235 ) | |
236 return | |
237 else: | |
238 signature_data["signature"] = str(sign_elt) | |
239 | |
240 data["signature"] = signature_data | |
241 | |
242 async def signature_set( | |
243 self, | |
244 client: SatXMPPEntity, | |
245 attachments_data: Dict[str, Any], | |
246 former_elt: Optional[domish.Element] | |
247 ) -> Optional[domish.Element]: | |
248 signature_data = attachments_data["extra"].get("signature") | |
249 if signature_data is None: | |
250 return former_elt | |
251 elif signature_data: | |
252 item_elt = signature_data.get("item_elt") | |
253 service = jid.JID(attachments_data["service"]) | |
254 if item_elt is None: | |
255 node = attachments_data["node"] | |
256 item_id = attachments_data["id"] | |
257 items, __ = await self._p.get_items( | |
258 client, service, node, item_ids=[item_id] | |
259 ) | |
260 if not items != 1: | |
261 raise exceptions.NotFound( | |
262 f"target item not found for {item_id!r} at {node!r} for {service}" | |
263 ) | |
264 item_elt = items[0] | |
265 | |
266 signer = signature_data.get("signer") or client.jid.userhost() | |
267 timestamp = time.time() | |
268 timestamp_xmpp = utils.xmpp_date(timestamp) | |
269 to_sign = self.get_data_to_sign(item_elt, service, timestamp, signer) | |
270 | |
271 signature_elt = domish.Element( | |
272 (NS_PUBSUB_SIGNING, "signature"), | |
273 ) | |
274 time_elt = signature_elt.addElement("time") | |
275 time_elt["stamp"] = timestamp_xmpp | |
276 signature_elt.addElement("signer", content=signer) | |
277 | |
278 sign_elt = signature_elt.addElement((NS_PUBSUB_SIGNING_OPENPGP, "sign")) | |
279 signing_keys = { | |
280 k for k in self._ox.list_secret_keys(client) | |
281 if client.gpg_provider.can_sign(k.public_key) | |
282 } | |
283 # the base64 encoded signature itself | |
284 sign_elt.addContent( | |
285 base64.b64encode( | |
286 client.gpg_provider.sign_detached(to_sign, signing_keys) | |
287 ).decode() | |
288 ) | |
289 return signature_elt | |
290 else: | |
291 return None | |
292 | |
293 async def _publish_trigger( | |
294 self, | |
295 client: SatXMPPEntity, | |
296 service: jid.JID, | |
297 node: str, | |
298 items: Optional[List[domish.Element]], | |
299 options: Optional[dict], | |
300 sender: jid.JID, | |
301 extra: Dict[str, Any] | |
302 ) -> bool: | |
303 if not items or not extra.get("signed"): | |
304 return True | |
305 | |
306 for item_elt in items: | |
307 # we need an ID to find corresponding attachment node, and so to sign an item | |
308 if not item_elt.hasAttribute("id"): | |
309 item_elt["id"] = shortuuid.uuid() | |
310 await self._a.set_attachements( | |
311 client, | |
312 { | |
313 "service": service.full(), | |
314 "node": node, | |
315 "id": item_elt["id"], | |
316 "extra": { | |
317 "signature": { | |
318 "item_elt": item_elt, | |
319 "signer": sender.userhost(), | |
320 } | |
321 } | |
322 } | |
323 ) | |
324 | |
325 return True | |
326 | |
327 | |
328 @implementer(iwokkel.IDisco) | |
329 class PubsubSigning_Handler(xmlstream.XMPPHandler): | |
330 | |
331 def getDiscoInfo(self, requestor, service, nodeIdentifier=""): | |
332 return [disco.DiscoFeature(NS_PUBSUB_SIGNING)] | |
333 | |
334 def getDiscoItems(self, requestor, service, nodeIdentifier=""): | |
335 return [] |