From 6cd09cb35fcb9cd12ae63119ab8f4787947bbdd2 Mon Sep 17 00:00:00 2001 From: Ari Argoud Date: Sun, 25 Aug 2024 15:03:08 -0700 Subject: [PATCH] Implemented listening package and listen command, adjusted directing to accepts cues. --- src/keri/app/cli/commands/listen.py | 64 ++++++++ src/keri/app/directing.py | 24 ++- src/keri/app/listening.py | 235 ++++++++++++++++++++++++++++ 3 files changed, 315 insertions(+), 8 deletions(-) create mode 100644 src/keri/app/cli/commands/listen.py create mode 100644 src/keri/app/listening.py diff --git a/src/keri/app/cli/commands/listen.py b/src/keri/app/cli/commands/listen.py new file mode 100644 index 000000000..94e1631bc --- /dev/null +++ b/src/keri/app/cli/commands/listen.py @@ -0,0 +1,64 @@ +# -*- encoding: utf-8 -*- +""" +KERI +keri.kli.commands module + +""" +import argparse +import os +import os.path + +import falcon +from hio import help +from hio.core.uxd import Server, ServerDoer +from hio.help import decking + +from keri import kering +from keri.app import habbing, directing +from keri.app.cli.common import existing + +from keri.app.listening import Authenticator, IdentifiersHandler, UnlockHandler, SignHandler +logger = help.ogler.getLogger() + +parser = argparse.ArgumentParser(description='Run Unix domain sockets server listening for browser support') +parser.set_defaults(handler=lambda args: handler(args), + transferable=True) +# parser.add_argument('--name', '-n', help='keystore name and file location of KERI keystore', required=True) +parser.add_argument('--base', '-b', help='additional optional prefix to file location of KERI keystore', + required=False, default="") +# parser.add_argument('--alias', '-a', help='human readable alias for the new identifier prefix', default=None) +parser.add_argument('--passcode', '-p', help='21 character encryption passcode for keystore (is not saved)', + dest="bran", default=None) # passcode => bran + +parser.add_argument("--verbose", "-V", help="print JSON of all current events", action="store_true") + + +def loadHandlers(hby, cues): + ids = IdentifiersHandler(cues=cues, base=hby.base) + hby.exc.addHandler(ids) + unlock = UnlockHandler(cues=cues, base=hby.base) + hby.exc.addHandler(unlock) + sign = SignHandler(cues=cues, base=hby.base) + hby.exc.addHandler(sign) + +def handler(args): + """ Command line list handler + + """ + hby = existing.setupHby(name="listener", base=args.base, bran=args.bran) + hab = hby.habByName("listener") + + hbyDoer = habbing.HaberyDoer(habery=hby) # setup doer + + cues = decking.Deck() + loadHandlers(hby, cues) + + if os.path.exists("/tmp/keripy_kli.s"): + os.remove("/tmp/keripy_kli.s") + + server = Server(path="/tmp/keripy_kli.s", + bufsize=8069) + serverDoer = ServerDoer(server=server) + directant = directing.Directant(hab=hab, server=server, exchanger=hby.exc, cues=cues) + + return [directant, serverDoer, hbyDoer] \ No newline at end of file diff --git a/src/keri/app/directing.py b/src/keri/app/directing.py index 6c72fc987..3ca754d49 100644 --- a/src/keri/app/directing.py +++ b/src/keri/app/directing.py @@ -6,7 +6,10 @@ simple direct mode demo support classes """ import itertools +import json + from hio.base import doing +from hio.help import decking from .. import help from ..core import eventing, routing @@ -356,7 +359,7 @@ class Directant(doing.DoDoer): ._tock is hidden attribute for .tock property """ - def __init__(self, hab, server, verifier=None, exchanger=None, doers=None, **kwa): + def __init__(self, hab, server, verifier=None, exchanger=None, doers=None, cues=None, **kwa): """ Initialize instance. @@ -374,6 +377,8 @@ def __init__(self, hab, server, verifier=None, exchanger=None, doers=None, **kwa self.exchanger = exchanger self.server = server # use server for cx self.rants = dict() + self.cues = cues if cues is not None else decking.Deck() + doers = doers if doers is not None else [] doers.extend([doing.doify(self.serviceDo)]) super(Directant, self).__init__(doers=doers, **kwa) @@ -419,7 +424,7 @@ def serviceDo(self, tymth=None, tock=0.0, **opts): if ca not in self.rants: # create Reactant and extend doers with it rant = Reactant(tymth=self.tymth, hab=self.hab, verifier=self.verifier, - exchanger=self.exchanger, remoter=ix) + exchanger=self.exchanger, remoter=ix, cues=self.cues) self.rants[ca] = rant # add Reactant (rant) doer to running doers self.extend(doers=[rant]) # open and run rant as doer @@ -498,7 +503,7 @@ class Reactant(doing.DoDoer): """ - def __init__(self, hab, remoter, verifier=None, exchanger=None, doers=None, **kwa): + def __init__(self, hab, remoter, verifier=None, exchanger=None, doers=None, cues=None, **kwa): """ Initialize instance. @@ -518,6 +523,7 @@ def __init__(self, hab, remoter, verifier=None, exchanger=None, doers=None, **kw self.verifier = verifier self.exchanger = exchanger self.remoter = remoter # use remoter for both rx and tx + self.cues = cues if cues is not None else decking.Deck() doers = doers if doers is not None else [] doers.extend([doing.doify(self.msgDo), @@ -560,7 +566,6 @@ def wind(self, tymth): super(Reactant, self).wind(tymth) self.remoter.wind(tymth) - def msgDo(self, tymth=None, tock=0.0, **opts): """ Returns doifiable Doist compatibile generator method (doer dog) to process @@ -586,8 +591,7 @@ def msgDo(self, tymth=None, tock=0.0, **opts): logger.info("Server %s: received:\n%s\n...\n", self.hab.name, self.parser.ims[:1024]) done = yield from self.parser.parsator(local=True) # process messages continuously - return done # should nover get here except forced close - + return done # should never get here except forced close def cueDo(self, tymth=None, tock=0.0, **opts): """ @@ -616,8 +620,13 @@ def cueDo(self, tymth=None, tock=0.0, **opts): self.sendMessage(msg, label="chit or receipt or replay") yield # throttle just do one cue at a time + while self.cues: + msg = self.cues.popleft() + data = json.dumps(msg).encode("utf-8") + + self.sendMessage(data, label="exn response") + yield # throttle just do one cue at a time yield - return False # should never get here except forced close def escrowDo(self, tymth=None, tock=0.0, **opts): @@ -645,7 +654,6 @@ def escrowDo(self, tymth=None, tock=0.0, **opts): if self.tevery is not None: self.tevery.processEscrows() yield - return False # should never get here except forced close def sendMessage(self, msg, label=""): """ diff --git a/src/keri/app/listening.py b/src/keri/app/listening.py new file mode 100644 index 000000000..26b99ac56 --- /dev/null +++ b/src/keri/app/listening.py @@ -0,0 +1,235 @@ +import argparse +import os +import os.path + +import falcon +from hio import help +from hio.core.uxd import Server, ServerDoer +from hio.help import decking + +from keri import kering +from hio.help import Hict +from keri.end import ending +from keri.help import helping +from keri.app import habbing, directing +from keri.app.cli.common import existing +class SignHandler: + resource = "/sign" + + def __init__(self, cues, base): + """ Initialize peer to peer challenge response messsage """ + + self.cues = cues + self.base = base + super(SignHandler, self).__init__() + + def handle(self, serder, attachments=None): + """ Do route specific processsing of Challenge response messages + + Parameters: + serder (Serder): Serder of the exn challenge response message + attachments (list): list of tuples of pather, CESR SAD path attachments to the exn event + """ + # print(serder.ked) + payload = serder.ked['a'] + + name = payload["name"] + passcode = payload["passcode"] if "passcode" in payload else None + method = payload["method"] if "method" in payload else None + data = payload["data"] if "data" in payload else None + path = payload["path"] if "path" in payload else None + signator = payload["signator"] if "signator" in payload else None + + print(data) + try: + hby = habbing.Habery(name=name, base=self.base, bran=passcode) + for hab in hby.habs.values(): + aid = hab.pre + if hab.name == signator: + try: + auth = Authenticator(path=path, name=signator, aid=aid, method=method, hab=hab) + except Exception as err: + print(err) + headers = auth.sign() + + + hby.close() + self.cues.append(dict(status=falcon.HTTP_200, body=headers)) + except (kering.AuthError, ValueError) as e: + msg = dict(status=falcon.HTTP_400, body=str(e)) + self.cues.append(msg) +class IdentifiersHandler: + """ Handle challenge response peer to peer `exn` message """ + + resource = "/identifiers" + + def __init__(self, cues, base): + """ Initialize peer to peer challenge response messsage """ + + self.cues = cues + self.base = base + super(IdentifiersHandler, self).__init__() + + def handle(self, serder, attachments=None): + """ Do route specific processsing of Challenge response messages + + Parameters: + serder (Serder): Serder of the exn challenge response message + attachments (list): list of tuples of pather, CESR SAD path attachments to the exn event + + """ + payload = serder.ked['a'] + name = payload["name"] + passcode = payload["passcode"] if "passcode" in payload else None + + try: + hby = habbing.Habery(name=name, base=self.base, bran=passcode) + print("habs") + identifiers = [] + for hab in hby.habs.values(): + msg = dict(name=hab.name, prefix=hab.pre) + identifiers.append(msg) + + hby.close() + self.cues.append(dict(status=falcon.HTTP_200, body=identifiers)) + except (kering.AuthError, ValueError) as e: + msg = dict(status=falcon.HTTP_400, body=str(e)) + self.cues.append(msg) + + +class UnlockHandler: + """ Handle challenge response peer to peer `exn` message """ + + resource = "/unlock" + + def __init__(self, cues, base): + """ Initialize peer to peer challenge response messsage """ + + self.cues = cues + self.base = base + super(UnlockHandler, self).__init__() + + def handle(self, serder, attachments=None): + """ Do route specific processsing of Challenge response messages + + Parameters: + serder (Serder): Serder of the exn challenge response message + attachments (list): list of tuples of pather, CESR SAD path attachments to the exn event + + """ + payload = serder.ked['a'] + name = payload["name"] + passcode = payload["passcode"] if "passcode" in payload else None + + try: + hby = habbing.Habery(name=name, base=self.base, bran=passcode, free=True) + print("unlocked") + msg = dict(status=falcon.HTTP_200, body={}) + hby.close() + + except (kering.AuthError, ValueError) as e: + msg = dict(status=falcon.HTTP_400, body=str(e)) + + self.cues.append(msg) + +class Authenticator: + def __init__(self, path, name, aid, method, hab): + self.path = path + self.name = name + self.aid = aid + self.method = method + self.hab = hab + self.default_fields = ["Signify-Resource", + "@method", + "@path", + "Signify-Timestamp"] + @staticmethod + def resource(response): + headers = response.headers + if "SIGNIFY-RESOURCE" not in headers: + raise kering.AuthNError("SIGNIFY-RESOURCE not found in header.") + return headers["SIGNIFY-RESOURCE"] + + def sign(self): + headers = Hict([ + ("Content-Type", "application/json"), + ("Content-Length", "256"), + ("Connection", "close"), + ("Signify-Resource", self.aid), + ("Signify-Timestamp", helping.nowIso8601()), + ]) + if self.method == "DELETE" or self.method == "GET": + headers = Hict([ + ("Connection", "close"), + ("Signify-Resource", self.aid), + ("Signify-Timestamp", helping.nowIso8601()), + ]) + header, qsig = ending.siginput("signify", method=self.method, path=self.path, headers=headers, fields=self.default_fields, + hab=self.hab, alg="ed25519", keyid=self.aid) + headers.extend(header) + signage = ending.Signage(markers=dict(signify=qsig), indexed=False, signer=None, ordinal=None, digest=None, + kind=None) + headers.extend(ending.signature([signage])) + + return dict(headers) + + def verify(self, response): + headers = response.headers + + if "SIGNATURE-INPUT" not in headers or "SIGNATURE" not in headers: + return False + + siginput = headers["SIGNATURE-INPUT"] + if not siginput: + return False + signature = headers["SIGNATURE"] + if not signature: + return False + + inputs = ending.desiginput(siginput.encode("utf-8")) + inputs = [i for i in inputs if i.name == "signify"] + + if not inputs: + return False + + for inputage in inputs: + items = [] + for field in inputage.fields: + key = field.upper() + field = field.lower() + if key not in headers: + continue + + value = ending.normalize(headers[key]) + items.append(f'"{field}": {value}') + + values = [f"({' '.join(inputage.fields)})", f"created={inputage.created}"] + if inputage.expires is not None: + values.append(f"expires={inputage.expires}") + if inputage.nonce is not None: + values.append(f"nonce={inputage.nonce}") + if inputage.keyid is not None: + values.append(f"keyid={inputage.keyid}") + if inputage.context is not None: + values.append(f"context={inputage.context}") + if inputage.alg is not None: + values.append(f"alg={inputage.alg}") + + params = ';'.join(values) + + items.append(f'"@signature-params: {params}"') + ser = "\n".join(items).encode("utf-8") + + resource = self.resource(response) + + with habbing.openHab(name="hkapi", temp=False) as (hkHby, hkHab): + if resource not in hkHab.kevers: + raise kering.AuthNError("unknown or invalid controller") + + ckever = hkHab.kevers[resource] + signages = ending.designature(signature) + cig = signages[0].markers[inputage.name] + if not ckever.verfers[0].verify(sig=cig.raw, ser=ser): + raise kering.AuthNError(f"Signature for {inputage} invalid") + + return True \ No newline at end of file