Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: TSP message types and optionally encrypted StreamPoster #935

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions src/keri/app/forwarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
module for enveloping and forwarding KERI message
"""
import random
import pysodium
from ordered_set import OrderedSet as oset

from hio.base import doing
from hio.help import decking, ogler

from keri import kering
from keri.app import agenting
from keri.app.habbing import GroupHab
from keri.core import coring, eventing, serdering
from keri.core import coring, eventing, serdering, MtrDex, Counter, Codens
from keri.db import dbing
from keri.kering import Roles
from keri.peer import exchanging
Expand Down Expand Up @@ -243,7 +243,7 @@ class StreamPoster:

"""

def __init__(self, hby, recp, src=None, hab=None, mbx=None, topic=None, headers=None, **kwa):
def __init__(self, hby, recp, src=None, hab=None, mbx=None, topic=None, headers=None, essr=False, **kwa):
if hab is not None:
self.hab = hab
else:
Expand All @@ -257,6 +257,7 @@ def __init__(self, hby, recp, src=None, hab=None, mbx=None, topic=None, headers=
self.mbx = mbx
self.topic = topic
self.headers = headers
self.essr = essr
self.evts = decking.Deck()

def deliver(self):
Expand Down Expand Up @@ -344,11 +345,35 @@ def send(self, serder, attachment=None):

def sendDirect(self, hab, ends, msg):
for ctrl, locs in ends.items():
self.messagers.append(agenting.streamMessengerFrom(hab=hab, pre=ctrl, urls=locs, msg=msg,
ims = self._essrWrapper(hab, msg, ctrl) if self.essr else msg
self.messagers.append(agenting.streamMessengerFrom(hab=hab, pre=ctrl, urls=locs, msg=ims,
headers=self.headers))

return self.messagers

def _essrWrapper(self, hab, msg, ctrl):
ims = bytearray()

# Can be added in deliver() once mailbox support added to avoid list copy
ims.extend(coring.Tsper(tsp=coring.Tsps.SCS).qb64b)
ims.extend(self.hab.kever.prefixer.qb64b)
ims.extend(msg)

rkever = self.hby.kevers[ctrl]
pubkey = pysodium.crypto_sign_pk_to_box_pk(rkever.verfers[0].raw)
raw = pysodium.crypto_box_seal(bytes(ims), pubkey)

texter = coring.Texter(raw=raw)
diger = coring.Diger(ser=raw, code=MtrDex.Blake3_256)
essr, _ = exchanging.exchange(route='/essr/req', sender=hab.pre, diger=diger,
modifiers=dict(src=hab.pre, dest=ctrl))

ims = hab.endorse(serder=essr, pipelined=False)
ims.extend(Counter(Codens.ESSRPayloadGroup, count=1,
gvrsn=kering.Vrsn_1_0).qb64b)
ims.extend(texter.qb64b)
return ims

def createForward(self, hab, ends, serder, atc, topic):
# If we are one of the mailboxes, just store locally in mailbox
owits = oset(ends.keys())
Expand Down
111 changes: 111 additions & 0 deletions src/keri/core/coring.py
Original file line number Diff line number Diff line change
Expand Up @@ -2543,6 +2543,117 @@ def b64ToVer(b64, *, texted=False):
return Versionage(major=b64ToInt(b64[0]), minor=b64ToInt(b64[1:3]))



# Trust Spanning Protocol protocol packet (message) types
Tspage = namedtuple("Tspage", 'HOP REL SCS')

Tsps = Tspage(HOP='HOP', REL='REL', SCS='SCS')


class Tsper(Tagger):
"""
Tsper is subclass of Tagger, cryptographic material, for formatted
message types (tsps) in Base64. Leverages Tagger support compact special
fixed size primitives with non-empty soft part and empty raw part.

Tsper provides a more compact representation than would be obtained by
converting the raw ASCII representation to Base64.

Attributes:

Inherited Properties: (See Tagger)
code (str): hard part of derivation code to indicate cypher suite
hard (str): hard part of derivation code. alias for code
soft (str): soft part of derivation code fs any.
Empty when ss = 0.
both (str): hard + soft parts of full text code
size (int | None): Number of quadlets/triplets of chars/bytes including
lead bytes of variable sized material (fs = None).
Converted value of the soft part (of len ss) of full
derivation code.
Otherwise None when not variably sized (fs != None)
fullSize (int): full size of primitive
raw (bytes): crypto material only. Not derivation code or lead bytes.
qb64 (str): Base64 fully qualified with derivation code + crypto mat
qb64b (bytes): Base64 fully qualified with derivation code + crypto mat
qb2 (bytes): binary with derivation code + crypto material
transferable (bool): True means transferable derivation code False otherwise
digestive (bool): True means digest derivation code False otherwise
prefixive (bool): True means identifier prefix derivation code False otherwise
special (bool): True when soft is special raw is empty and fixed size
composable (bool): True when .qb64b and .qb2 are 24 bit aligned and round trip
tag (str): B64 primitive without prepad (strips prepad from soft)


Properties:
tsp (str): message type from Tsps of Tspage

Inherited Hidden: (See Tagger)
_code (str): value for .code property
_soft (str): soft value of full code
_raw (bytes): value for .raw property
_rawSize():
_leadSize():
_special():
_infil(): creates qb64b from .raw and .code (fully qualified Base64)
_binfil(): creates qb2 from .raw and .code (fully qualified Base2)
_exfil(): extracts .code and .raw from qb64b (fully qualified Base64)
_bexfil(): extracts .code and .raw from qb2 (fully qualified Base2)

Hidden:


Methods:

"""


def __init__(self, qb64b=None, qb64=None, qb2=None, tag='', tsp='', **kwa):
"""
Inherited Parameters: (see Tagger)
raw (bytes | bytearray | None): unqualified crypto material usable
for crypto operations.
code (str): stable (hard) part of derivation code
soft (str | bytes): soft part for special codes
rize (int | None): raw size in bytes when variable sized material not
including lead bytes if any
Otherwise None
qb64b (bytes | None): fully qualified crypto material Base64
qb64 (str | bytes | None): fully qualified crypto material Base64
qb2 (bytes | None): fully qualified crypto material Base2
strip (bool): True means strip (delete) matter from input stream
bytearray after parsing qb64b or qb2. False means do not strip
tag (str | bytes): Base64 plain. Prepad is added as needed.

Parameters:
tsp (str): message type from Tsps of Tspage

"""
if not (qb64b or qb64 or qb2):
if tsp:
tag = tsp

super(Tsper, self).__init__(qb64b=qb64b, qb64=qb64, qb2=qb2, tag=tag, **kwa)

if self.code not in (MtrDex.Tag3, ):
raise InvalidCodeError(f"Invalid code={self.code} for Tsper "
f"{self.tsp=}.")
if self.tsp not in Tsps:
raise InvalidSoftError(f"Invalid tsp={self.tsp} for Tsper.")



@property
def tsp(self):
"""Returns:
tag (str): B64 primitive without prepad (strips prepad from soft)

Alias for self.tag

"""
return self.tag


class Texter(Matter):
"""
Texter is subclass of Matter, cryptographic material, for variable length
Expand Down
78 changes: 77 additions & 1 deletion tests/app/test_forwarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

"""
import time
import falcon

from hio.base import doing, tyming
from hio.core import http

from keri import core
from keri import core, kering, help
from keri.core import coring, eventing, parsing, serdering

from keri.app import forwarding, habbing, indirecting, storing
Expand Down Expand Up @@ -83,3 +85,77 @@ def test_forward_handler():
mbx = storing.Mailboxer()
forwarder = forwarding.ForwardHandler(hby=hby, mbx=mbx)
# TODO: implement a real test here

def test_essr_stream(seeder):
with habbing.openHab(name="test", transferable=True, temp=True) as (hby, hab), \
habbing.openHab(name="test", transferable=True, temp=True) as (recpHby, recpHab):

app = falcon.App()
httpEnd = indirecting.HttpEnd(rxbs=recpHab.psr.ims)
app.add_route("/", httpEnd)
server = http.Server(port=5555, app=app)
httpServerDoer = http.ServerDoer(server=server)

kvy = eventing.Kevery(db=hab.db)
parsing.Parser().parseOne(bytearray(recpHab.makeOwnEvent(sn=0)), kvy=kvy, local=True)
kvy.processEscrows()
assert recpHab.pre in kvy.kevers

recpKvy = eventing.Kevery(db=recpHab.db)
icp = hab.makeOwnEvent(sn=0)
parsing.Parser().parseOne(bytearray(icp), kvy=recpKvy, local=True)
kvy.processEscrows()
assert hab.pre in recpKvy.kevers

msgs = bytearray()
msgs.extend(recpHab.makeEndRole(eid=recpHab.pre,
role=kering.Roles.controller,
stamp=help.nowIso8601()))

msgs.extend(recpHab.makeLocScheme(url='http://127.0.0.1:5555',
scheme=kering.Schemes.http,
stamp=help.nowIso8601()))
hab.psr.parse(ims=msgs)

postman = forwarding.StreamPoster(hby=hby, hab=hab, recp=recpHab.pre, essr=True)

exn, _ = exchanging.exchange(route="/echo", payload=dict(msg="test"), sender=hab.pre)
atc = hab.endorse(exn, last=False)
del atc[:exn.size]

postman.send(exn, atc)

doers = [httpServerDoer, doing.DoDoer(doers=postman.deliver())]
limit = 1.0
tock = 0.03125
doist = doing.Doist(tock=tock, limit=limit, doers=doers)
doist.enter()

tymer = tyming.Tymer(tymth=doist.tymen(), duration=doist.limit)

while not tymer.expired:
doist.recur()
time.sleep(doist.tock)

assert doist.limit == limit

doist.exit()

recpHby.psr.parseOne() # ims already populated from http server
exnSaid, exnSerder = next(recpHby.db.exns.getItemIter())
assert exnSerder.ked["r"] == "/essr/req"
assert exnSerder.ked["q"] == {'src': hab.pre, 'dest': recpHab.pre}

texter = recpHby.db.essrs.get(exnSaid)[0]
ims = bytearray(recpHab.decrypt(texter.raw))

tag = parsing.Parser.extract(ims, coring.Tsper)
assert tag.tsp == coring.Tsps.SCS
pre = parsing.Parser.extract(ims, coring.Prefixer)
assert pre.qb64 == hab.pre # encrypt sender

recpHby.psr.parseOne(ims=ims)
serder = recpHby.db.exns.get(exn.said)
assert serder.ked["t"] == coring.Ilks.exn
assert serder.ked["r"] == "/echo"
assert serder.ked["a"] == dict(msg="test")
Loading