From 44a1fa763e0da5cb577464436feefa4cee2def0b Mon Sep 17 00:00:00 2001 From: Philip Feairheller Date: Sun, 19 Nov 2023 15:09:13 -0800 Subject: [PATCH] Performance improvement in streaming CESR over HTTP. (#605) * First attempt at an ESSR packet. Signed-off-by: pfeairheller * Test command Signed-off-by: pfeairheller * Essr support being added to kli and Hab Signed-off-by: pfeairheller * Update XBRL attestation vLEI script to use new kli commands for credential creation and granting. Signed-off-by: pfeairheller * New class StreamPoster and PUT endpoint for HttpEnd to allow for streaming many messages over HTTP with significantly reduced overhead. All the IPEX commands have been updated to use the new class for sending events to all endpoint types. Signed-off-by: pfeairheller * * Add the ability to add headers to any request sent by StreamPoster * Remove unused "share oobi" endpoint. * Update shareArtifacts (credentials) to use a StreamPoster instead of Poster. Signed-off-by: pfeairheller * Remove print statement Signed-off-by: pfeairheller * Moving command to new repo. Signed-off-by: pfeairheller --------- Signed-off-by: pfeairheller --- scripts/demo/basic/essr.sh | 19 ++ scripts/demo/vLEI/issue-xbrl-attestation.sh | 50 ++++-- src/keri/app/agenting.py | 181 +++++++++++++++++++- src/keri/app/cli/commands/ends/add.py | 2 +- src/keri/app/cli/commands/ipex/admit.py | 30 ++-- src/keri/app/cli/commands/ipex/grant.py | 39 +++-- src/keri/app/cli/commands/ipex/list.py | 2 +- src/keri/app/cli/commands/ipex/spurn.py | 28 +-- src/keri/app/cli/commands/vc/present.py | 131 -------------- src/keri/app/forwarding.py | 158 ++++++++++++++++- src/keri/app/habbing.py | 28 ++- src/keri/app/indirecting.py | 71 ++++++-- src/keri/app/keeping.py | 51 ++++++ src/keri/app/oobiing.py | 73 +------- src/keri/core/eventing.py | 6 +- src/keri/core/parsing.py | 3 + src/keri/vc/protocoling.py | 29 ++-- src/keri/vdr/credentialing.py | 33 ++-- src/keri/vdr/eventing.py | 4 +- src/keri/vdr/verifying.py | 2 +- tests/app/test_oobiing.py | 29 +--- tests/end/test_ending.py | 7 +- tests/vc/test_protocoling.py | 4 +- 23 files changed, 618 insertions(+), 362 deletions(-) create mode 100755 scripts/demo/basic/essr.sh delete mode 100644 src/keri/app/cli/commands/vc/present.py diff --git a/scripts/demo/basic/essr.sh b/scripts/demo/basic/essr.sh new file mode 100755 index 000000000..e3a5cb957 --- /dev/null +++ b/scripts/demo/basic/essr.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +#kli init --name sender --salt 0ACDEyMzQ1Njc4OWxtbm9aBc --nopasscode --config-dir ${KERI_SCRIPT_DIR} --config-file demo-witness-oobis +#kli incept --name sender --alias sender --file ${KERI_DEMO_SCRIPT_DIR}/data/gleif-sample.json +# +#kli init --name recipient --salt 0ACDEyMzQ1Njc4OWxtbm9qWc --nopasscode --config-dir ${KERI_SCRIPT_DIR} --config-file demo-witness-oobis +#kli incept --name recipient --alias recipient --file ${KERI_DEMO_SCRIPT_DIR}/data/gleif-sample.json +# +#kli oobi resolve --name sender --oobi-alias recipient --oobi http://127.0.0.1:5642/oobi/EFXJsTFSo10FAZGR-8_Uw1DlhU8nuRFOAN9Z8ajJ56ci/witness +#kli oobi resolve --name recipient --oobi-alias sender --oobi http://127.0.0.1:5642/oobi/EJf7MfzNmehwY5310MUWXPSxhAA_3ifPW2bdsjwqnvae/witness +# +#kli vc registry incept --name sender --alias sender --registry-name vLEI +# +#kli vc create --name sender --alias sender --registry-name vLEI --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k --data @${KERI_DEMO_SCRIPT_DIR}/data/credential-data.json +#SAID=$(kli vc list --name sender --alias sender --issued --said --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao) +# +#kli ipex grant --name sender --alias sender --said "${SAID}" --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k + +kli essr send --name sender --alias sender --recipient recipient \ No newline at end of file diff --git a/scripts/demo/vLEI/issue-xbrl-attestation.sh b/scripts/demo/vLEI/issue-xbrl-attestation.sh index 628e5828f..8687de97b 100755 --- a/scripts/demo/vLEI/issue-xbrl-attestation.sh +++ b/scripts/demo/vLEI/issue-xbrl-attestation.sh @@ -52,42 +52,66 @@ kli vc registry incept --name legal-entity --alias legal-entity --registry-name kli vc registry incept --name person --passcode DoB26Fj4x9LboAFWJra17O --alias person --registry-name vLEI-person # Issue QVI credential vLEI from GLEIF External to QVI -kli vc issue --name external --alias external --registry-name vLEI-external --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao --recipient EHMnCf8_nIemuPx-cUHaDQq8zSnQIFAurdEpwHpNbnvX --data @${KERI_DEMO_SCRIPT_DIR}/data/qvi-data.json -kli vc list --name qvi --alias qvi --poll +kli vc create --name external --alias external --registry-name vLEI-external --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao --recipient EHMnCf8_nIemuPx-cUHaDQq8zSnQIFAurdEpwHpNbnvX --data @${KERI_DEMO_SCRIPT_DIR}/data/qvi-data.json +SAID=$(kli vc list --name external --alias external --issued --said --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao) +kli ipex grant --name external --alias external --said "${SAID}" --recipient EHMnCf8_nIemuPx-cUHaDQq8zSnQIFAurdEpwHpNbnvX +GRANT=$(kli ipex list --name qvi --alias qvi --poll --said) +kli ipex admit --name qvi --alias qvi --said "${GRANT}" +kli vc list --name qvi --alias qvi # Issue LE credential from QVI to Legal Entity - have to create the edges first QVI_SAID=`kli vc list --name qvi --alias qvi --said --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao` echo \"$QVI_SAID\" | jq -f ${KERI_DEMO_SCRIPT_DIR}/data/legal-entity-edges-filter.jq > /tmp/legal-entity-edges.json kli saidify --file /tmp/legal-entity-edges.json -kli vc issue --name qvi --alias qvi --registry-name vLEI-qvi --schema ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY --recipient EIitNxxiNFXC1HDcPygyfyv3KUlBfS_Zf-ZYOvwjpTuz --data @${KERI_DEMO_SCRIPT_DIR}/data/legal-entity-data.json --edges @/tmp/legal-entity-edges.json --rules @${KERI_DEMO_SCRIPT_DIR}/data/rules.json -kli vc list --name legal-entity --alias legal-entity --poll +kli vc create --name qvi --alias qvi --registry-name vLEI-qvi --schema ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY --recipient EIitNxxiNFXC1HDcPygyfyv3KUlBfS_Zf-ZYOvwjpTuz --data @${KERI_DEMO_SCRIPT_DIR}/data/legal-entity-data.json --edges @/tmp/legal-entity-edges.json --rules @${KERI_DEMO_SCRIPT_DIR}/data/rules.json +SAID=$(kli vc list --name qvi --alias qvi --issued --said --schema ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY) +kli ipex grant --name qvi --alias qvi --said "${SAID}" --recipient EIitNxxiNFXC1HDcPygyfyv3KUlBfS_Zf-ZYOvwjpTuz +GRANT=$(kli ipex list --name legal-entity --alias legal-entity --poll --said) +kli ipex admit --name legal-entity --alias legal-entity --said "${GRANT}" +kli vc list --name legal-entity --alias legal-entity # Issue ECR Authorization credential from Legal Entity to QVI - have to create the edges first LE_SAID=`kli vc list --name legal-entity --alias legal-entity --said` echo \"$LE_SAID\" | jq -f ${KERI_DEMO_SCRIPT_DIR}/data/ecr-auth-edges-filter.jq > /tmp/ecr-auth-edges.json kli saidify --file /tmp/ecr-auth-edges.json -kli vc issue --name legal-entity --alias legal-entity --registry-name vLEI-legal-entity --schema EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g --recipient EHMnCf8_nIemuPx-cUHaDQq8zSnQIFAurdEpwHpNbnvX --data @${KERI_DEMO_SCRIPT_DIR}/data/ecr-auth-data.json --edges @/tmp/ecr-auth-edges.json --rules @${KERI_DEMO_SCRIPT_DIR}/data/ecr-auth-rules.json -kli vc list --name qvi --alias qvi --poll +kli vc create --name legal-entity --alias legal-entity --registry-name vLEI-legal-entity --schema EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g --recipient EHMnCf8_nIemuPx-cUHaDQq8zSnQIFAurdEpwHpNbnvX --data @${KERI_DEMO_SCRIPT_DIR}/data/ecr-auth-data.json --edges @/tmp/ecr-auth-edges.json --rules @${KERI_DEMO_SCRIPT_DIR}/data/ecr-auth-rules.json +SAID=$(kli vc list --name legal-entity --alias legal-entity --issued --said --schema EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g) +kli ipex grant --name legal-entity --alias legal-entity --said "${SAID}" --recipient EHMnCf8_nIemuPx-cUHaDQq8zSnQIFAurdEpwHpNbnvX +GRANT=$(kli ipex list --name qvi --alias qvi --poll --said | tail -1) +kli ipex admit --name qvi --alias qvi --said "${GRANT}" +kli vc list --name qvi --alias qvi # Issue ECR credential from QVI to Person AUTH_SAID=`kli vc list --name qvi --alias qvi --said --schema EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g` echo "[\"$QVI_SAID\", \"$AUTH_SAID\"]" | jq -f ${KERI_DEMO_SCRIPT_DIR}/data/ecr-edges-filter.jq > /tmp/ecr-edges.json kli saidify --file /tmp/ecr-edges.json -kli vc issue --name qvi --alias qvi --private --registry-name vLEI-qvi --schema EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw --recipient EKE7b7owCvObR6dBTrU7w38_oATL9Tcrp_-xjPn05zYe --data @${KERI_DEMO_SCRIPT_DIR}/data/ecr-data.json --edges @/tmp/ecr-edges.json --rules @${KERI_DEMO_SCRIPT_DIR}/data/ecr-rules.json -kli vc list --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O --poll +kli vc create --name qvi --alias qvi --private --registry-name vLEI-qvi --schema EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw --recipient EKE7b7owCvObR6dBTrU7w38_oATL9Tcrp_-xjPn05zYe --data @${KERI_DEMO_SCRIPT_DIR}/data/ecr-data.json --edges @/tmp/ecr-edges.json --rules @${KERI_DEMO_SCRIPT_DIR}/data/ecr-rules.json +SAID=$(kli vc list --name qvi --alias qvi --issued --said --schema EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw) +kli ipex grant --name qvi --alias qvi --said "${SAID}" --recipient EKE7b7owCvObR6dBTrU7w38_oATL9Tcrp_-xjPn05zYe +GRANT=$(kli ipex list --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O --poll --said) +kli ipex admit --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O --said "${GRANT}" +kli vc list --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O # Issue OOR Authorization credential from Legal Entity to QVI - have to create the edges first echo \"$LE_SAID\" | jq -f ${KERI_DEMO_SCRIPT_DIR}/data/oor-auth-edges-filter.jq > /tmp/oor-auth-edges.json kli saidify --file /tmp/oor-auth-edges.json -kli vc issue --name legal-entity --alias legal-entity --registry-name vLEI-legal-entity --schema EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E --recipient EHMnCf8_nIemuPx-cUHaDQq8zSnQIFAurdEpwHpNbnvX --data @${KERI_DEMO_SCRIPT_DIR}/data/oor-auth-data.json --edges @/tmp/oor-auth-edges.json --rules @${KERI_DEMO_SCRIPT_DIR}/data/rules.json -kli vc list --name qvi --alias qvi --poll +kli vc create --name legal-entity --alias legal-entity --registry-name vLEI-legal-entity --schema EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E --recipient EHMnCf8_nIemuPx-cUHaDQq8zSnQIFAurdEpwHpNbnvX --data @${KERI_DEMO_SCRIPT_DIR}/data/oor-auth-data.json --edges @/tmp/oor-auth-edges.json --rules @${KERI_DEMO_SCRIPT_DIR}/data/rules.json +SAID=$(kli vc list --name legal-entity --alias legal-entity --issued --said --schema EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E) +kli ipex grant --name legal-entity --alias legal-entity --said "${SAID}" --recipient EHMnCf8_nIemuPx-cUHaDQq8zSnQIFAurdEpwHpNbnvX +GRANT=$(kli ipex list --name qvi --alias qvi --poll --said | tail -1) +kli ipex admit --name qvi --alias qvi --said "${GRANT}" +kli vc list --name qvi --alias qvi # Issue OOR credential from QVI to Person AUTH_SAID=`kli vc list --name qvi --alias qvi --said --schema EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E` echo "[\"$QVI_SAID\", \"$AUTH_SAID\"]" | jq -f ${KERI_DEMO_SCRIPT_DIR}/data/oor-edges-filter.jq > /tmp/oor-edges.json kli saidify --file /tmp/oor-edges.json -kli vc issue --name qvi --alias qvi --registry-name vLEI-qvi --schema EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy --recipient EKE7b7owCvObR6dBTrU7w38_oATL9Tcrp_-xjPn05zYe --data @${KERI_DEMO_SCRIPT_DIR}/data/oor-data.json --edges @/tmp/oor-edges.json --rules @${KERI_DEMO_SCRIPT_DIR}/data/rules.json -kli vc list --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O --poll +kli vc create --name qvi --alias qvi --registry-name vLEI-qvi --schema EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy --recipient EKE7b7owCvObR6dBTrU7w38_oATL9Tcrp_-xjPn05zYe --data @${KERI_DEMO_SCRIPT_DIR}/data/oor-data.json --edges @/tmp/oor-edges.json --rules @${KERI_DEMO_SCRIPT_DIR}/data/rules.json +SAID=$(kli vc list --name qvi --alias qvi --issued --said --schema EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy) +kli ipex grant --name qvi --alias qvi --said "${SAID}" --recipient EKE7b7owCvObR6dBTrU7w38_oATL9Tcrp_-xjPn05zYe +GRANT=$(kli ipex list --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O --poll --said | tail -1) +kli ipex admit --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O --said "${GRANT}" +kli vc list --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O # Issue iXBRL data attestation from Person OOR_SAID=`kli vc list --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O --said --schema EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy` @@ -96,5 +120,5 @@ kli saidify --file /tmp/xbrl-edges.json NOW=`date -u +"%Y-%m-%dT%H:%M:%S+00:00"` echo \"$NOW\" | jq -f ${KERI_DEMO_SCRIPT_DIR}/data/xbrl-data.jq > /tmp/xbrl-data.json kli saidify --file /tmp/xbrl-data.json -kli vc issue --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O --registry-name vLEI-person --schema EMhvwOlyEJ9kN4PrwCpr9Jsv7TxPhiYveZ0oP3lJzdEi --data @/tmp/xbrl-data.json --edges @/tmp/xbrl-edges.json +kli vc create --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O --registry-name vLEI-person --schema EMhvwOlyEJ9kN4PrwCpr9Jsv7TxPhiYveZ0oP3lJzdEi --data @/tmp/xbrl-data.json --edges @/tmp/xbrl-edges.json kli vc list --name person --alias person --passcode DoB26Fj4x9LboAFWJra17O --issued \ No newline at end of file diff --git a/src/keri/app/agenting.py b/src/keri/app/agenting.py index ca2335ab3..1ea3842f3 100644 --- a/src/keri/app/agenting.py +++ b/src/keri/app/agenting.py @@ -10,7 +10,7 @@ from hio.base import doing from hio.core import http from hio.core.tcp import clienting -from hio.help import decking +from hio.help import decking, Hict from . import httping, forwarding from .. import help @@ -69,7 +69,6 @@ def receipt(self, pre, sn=None): if ser.ked['t'] in (coring.Ilks.rot,): adds = ser.ked["ba"] for wit in adds: - print(f"catching up {wit}") yield from self.catchup(ser.pre, wit) clients = dict() @@ -722,6 +721,100 @@ def idle(self): return len(self.sent) == self.posted +class TCPStreamMessenger(doing.DoDoer): + """ Send events to witnesses for receipting using TCP direct connection + + """ + + def __init__(self, hab, wit, url, msgs=None, sent=None, doers=None, **kwa): + """ + For the current event, gather the current set of witnesses, send the event, + gather all receipts and send them to all other witnesses + + Parameters: + hab: Habitat of the identifier to populate witnesses + + """ + self.hab = hab + self.wit = wit + self.url = url + self.posted = 0 + self.msgs = msgs if msgs is not None else decking.Deck() + self.sent = sent if sent is not None else decking.Deck() + self.parser = None + doers = doers if doers is not None else [] + doers.extend([doing.doify(self.receiptDo)]) + + self.kevery = eventing.Kevery(db=self.hab.db, + **kwa) + + super(TCPStreamMessenger, self).__init__(doers=doers) + + def receiptDo(self, tymth=None, tock=0.0): + """ + Returns doifiable Doist compatible generator method (doer dog) + + Usage: + add result of doify on this method to doers list + """ + self.wind(tymth) + self.tock = tock + _ = (yield self.tock) + + up = urlparse(self.url) + if up.scheme != kering.Schemes.tcp: + raise ValueError(f"invalid scheme {up.scheme} for TcpWitnesser") + + client = clienting.Client(host=up.hostname, port=up.port) + self.parser = parsing.Parser(ims=client.rxbs, + framed=True, + kvy=self.kevery) + + clientDoer = clienting.ClientDoer(client=client) + self.extend([clientDoer, doing.doify(self.msgDo)]) + + while True: + while not self.msgs: + yield self.tock + + msg = self.msgs.popleft() + self.posted += 1 + + client.tx(msg) # send to connected remote + + while client.txbs: + yield self.tock + + self.sent.append(msg) + yield self.tock + + def msgDo(self, tymth=None, tock=0.0, **opts): + """ + Returns doifiable Doist compatible generator method (doer dog) to process + incoming message stream of .kevery + + Doist Injected Attributes: + g.tock = tock # default tock attributes + g.done = None # default done state + g.opts + + Parameters: + tymth is injected function wrapper closure returned by .tymen() of + Tymist instance. Calling tymth() returns associated Tymist .tyme. + tock is injected initial tock value + opts is dict of injected optional additional parameters + + + Usage: + add result of doify on this method to doers list + """ + yield from self.parser.parsator() # process messages continuously + + @property + def idle(self): + return len(self.sent) == self.posted + + class HTTPMessenger(doing.DoDoer): """ Interacts with Recipients on HTTP and SSE for sending events and receiving receipts @@ -800,6 +893,65 @@ def idle(self): return len(self.msgs) == 0 and self.posted == len(self.sent) +class HTTPStreamMessenger(doing.DoDoer): + """ + Interacts with Recipients on HTTP and SSE for sending events and receiving receipts + + """ + + def __init__(self, hab, wit, url, msg=b'', headers=None, **kwa): + """ + For the current event, gather the current set of witnesses, send the event, + gather all receipts and send them to all other witnesses + + Parameters: + hab: Habitat of the identifier to populate witnesses + + """ + self.hab = hab + self.wit = wit + self.rep = None + headers = headers if headers is not None else {} + + up = urlparse(url) + if up.scheme != kering.Schemes.http and up.scheme != kering.Schemes.https: + raise ValueError(f"invalid scheme {up.scheme} for HTTPMessenger") + + self.client = http.clienting.Client(scheme=up.scheme, hostname=up.hostname, port=up.port) + clientDoer = http.clienting.ClientDoer(client=self.client) + + headers = Hict([ + ("Content-Type", "application/cesr"), + ("Content-Length", len(msg)), + (httping.CESR_DESTINATION_HEADER, self.wit), + ] + list(headers.items())) + + self.client.request( + method="PUT", + path="/", + headers=headers, + body=bytes(msg) + ) + + doers = [clientDoer] + + super(HTTPStreamMessenger, self).__init__(doers=doers, **kwa) + + def recur(self, tyme, deeds=None): + """ + Returns doifiable Doist compatible generator method (doer dog) + + Usage: + add result of doify on this method to doers list + """ + if self.client.responses: + self.rep = self.client.respond() + self.remove([self.client]) + return True + + return super(HTTPStreamMessenger, self).recur(tyme, deeds) + + def mailbox(hab, cid): for (_, erole, eid), end in hab.db.ends.getItemIter(keys=(cid, kering.Roles.mailbox)): if end.allowed: @@ -853,6 +1005,31 @@ def messengerFrom(hab, pre, urls): return witer +def streamMessengerFrom(hab, pre, urls, msg, headers=None): + """ Create a Witnesser (tcp or http) based on provided endpoints + + Parameters: + hab (Habitat): Environment to use to look up witness URLs + pre (str): qb64 identifier prefix of recipient to create a messanger for + urls (dict): map of schemes to urls of available endpoints + msg (bytes): bytes of message to send + headers (dict): optional headers to send with HTTP requests + + Returns: + Optional(TcpWitnesser, HTTPMessenger): witnesser for ensuring full reciepts + """ + if kering.Schemes.http in urls or kering.Schemes.https in urls: + url = urls[kering.Schemes.http] if kering.Schemes.http in urls else urls[kering.Schemes.https] + witer = HTTPStreamMessenger(hab=hab, wit=pre, url=url, msg=msg, headers=headers) + elif kering.Schemes.tcp in urls: + url = urls[kering.Schemes.tcp] + witer = TCPStreamMessenger(hab=hab, wit=pre, url=url) + else: + raise kering.ConfigurationError(f"unable to find a valid endpoint for witness {pre}") + + return witer + + def httpClient(hab, wit): """ Create and return a http.client and http.ClientDoer for the witness diff --git a/src/keri/app/cli/commands/ends/add.py b/src/keri/app/cli/commands/ends/add.py index 9b275e8df..e672abfda 100644 --- a/src/keri/app/cli/commands/ends/add.py +++ b/src/keri/app/cli/commands/ends/add.py @@ -33,7 +33,7 @@ parser.add_argument("--eid", "-e", help="qualified base64 of AID to authorize with new role for the AID identified " "by alias", required=True) -parser.add_argument("--time", help="timestamp for the revocation", required=False, default=None) +parser.add_argument("--time", help="timestamp for the end auth", required=False, default=None) def add_end(args): diff --git a/src/keri/app/cli/commands/ipex/admit.py b/src/keri/app/cli/commands/ipex/admit.py index a06618874..f45918aa2 100644 --- a/src/keri/app/cli/commands/ipex/admit.py +++ b/src/keri/app/cli/commands/ipex/admit.py @@ -7,7 +7,7 @@ from hio.base import doing -from keri.app import forwarding, connecting, habbing, grouping, indirecting, agenting +from keri.app import connecting, habbing, grouping, indirecting, agenting, forwarding from keri.app.cli.common import existing from keri.app.notifying import Notifier from keri.core import parsing, coring, eventing @@ -52,7 +52,6 @@ def __init__(self, name, alias, base, bran, said, message): self.hab = self.hby.habByName(alias) self.rgy = credentialing.Regery(hby=self.hby, name=name, base=base) self.org = connecting.Organizer(hby=self.hby) - self.postman = forwarding.Poster(hby=self.hby) self.witq = agenting.WitnessInquisitor(hby=self.hby) self.kvy = eventing.Kevery(db=self.hby.db) @@ -66,13 +65,13 @@ def __init__(self, name, alias, base, bran, said, message): self.exc = exchanging.Exchanger(hby=self.hby, handlers=[]) grouping.loadHandlers(self.exc, mux) - protocoling.loadHandlers(self.hby, rgy=self.rgy, exc=self.exc, notifier=notifier) + protocoling.loadHandlers(self.hby, exc=self.exc, notifier=notifier) mbx = indirecting.MailboxDirector(hby=self.hby, topics=["/receipt", "/multisig", "/replay", "/credential"], exc=self.exc, kvy=self.kvy, tvy=self.tvy, verifier=self.vry) - self.toRemove = [self.postman, mbx, self.witq] + self.toRemove = [mbx, self.witq] super(AdmitDoer, self).__init__(doers=self.toRemove + [doing.doify(self.admitDo)]) def admitDo(self, tymth, tock=0.0): @@ -132,24 +131,25 @@ def admitDo(self, tymth, tock=0.0): smids.remove(self.hab.mhab.pre) for recp in smids: # this goes to other participants only as a signaling mechanism - self.postman.send(src=self.hab.mhab.pre, - dest=recp, - topic="multisig", - serder=wexn, - attachment=watc) + postman = forwarding.StreamPoster(hby=self.hby, hab=self.hab.mhab, recp=recp, topic="multisig") + postman.send(serder=wexn, + attachment=watc) + doer = doing.DoDoer(doers=postman.deliver()) + self.extend([doer]) while not self.exc.complete(said=wexn.said): yield self.tock if self.exc.lead(self.hab, said=exn.said): print(f"Sending admit message to {recp}") - self.postman.send(src=self.hab.pre, - dest=recp, - topic="credential", - serder=exn, - attachment=atc) + postman = forwarding.StreamPoster(hby=self.hby, hab=self.hab, recp=recp, topic="credential") + postman.send(serder=exn, + attachment=atc) - while not self.postman.sent(exn.said): + doer = doing.DoDoer(doers=postman.deliver()) + self.extend([doer]) + + while not doer.done: yield self.tock self.remove(self.toRemove) diff --git a/src/keri/app/cli/commands/ipex/grant.py b/src/keri/app/cli/commands/ipex/grant.py index bcc9157cb..e169ced62 100644 --- a/src/keri/app/cli/commands/ipex/grant.py +++ b/src/keri/app/cli/commands/ipex/grant.py @@ -57,19 +57,18 @@ def __init__(self, name, alias, base, bran, said, recp, message, timestamp): self.hab = self.hby.habByName(alias) self.rgy = credentialing.Regery(hby=self.hby, name=name, base=base) self.org = connecting.Organizer(hby=self.hby) - self.postman = forwarding.Poster(hby=self.hby) notifier = Notifier(self.hby) mux = grouping.Multiplexor(self.hby, notifier=notifier) self.exc = exchanging.Exchanger(hby=self.hby, handlers=[]) grouping.loadHandlers(self.exc, mux) - protocoling.loadHandlers(self.hby, rgy=self.rgy, exc=self.exc, notifier=notifier) + protocoling.loadHandlers(self.hby, exc=self.exc, notifier=notifier) mbx = indirecting.MailboxDirector(hby=self.hby, topics=["/receipt", "/multisig", "/replay", "/credential"], exc=self.exc) - self.toRemove = [self.postman, mbx] + self.toRemove = [mbx] super(GrantDoer, self).__init__(doers=self.toRemove + [doing.doify(self.grantDo)]) def grantDo(self, tymth, tock=0.0): @@ -123,42 +122,44 @@ def grantDo(self, tymth, tock=0.0): parsing.Parser().parseOne(ims=bytes(msg), exc=self.exc) - sender = self.hab.pre if isinstance(self.hab, habbing.GroupHab): - sender = self.hab.mhab.pre wexn, watc = grouping.multisigExn(self.hab, exn=msg) smids = self.hab.db.signingMembers(pre=self.hab.pre) smids.remove(self.hab.mhab.pre) for part in smids: # this goes to other participants - self.postman.send(src=self.hab.mhab.pre, - dest=part, - topic="multisig", - serder=wexn, - attachment=watc) + postman = forwarding.StreamPoster(hby=self.hby, hab=self.hab.mhab, recp=part, topic="multisig") + postman.send(serder=wexn, + attachment=watc) + doer = doing.DoDoer(doers=postman.deliver()) + self.extend([doer]) while not self.exc.complete(said=exn.said): yield self.tock if self.exc.lead(self.hab, said=exn.said): print(f"Sending message {exn.said} to {recp}") + postman = forwarding.StreamPoster(hby=self.hby, hab=self.hab, recp=recp, topic="credential") + sources = self.rgy.reger.sources(self.hby.db, creder) + credentialing.sendArtifacts(self.hby, self.rgy.reger, postman, creder, recp) for source, atc in sources: - credentialing.sendArtifacts(self.hby, self.rgy.reger, self.postman, source, sender, recp) - self.postman.send(src=sender, dest=recp, topic="credential", serder=source, attachment=atc) + credentialing.sendArtifacts(self.hby, self.rgy.reger, postman, source, recp) + postman.send(serder=source, attachment=atc) atc = exchanging.serializeMessage(self.hby, exn.said) del atc[:exn.size] - self.postman.send(src=sender, - dest=recp, - topic="credential", - serder=exn, - attachment=atc) + postman.send(serder=exn, + attachment=atc) + + doer = doing.DoDoer(doers=postman.deliver()) + self.extend([doer]) - while not self.postman.sent(said=exn.said): + while not doer.done: yield self.tock - print("... grant message sent") + print(f"... grant message sent") + self.remove([postman]) self.remove(self.toRemove) diff --git a/src/keri/app/cli/commands/ipex/list.py b/src/keri/app/cli/commands/ipex/list.py index 4d977c41f..37148bc54 100644 --- a/src/keri/app/cli/commands/ipex/list.py +++ b/src/keri/app/cli/commands/ipex/list.py @@ -81,7 +81,7 @@ def __init__(self, name, alias, base, bran, poll=False, verbose=False, said=Fals self.rgy = credentialing.Regery(hby=self.hby, name=name, base=base) self.vry = verifying.Verifier(hby=self.hby, reger=self.rgy.reger) self.exc = exchanging.Exchanger(hby=self.hby, handlers=[]) - protocoling.loadHandlers(self.hby, self.exc, self.rgy, self.notifier) + protocoling.loadHandlers(self.hby, self.exc, self.notifier) self.mbx = indirecting.MailboxDirector(hby=self.hby, topics=['/replay', 'reply', '/credential'], exc=self.exc, verifier=self.vry) diff --git a/src/keri/app/cli/commands/ipex/spurn.py b/src/keri/app/cli/commands/ipex/spurn.py index 2ca50d1fe..3d4ecebe8 100644 --- a/src/keri/app/cli/commands/ipex/spurn.py +++ b/src/keri/app/cli/commands/ipex/spurn.py @@ -53,7 +53,6 @@ def __init__(self, name, alias, base, bran, said, message): self.hab = self.hby.habByName(alias) self.rgy = credentialing.Regery(hby=self.hby, name=name, base=base) self.org = connecting.Organizer(hby=self.hby) - self.postman = forwarding.Poster(hby=self.hby) kvy = eventing.Kevery(db=self.hby.db) tvy = teventing.Tevery(db=self.hby.db, reger=self.rgy.reger) @@ -66,13 +65,13 @@ def __init__(self, name, alias, base, bran, said, message): self.exc = exchanging.Exchanger(hby=self.hby, handlers=[]) grouping.loadHandlers(self.exc, mux) - protocoling.loadHandlers(self.hby, rgy=self.rgy, exc=self.exc, notifier=notifier) + protocoling.loadHandlers(self.hby, exc=self.exc, notifier=notifier) mbx = indirecting.MailboxDirector(hby=self.hby, topics=["/receipt", "/multisig", "/replay", "/credential"], exc=self.exc) - self.toRemove = [self.postman, mbx] + self.toRemove = [mbx] super(SpurnDoer, self).__init__(doers=self.toRemove + [doing.doify(self.spurnDo)]) def spurnDo(self, tymth, tock=0.0): @@ -119,24 +118,25 @@ def spurnDo(self, tymth, tock=0.0): smids.remove(self.hab.mhab.pre) for recp in smids: # this goes to other participants only as a signaling mechanism - self.postman.send(src=self.hab.mhab.pre, - dest=recp, - topic="multisig", - serder=wexn, - attachment=watc) + postman = forwarding.StreamPoster(hby=self.hby, hab=self.hab.mhab, recp=recp, topic="multisig") + postman.send(serder=wexn, + attachment=watc) + doer = doing.DoDoer(doers=postman.deliver()) + self.extend([doer]) while not self.exc.complete(said=wexn.said): yield self.tock if self.exc.lead(self.hab, said=exn.said): print("Sending spurn message...") - self.postman.send(src=self.hab.pre, - dest=recp, - topic="credential", - serder=exn, - attachment=atc) + postman = forwarding.StreamPoster(hby=self.hby, hab=self.hab, recp=recp, topic="credential") + postman.send(serder=exn, + attachment=atc) - while not self.postman.cues: + doer = doing.DoDoer(doers=postman.deliver()) + self.extend([doer]) + + while not doer.done: yield self.tock self.remove(self.toRemove) diff --git a/src/keri/app/cli/commands/vc/present.py b/src/keri/app/cli/commands/vc/present.py deleted file mode 100644 index 29f11e954..000000000 --- a/src/keri/app/cli/commands/vc/present.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- encoding: utf-8 -*- -""" -KERI -keri.kli.commands module - -""" -import argparse - -from hio import help -from hio.base import doing - -from keri.app import connecting, forwarding -from keri.app.cli.common import existing -from keri.app.habbing import GroupHab -from keri.core import coring -from keri.vc import protocoling -from keri.vdr import credentialing - -logger = help.ogler.getLogger() - -parser = argparse.ArgumentParser(description='Send credential presentation for specified credential to recipient') -parser.set_defaults(handler=lambda args: present_credential(args), - transferable=True) -parser.add_argument('--name', '-n', help='keystore name and file location of KERI keystore', required=True) -parser.add_argument('--alias', '-a', help='human readable alias for the identifier to whom the credential was issued', - required=True) -parser.add_argument('--base', '-b', help='additional optional prefix to file location of KERI keystore', - required=False, default="") -parser.add_argument('--passcode', '-p', help='22 character encryption passcode for keystore (is not saved)', - dest="bran", default=None) # passcode => bran - -parser.add_argument("--said", "-s", help="SAID of the credential to present.", required=True) -parser.add_argument("--include", "-i", help="send credential and all other cryptographic artifacts with presentation", - action="store_true") -parser.add_argument("--recipient", "-r", help="alias or qb64 AID ") - - -def present_credential(args): - """ Command line credential presentation handler - - """ - - ed = PresentDoer(name=args.name, - alias=args.alias, - base=args.base, - bran=args.bran, - said=args.said, - recipient=args.recipient, - include=args.include) - return [ed] - - -class PresentDoer(doing.DoDoer): - - def __init__(self, name, alias, base, bran, said, recipient, include): - self.said = said - self.recipient = recipient - self.include = include - - self.hby = existing.setupHby(name=name, base=base, bran=bran) - self.hab = self.hby.habByName(alias) - self.org = connecting.Organizer(hby=self.hby) - self.rgy = credentialing.Regery(hby=self.hby, name=name, base=base) - self.postman = forwarding.Poster(hby=self.hby) - - doers = [self.postman, doing.doify(self.presentDo)] - - super(PresentDoer, self).__init__(doers=doers) - - def presentDo(self, tymth, tock=0.0): - """ Present credential from store and any related material - - Parameters: - tymth (function): injected function wrapper closure returned by .tymen() of - Tymist instance. Calling tymth() returns associated Tymist .tyme. - tock (float): injected initial tock value - - Returns: doifiable Doist compatible generator method - - """ - # enter context - self.wind(tymth) - self.tock = tock - _ = (yield self.tock) - - creder = self.rgy.reger.creds.get(self.said) - if creder is None: - raise ValueError(f"invalid credential SAID {self.said}") - - if self.recipient in self.hby.kevers: - recp = self.recipient - else: - recp = self.org.find("alias", self.recipient) - if len(recp) != 1: - raise ValueError(f"invalid recipient {self.recipient}") - recp = recp[0]['id'] - - if self.include: - credentialing.sendCredential(self.hby, hab=self.hab, reger=self.rgy.reger, postman=self.postman, - creder=creder, recp=recp) - - if isinstance(self.hab, GroupHab): - senderHab = self.hab.mhab - else: - senderHab = self.hab - - if senderHab.pre != creder.issuer: - for msg in senderHab.db.cloneDelegation(senderHab.kever): - serder = coring.Serder(raw=msg) - atc = msg[serder.size:] - self.postman.send(src=senderHab.pre, dest=recp, topic="credential", serder=serder, attachment=atc) - - for msg in senderHab.db.clonePreIter(pre=senderHab.pre): - serder = coring.Serder(raw=msg) - atc = msg[serder.size:] - self.postman.send(src=senderHab.pre, dest=recp, topic="credential", serder=serder, attachment=atc) - - exn, atc = protocoling.presentationExchangeExn(hab=senderHab, reger=self.rgy.reger, said=self.said) - self.postman.send(src=senderHab.pre, dest=recp, topic="credential", serder=exn, attachment=atc) - - while True: - while self.postman.cues: - cue = self.postman.cues.popleft() - if "said" in cue and cue["said"] == exn.said: - print("Presentation sent") - toRemove = [self.postman] - self.remove(toRemove) - return True - yield self.tock - yield self.tock - diff --git a/src/keri/app/forwarding.py b/src/keri/app/forwarding.py index 8ba5e2ce5..26a0417cf 100644 --- a/src/keri/app/forwarding.py +++ b/src/keri/app/forwarding.py @@ -30,12 +30,11 @@ class Poster(doing.DoDoer): """ - def __init__(self, hby, mbx=None, evts=None, cues=None, klas=None, **kwa): + def __init__(self, hby, mbx=None, evts=None, cues=None, **kwa): self.hby = hby self.mbx = mbx self.evts = evts if evts is not None else decking.Deck() self.cues = cues if cues is not None else decking.Deck() - self.klas = klas if klas is not None else agenting.HTTPMessenger doers = [doing.doify(self.deliverDo)] super(Poster, self).__init__(doers=doers, **kwa) @@ -165,10 +164,10 @@ def sendDirect(self, hab, ends, serder, atc): witer.msgs.append(bytearray(msg)) # make a copy self.extend([witer]) - while not witer.idle: - _ = (yield self.tock) + while not witer.idle: + _ = (yield self.tock) - self.remove([witer]) + self.remove([witer]) def forward(self, hab, ends, recp, serder, atc, topic): # If we are one of the mailboxes, just store locally in mailbox @@ -237,6 +236,155 @@ def forwardToWitness(self, hab, ends, recp, serder, atc, topic): _ = (yield self.tock) +class StreamPoster: + """ + DoDoer that wraps any KERI event (KEL, TEL, Peer to Peer) in a /fwd `exn` envelope and + delivers them to one of the target recipient's witnesses for store and forward + to the intended recipient + + """ + + def __init__(self, hby, recp, src=None, hab=None, mbx=None, topic=None, headers=None, **kwa): + if hab is not None: + self.hab = hab + else: + self.hab = hby.habs[src] + + self.hby = hby + self.hab = hab + self.recp = recp + self.src = src + self.messagers = [] + self.mbx = mbx + self.topic = topic + self.headers = headers + self.evts = decking.Deck() + + def deliver(self): + """ + Returns: doifiable Doist compatible generator method that processes + a queue of messages and envelopes them in a `fwd` message + and sends them to one of the witnesses of the recipient for + store and forward. + + Usage: + add result of doify on this method to doers list + """ + msg = bytearray() + + while self.evts: + evt = self.evts.popleft() + + serder = evt["serder"] + atc = evt["attachment"] if "attachment" in evt else b'' + + msg.extend(serder.raw) + msg.extend(atc) + + if len(msg) == 0: + return [] + + ends = self.hab.endsFor(self.recp) + try: + # If there is a controller or agent in ends, send to all + if {Roles.controller, Roles.agent, Roles.mailbox} & set(ends): + for role in (Roles.controller, Roles.agent, Roles.mailbox): + if role in ends: + if role == Roles.mailbox: + return self.forward(self.hab, ends[role], msg=msg, topic=self.topic) + else: + return self.sendDirect(self.hab, ends[role], msg=msg) + # otherwise send to one witness + elif Roles.witness in ends: + return self.forward(self.hab, ends[Roles.witness], msg=msg, topic=self.topic) + + else: + logger.info(f"No end roles for {self.recp} to send evt={self.recp}") + return [] + + except kering.ConfigurationError as e: + logger.error(f"Error sending to {self.recp} with ends={ends}. Err={e}") + return [] + + def send(self, serder, attachment=None): + """ + Utility function to queue a msg on the Poster's buffer for + enveloping and forwarding to a witness + + Parameters: + serder (Serder) KERI event message to envelope and forward: + attachment (bytes): attachment bytes + + """ + ends = self.hab.endsFor(self.recp) + try: + # If there is a controller, agent or mailbox in ends, send to all + if {Roles.controller, Roles.agent, Roles.mailbox} & set(ends): + for role in (Roles.controller, Roles.agent, Roles.mailbox): + if role in ends: + if role == Roles.mailbox: + serder, attachment = self.createForward(self.hab, serder=serder, ends=ends, + atc=attachment, topic=self.topic) + + # otherwise send to one witness + elif Roles.witness in ends: + serder, attachment = self.createForward(self.hab, ends=ends, serder=serder, + atc=attachment, topic=self.topic) + else: + logger.info(f"No end roles for {self.recp} to send evt={self.recp}") + raise kering.ValidationError(f"No end roles for {self.recp} to send evt={self.recp}") + except kering.ConfigurationError as e: + logger.error(f"Error sending to {self.recp} with ends={ends}. Err={e}") + raise kering.ValidationError(f"Error sending to {self.recp} with ends={ends}. Err={e}") + + evt = dict(serder=serder) + if attachment is not None: + evt["attachment"] = attachment + + self.evts.append(evt) + + 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, + headers=self.headers)) + + return self.messagers + + def createForward(self, hab, ends, serder, atc, topic): + # If we are one of the mailboxes, just store locally in mailbox + owits = oset(ends.keys()) + if self.mbx and owits.intersection(hab.prefixes): + msg = bytearray(serder.raw) + if atc is not None: + msg.extend(atc) + self.mbx.storeMsg(topic=f"{self.recp}/{topic}".encode("utf-8"), msg=msg) + return None, None + + # Its not us, randomly select a mailbox and forward it on + evt = bytearray(serder.raw) + evt.extend(atc) + fwd, atc = exchanging.exchange(route='/fwd', modifiers=dict(pre=self.recp, topic=topic), + payload={}, embeds=dict(evt=evt), sender=hab.pre) + ims = hab.endorse(serder=fwd, last=False, pipelined=False) + return fwd, ims + atc + + def forward(self, hab, ends, msg, topic): + # If we are one of the mailboxes, just store locally in mailbox + owits = oset(ends.keys()) + if self.mbx and owits.intersection(hab.prefixes): + self.mbx.storeMsg(topic=f"{self.recp}/{topic}".encode("utf-8"), msg=msg) + return [] + + # Its not us, randomly select a mailbox and forward it on + mbx, mailbox = random.choice(list(ends.items())) + ims = bytearray() + ims.extend(introduce(hab, mbx)) + ims.extend(msg) + + self.messagers.append(agenting.streamMessengerFrom(hab=hab, pre=mbx, urls=mailbox, msg=bytes(ims))) + return self.messagers + + class ForwardHandler: """ Handler for forward `exn` messages used to envelope other KERI messages intended for another recipient. diff --git a/src/keri/app/habbing.py b/src/keri/app/habbing.py index 2b8c7019e..87f8e79e7 100644 --- a/src/keri/app/habbing.py +++ b/src/keri/app/habbing.py @@ -1338,6 +1338,25 @@ def sign(self, ser, verfers=None, indexed=True, indices=None, ondices=None, **kw indices=indices, ondices=ondices) + def decrypt(self, ser, verfers=None, **kwa): + """Sign given serialization ser using appropriate keys. + Use provided verfers or .kever.verfers to lookup keys to sign. + + Parameters: + ser (bytes): serialization to sign + verfers (list[Verfer] | None): Verfer instances to get pub verifier + keys to lookup private siging keys. + verfers None means use .kever.verfers. Assumes that when group + and verfers is not None then provided verfers must be .kever.verfers + + """ + if verfers is None: + verfers = self.kever.verfers # when group these provide group signing keys + + return self.mgr.decrypt(ser=ser, + verfers=verfers, + ) + def query(self, pre, src, query=None, **kwa): """ Create, sign and return a `qry` message against the attester for the prefix @@ -1934,6 +1953,8 @@ def replyEndRole(self, cid, role=None, eids=None, scheme=""): if cid not in self.kevers: return msgs + msgs.extend(self.replay(cid)) + kever = self.kevers[cid] witness = self.pre in kever.wits # see if we are cid's witness @@ -1953,7 +1974,6 @@ def replyEndRole(self, cid, role=None, eids=None, scheme=""): msgs.extend(self.loadLocScheme(eid=eid, scheme=scheme)) msgs.extend(self.loadEndRole(cid=cid, eid=eid, role=erole)) - msgs.extend(self.replay(cid)) return msgs def replyToOobi(self, aid, role, eids=None): @@ -2465,6 +2485,9 @@ def replyEndRole(self, cid, role=None, eids=None, scheme=""): if eids is None: eids = [] + # introduce yourself, please + msgs.extend(self.replay(cid)) + if role == kering.Roles.witness: if kever := self.kevers[cid] if cid in self.kevers else None: witness = self.pre in kever.wits # see if we are cid's witness @@ -2484,9 +2507,6 @@ def replyEndRole(self, cid, role=None, eids=None, scheme=""): msgs.extend(self.loadLocScheme(eid=eid, scheme=scheme)) msgs.extend(self.loadEndRole(cid=cid, eid=eid, role=erole)) - # introduce yourself, please - msgs.extend(self.replay(cid)) - return msgs diff --git a/src/keri/app/indirecting.py b/src/keri/app/indirecting.py index d44f5cc28..9896649ca 100644 --- a/src/keri/app/indirecting.py +++ b/src/keri/app/indirecting.py @@ -59,7 +59,7 @@ def setupWitness(hby, alias="witness", mbx=None, aids=None, tcpPort=5631, httpPo app = falcon.App(cors_enable=True) ending.loadEnds(app=app, hby=hby, default=hab.pre) - oobiRes = oobiing.loadEnds(app=app, hby=hby, prefix="/ext") + oobiing.loadEnds(app=app, hby=hby, prefix="/ext") rep = storing.Respondant(hby=hby, mbx=mbx, aids=aids) rvy = routing.Revery(db=hby.db, cues=cues) @@ -108,7 +108,6 @@ def setupWitness(hby, alias="witness", mbx=None, aids=None, tcpPort=5631, httpPo kvy=kvy, tvy=tvy, rvy=rvy, exc=exchanger, replies=rep.reps, responses=rep.cues, queries=httpEnd.qrycues) - doers.extend(oobiRes) doers.extend([regDoer, httpServerDoer, rep, witStart, receiptEnd, *oobiery.doers]) return doers @@ -890,27 +889,69 @@ def on_post(self, req, rep): rep.set_header('connection', "close") cr = httping.parseCesrHttpRequest(req=req) - serder = eventing.Serder(ked=cr.payload, kind=eventing.Serials.json) - msg = bytearray(serder.raw) + sadder = coring.Sadder(ked=cr.payload, kind=eventing.Serials.json) + msg = bytearray(sadder.raw) msg.extend(cr.attachments.encode("utf-8")) self.rxbs.extend(msg) - ilk = serder.ked["t"] - if ilk in (Ilks.icp, Ilks.rot, Ilks.ixn, Ilks.dip, Ilks.drt, Ilks.exn, Ilks.rpy): + if sadder.proto in ("ACDC",): rep.set_header('Content-Type', "application/json") rep.status = falcon.HTTP_204 - elif ilk in (Ilks.vcp, Ilks.vrt, Ilks.iss, Ilks.rev, Ilks.bis, Ilks.brv): - rep.set_header('Content-Type', "application/json") - rep.status = falcon.HTTP_204 - elif ilk in (Ilks.qry,): - if serder.ked["r"] in ("mbx",): - rep.set_header('Content-Type', "text/event-stream") - rep.status = falcon.HTTP_200 - rep.stream = QryRpyMailboxIterable(mbx=self.mbx, cues=self.qrycues, said=serder.said) - else: + else: + ilk = sadder.ked["t"] + if ilk in (Ilks.icp, Ilks.rot, Ilks.ixn, Ilks.dip, Ilks.drt, Ilks.exn, Ilks.rpy): + rep.set_header('Content-Type', "application/json") + rep.status = falcon.HTTP_204 + elif ilk in (Ilks.vcp, Ilks.vrt, Ilks.iss, Ilks.rev, Ilks.bis, Ilks.brv): rep.set_header('Content-Type', "application/json") rep.status = falcon.HTTP_204 + elif ilk in (Ilks.qry,): + if sadder.ked["r"] in ("mbx",): + rep.set_header('Content-Type', "text/event-stream") + rep.status = falcon.HTTP_200 + rep.stream = QryRpyMailboxIterable(mbx=self.mbx, cues=self.qrycues, said=sadder.said) + else: + rep.set_header('Content-Type', "application/json") + rep.status = falcon.HTTP_204 + + def on_put(self, req, rep): + """ + Handles PUT for KERI mbx event messages. + + Parameters: + req (Request) Falcon HTTP request + rep (Response) Falcon HTTP response + + --- + summary: Accept KERI events with attachment headers and parse + description: Accept KERI events with attachment headers and parse. + tags: + - Events + requestBody: + required: true + content: + application/json: + schema: + type: object + description: KERI event message + responses: + 200: + description: Mailbox query response for server sent events + 204: + description: KEL or EXN event accepted. + """ + if req.method == "OPTIONS": + rep.status = falcon.HTTP_200 + return + + rep.set_header('Cache-Control', "no-cache") + rep.set_header('connection', "close") + + self.rxbs.extend(req.bounded_stream.read()) + + rep.set_header('Content-Type', "application/json") + rep.status = falcon.HTTP_204 class QryRpyMailboxIterable: diff --git a/src/keri/app/keeping.py b/src/keri/app/keeping.py index 516b56853..085957fa5 100644 --- a/src/keri/app/keeping.py +++ b/src/keri/app/keeping.py @@ -26,6 +26,7 @@ from collections import namedtuple, deque from dataclasses import dataclass, asdict, field +import pysodium from hio.base import doing from .. import kering @@ -1392,6 +1393,56 @@ def sign(self, ser, pubs=None, verfers=None, indexed=True, cigars.append(signer.sign(ser)) # assigns .verfer to cigar return cigars + def decrypt(self, ser, pubs=None, verfers=None): + """ + Returns list of signatures of ser if indexed as Sigers else as Cigars with + .verfer assigned. + + Parameters: + ser (bytes): serialization to sign + pubs (list[str] | None): of qb64 public keys to lookup private keys + one of pubs or verfers is required. If both then verfers is ignored. + verfers (list[Verfer] | None): Verfer instances of public keys + one of pubs or verfers is required. If both then verfers is ignored. + If not pubs then gets public key from verfer.qb64 + + Returns: + bytes: decrypted data + + """ + signers = [] + if pubs: + for pub in pubs: + if self.aeid and not self.decrypter: + raise kering.DecryptError("Unauthorized decryption attempt. " + "Aeid but no decrypter.") + if ((signer := self.ks.pris.get(pub, decrypter=self.decrypter)) + is None): + raise ValueError("Missing prikey in db for pubkey={}".format(pub)) + signers.append(signer) + + else: + for verfer in verfers: + if self.aeid and not self.decrypter: + raise kering.DecryptError("Unauthorized decryption attempt. " + "Aeid but no decrypter.") + if ((signer := self.ks.pris.get(verfer.qb64, + decrypter=self.decrypter)) + is None): + raise ValueError("Missing prikey in db for pubkey={}".format(verfer.qb64)) + signers.append(signer) + + plain = ser + for signer in signers: + sigkey = signer.raw + signer.verfer.raw # sigkey is raw seed + raw verkey + prikey = pysodium.crypto_sign_sk_to_box_sk(sigkey) # raw private encrypt key + pubkey = pysodium.crypto_scalarmult_curve25519_base(prikey) + plain = pysodium.crypto_box_seal_open(plain, pubkey, prikey) # qb64b + + if plain == ser: + raise ValueError("unable to decrypt data") + + return plain def ingest(self, secrecies, iridx=0, ncount=1, ncode=coring.MtrDex.Ed25519_Seed, dcode=coring.MtrDex.Blake3_256, diff --git a/src/keri/app/oobiing.py b/src/keri/app/oobiing.py index b596a0ec3..073e690b9 100644 --- a/src/keri/app/oobiing.py +++ b/src/keri/app/oobiing.py @@ -36,9 +36,7 @@ def loadEnds(app, *, hby, prefix=""): oobiEnd = OobiResource(hby=hby) app.add_route(prefix + "/oobi", oobiEnd) - app.add_route(prefix + "/oobi/groups/{alias}/share", oobiEnd, suffix="share") - - return [oobiEnd] + return [] def loadHandlers(hby, exc, notifier): @@ -54,7 +52,7 @@ def loadHandlers(hby, exc, notifier): exc.addHandler(oobireq) -class OobiResource(doing.DoDoer): +class OobiResource: """ Resource for managing OOBIs @@ -69,11 +67,6 @@ def __init__(self, hby): """ self.hby = hby - self.postman = forwarding.Poster(hby=self.hby) - doers = [self.postman] - - super(OobiResource, self).__init__(doers=doers) - def on_get_alias(self, req, rep, alias=None): """ OOBI GET endpoint @@ -210,68 +203,6 @@ def on_post(self, req, rep): rep.status = falcon.HTTP_202 - def on_post_share(self, req, rep, alias): - """ Share OOBI endpoint. - - Parameters: - req: falcon.Request HTTP request - rep: falcon.Response HTTP response - alias: human readable name of the local identifier context for resolving this OOBI - - --- - summary: Share OOBI and alias for remote identifier with other aids - description: Send all other participants in a group AID a copy of the OOBI with suggested alias - tags: - - OOBIs - parameters: - - in: path - name: alias - schema: - type: string - required: true - description: Human readable alias for AID to use to sign exn message - requestBody: - required: true - content: - application/json: - schema: - description: OOBI - properties: - oobis: - type: array - items: - type: string - description: URL OOBI - responses: - 202: - description: OOBI resolution to key state successful - - """ - body = req.get_media() - hab = self.hby.habByName(alias) - if hab is None: - rep.status = falcon.HTTP_404 - rep.text = f"Unknown identifier {alias}" - return - - if not isinstance(hab, GroupHab): - rep.status = falcon.HTTP_400 - rep.text = f"Identifier for {alias} is not a group hab, not supported" - return - - oobis = body["oobis"] - both = list(set(hab.smids + (hab.rmids or []))) - for mid in both: # hab.smids - if mid == hab.mhab.pre: - continue - - for oobi in oobis: - exn, atc = oobiRequestExn(hab.mhab, mid, oobi) - self.postman.send(src=hab.mhab.pre, dest=mid, topic="oobi", serder=exn, attachment=atc) - - rep.status = falcon.HTTP_200 - return - class OobiRequestHandler: """ diff --git a/src/keri/core/eventing.py b/src/keri/core/eventing.py index 066b081f5..9b008ee33 100644 --- a/src/keri/core/eventing.py +++ b/src/keri/core/eventing.py @@ -3539,7 +3539,7 @@ def processReplyEndRole(self, *, serder, saider, route, aid=aid, osaider=osaider, cigars=cigars, tsgs=tsgs) if not accepted: - raise UnverifiedReplyError(f"Unverified reply. {serder.ked}") + raise UnverifiedReplyError(f"Unverified end role reply. {serder.ked}") self.updateEnd(keys=keys, saider=saider, allowed=allowed) # update .eans and .ends @@ -3636,7 +3636,7 @@ def processReplyLocScheme(self, *, serder, saider, route, aid=aid, osaider=osaider, cigars=cigars, tsgs=tsgs) if not accepted: - raise UnverifiedReplyError(f"B Unverified reply. {serder.ked}") + raise UnverifiedReplyError(f"Unverified loc scheme reply. {serder.ked}") self.updateLoc(keys=keys, saider=saider, url=url) # update .lans and .locs @@ -3750,7 +3750,7 @@ def processReplyKeyStateNotice(self, *, serder, saider, route, aid=aid, osaider=osaider, cigars=cigars, tsgs=tsgs) if not accepted: - raise UnverifiedReplyError(f"C Unverified reply. {serder.ked}") + raise UnverifiedReplyError(f"Unverified key state notice reply. {serder.ked}") ldig = self.db.getKeLast(key=snKey(pre=pre, sn=sn)) # retrieve dig of last event at sn. diger = coring.Diger(qb64=ksr.d) diff --git a/src/keri/core/parsing.py b/src/keri/core/parsing.py index 036a3f069..e47e64248 100644 --- a/src/keri/core/parsing.py +++ b/src/keri/core/parsing.py @@ -1106,8 +1106,11 @@ def msgParsator(self, ims=None, framed=True, pipeline=False, exc.processEvent(tsgs=tsgs, **args) except AttributeError as e: + print(e) raise kering.ValidationError("No Exchange to process so dropped msg" "= {}.".format(serder.pretty())) + except Exception as e: + print(e) elif ilk in (Ilks.vcp, Ilks.vrt, Ilks.iss, Ilks.rev, Ilks.bis, Ilks.brv): # TEL msg diff --git a/src/keri/vc/protocoling.py b/src/keri/vc/protocoling.py index 4878311b3..12f4c991b 100644 --- a/src/keri/vc/protocoling.py +++ b/src/keri/vc/protocoling.py @@ -27,19 +27,17 @@ class IpexHandler: """ - def __init__(self, resource, hby, rgy, notifier): + def __init__(self, resource, hby, notifier): """ Initialize instance Parameters: resource (str): route of messages for this handler hby (Habery): local identifier environment - rgy (Regery): Credential database environment notifier (Notifier): outbound notifications """ self.resource = resource self.hby = hby - self.rgy = rgy self.notifier = notifier def verify(self, serder, attachments=None): @@ -221,7 +219,7 @@ def ipexAgreeExn(hab, message, offer): return exn, ims -def ipexGrantExn(hab, recp, message, acdc, iss, anc, agree=None, dt=None): +def ipexGrantExn(hab, recp, message, acdc, iss=None, anc=None, agree=None, dt=None): """ Disclose an ACDC Parameters: @@ -246,10 +244,14 @@ def ipexGrantExn(hab, recp, message, acdc, iss, anc, agree=None, dt=None): embeds = dict( acdc=acdc, - iss=iss, - anc=anc ) + if iss is not None: + embeds['iss'] = iss + + if anc is not None: + embeds['anc'] = anc + kwa = dict() if agree is not None: kwa['dig'] = agree.said @@ -312,19 +314,18 @@ def ipexSpurnExn(hab, message, spurned): return exn, ims -def loadHandlers(hby, exc, rgy, notifier): +def loadHandlers(hby, exc, notifier): """ Load handlers for the IPEX protocol Parameters: hby (Habery): Database and keystore for environment exc (Exchanger): Peer-to-peer message router - rgy (Regery): Credential database environment notifier (Notifier): outbound notifications """ - exc.addHandler(IpexHandler(resource="/ipex/apply", hby=hby, rgy=rgy, notifier=notifier)) - exc.addHandler(IpexHandler(resource="/ipex/offer", hby=hby, rgy=rgy, notifier=notifier)) - exc.addHandler(IpexHandler(resource="/ipex/agree", hby=hby, rgy=rgy, notifier=notifier)) - exc.addHandler(IpexHandler(resource="/ipex/grant", hby=hby, rgy=rgy, notifier=notifier)) - exc.addHandler(IpexHandler(resource="/ipex/admit", hby=hby, rgy=rgy, notifier=notifier)) - exc.addHandler(IpexHandler(resource="/ipex/spurn", hby=hby, rgy=rgy, notifier=notifier)) + exc.addHandler(IpexHandler(resource="/ipex/apply", hby=hby, notifier=notifier)) + exc.addHandler(IpexHandler(resource="/ipex/offer", hby=hby, notifier=notifier)) + exc.addHandler(IpexHandler(resource="/ipex/agree", hby=hby, notifier=notifier)) + exc.addHandler(IpexHandler(resource="/ipex/grant", hby=hby, notifier=notifier)) + exc.addHandler(IpexHandler(resource="/ipex/admit", hby=hby, notifier=notifier)) + exc.addHandler(IpexHandler(resource="/ipex/spurn", hby=hby, notifier=notifier)) diff --git a/src/keri/vdr/credentialing.py b/src/keri/vdr/credentialing.py index edf61096e..591936bf2 100644 --- a/src/keri/vdr/credentialing.py +++ b/src/keri/vdr/credentialing.py @@ -886,7 +886,7 @@ def sendCredential(hby, hab, reger, postman, creder, recp): hby: hab: reger: - postman: + postman (StreamPoster): poster to stream credential with creder: recp: @@ -898,30 +898,29 @@ def sendCredential(hby, hab, reger, postman, creder, recp): else: sender = hab.pre - sendArtifacts(hby, reger, postman, creder, sender, recp) + sendArtifacts(hby, reger, postman, creder, recp) sources = reger.sources(hby.db, creder) for source, atc in sources: - sendArtifacts(hby, reger, postman, source, sender, recp) - postman.send(src=sender, dest=recp, topic="credential", serder=source, attachment=atc) + sendArtifacts(hby, reger, postman, source, recp) + postman.send(serder=source, attachment=atc) serder, prefixer, seqner, saider = reger.cloneCred(creder.said) atc = bytearray(coring.Counter(coring.CtrDex.SealSourceTriples, count=1).qb64b) atc.extend(prefixer.qb64b) atc.extend(seqner.qb64b) atc.extend(saider.qb64b) - postman.send(src=sender, dest=recp, topic="credential", serder=creder, attachment=atc) + postman.send(serder=creder, attachment=atc) -def sendArtifacts(hby, reger, postman, creder, sender, recp): +def sendArtifacts(hby, reger, postman, creder, recp): """ Stream credential artifacts to recipient using postman Parameters: hby: reger: - postman: + postman (StreamPoster): poster to stream credential with creder: - sender: recp: Returns: @@ -935,35 +934,35 @@ def sendArtifacts(hby, reger, postman, creder, sender, recp): for msg in hby.db.cloneDelegation(ikever): serder = coring.Serder(raw=msg) atc = msg[serder.size:] - postman.send(src=sender, dest=recp, topic="credential", serder=serder, attachment=atc) + postman.send(serder=serder, attachment=atc) for msg in hby.db.clonePreIter(pre=issr): serder = coring.Serder(raw=msg) atc = msg[serder.size:] - postman.send(src=sender, dest=recp, topic="credential", serder=serder, attachment=atc) + postman.send(serder=serder, attachment=atc) if isse != recp: ikever = hby.db.kevers[isse] for msg in hby.db.cloneDelegation(ikever): serder = coring.Serder(raw=msg) atc = msg[serder.size:] - postman.send(src=sender, dest=recp, topic="credential", serder=serder, attachment=atc) + postman.send(serder=serder, attachment=atc) for msg in hby.db.clonePreIter(pre=isse): serder = coring.Serder(raw=msg) atc = msg[serder.size:] - postman.send(src=sender, dest=recp, topic="credential", serder=serder, attachment=atc) + postman.send(serder=serder, attachment=atc) if regk is not None: for msg in reger.clonePreIter(pre=regk): serder = coring.Serder(raw=msg) atc = msg[serder.size:] - postman.send(src=sender, dest=recp, topic="credential", serder=serder, attachment=atc) + postman.send(serder=serder, attachment=atc) for msg in reger.clonePreIter(pre=creder.said): serder = coring.Serder(raw=msg) atc = msg[serder.size:] - postman.send(src=sender, dest=recp, topic="credential", serder=serder, attachment=atc) + postman.send(serder=serder, attachment=atc) def sendRegistry(hby, reger, postman, creder, sender, recp): @@ -977,14 +976,14 @@ def sendRegistry(hby, reger, postman, creder, sender, recp): for msg in hby.db.cloneDelegation(ikever): serder = coring.Serder(raw=msg) atc = msg[serder.size:] - postman.send(src=sender, dest=recp, topic="credential", serder=serder, attachment=atc) + postman.send(serder=serder, attachment=atc) for msg in hby.db.clonePreIter(pre=issr): serder = coring.Serder(raw=msg) atc = msg[serder.size:] - postman.send(src=sender, dest=recp, topic="credential", serder=serder, attachment=atc) + postman.send(serder=serder, attachment=atc) for msg in reger.clonePreIter(pre=regk): serder = coring.Serder(raw=msg) atc = msg[serder.size:] - postman.send(src=sender, dest=recp, topic="credential", serder=serder, attachment=atc) + postman.send(serder=serder, attachment=atc) diff --git a/src/keri/vdr/eventing.py b/src/keri/vdr/eventing.py index a2d1df6c4..481db5527 100644 --- a/src/keri/vdr/eventing.py +++ b/src/keri/vdr/eventing.py @@ -1824,7 +1824,7 @@ def processReplyRegistryTxnState(self, *, serder, saider, route, cigars=None, ts aid=aid, osaider=osaider, cigars=cigars, tsgs=tsgs) if not accepted: - raise kering.UnverifiedReplyError(f"Unverified reply.") + raise kering.UnverifiedReplyError(f"Unverified registry txn state reply.") ldig = self.reger.getTel(key=snKey(pre=regk, sn=sn)) # retrieve dig of last event at sn. @@ -1970,7 +1970,7 @@ def processReplyCredentialTxnState(self, *, serder, saider, route, cigars=None, aid=aid, osaider=osaider, cigars=cigars, tsgs=tsgs) if not accepted: - raise kering.UnverifiedReplyError(f"Unverified reply.") + raise kering.UnverifiedReplyError(f"Unverified credential state reply.") ldig = self.reger.getTel(key=snKey(pre=vci, sn=sn)) # retrieve dig of last event at sn. diff --git a/src/keri/vdr/verifying.py b/src/keri/vdr/verifying.py index afcfcd405..655cbc80f 100644 --- a/src/keri/vdr/verifying.py +++ b/src/keri/vdr/verifying.py @@ -42,7 +42,7 @@ def __init__(self, hby, reger=None, creds=None, cues=None, expiry=36000000000): """ self.hby = hby - self.reger = reger if reger is not None else Reger(name=self.hby.name, temp=True) + self.reger = reger if reger is not None else Reger(name=self.hby.name, temp=self.hby.temp) self.creds = creds if creds is not None else decking.Deck() # subclass of deque self.cues = cues if cues is not None else decking.Deck() # subclass of deque self.CredentialExpiry = expiry diff --git a/tests/app/test_oobiing.py b/tests/app/test_oobiing.py index c901ccab3..c6ee54712 100644 --- a/tests/app/test_oobiing.py +++ b/tests/app/test_oobiing.py @@ -72,31 +72,6 @@ def test_oobi_share(mockHelpingNowUTC): b'p-2QZzIZJ94_9hIP') -def test_oobi_share_endpoint(): - with openMultiSig(prefix="test") as ((hby1, hab1), (hby2, hab2), (hby3, hab3)): - app = falcon.App() - oobiEnd = oobiing.OobiResource(hby=hby1) - app.add_route("/oobi/groups/{alias}/share", oobiEnd, suffix="share") - client = testing.TestClient(app) - - body = dict(oobis=[ - "http://127.0.0.1:3333/oobi", - "http://127.0.0.1:5555/oobi", - "http://127.0.0.1:7777/oobi" - ]) - raw = json.dumps(body).encode("utf-8") - - result = client.simulate_post(path="/oobi/groups/test_1/share", body=raw) - assert result.status == falcon.HTTP_400 - result = client.simulate_post(path="/oobi/groups/fake/share", body=raw) - assert result.status == falcon.HTTP_404 - result = client.simulate_post(path="/oobi/groups/test_group1/share", body=raw) - assert result.status == falcon.HTTP_200 - - # Assert that a message has been send to each participant for each OOBI - assert len(oobiEnd.postman.evts) == 6 - - def test_oobiery(): with habbing.openHby(name="oobi") as hby: hab = hby.makeHab(name="oobi") @@ -198,11 +173,11 @@ def test_authenticator(mockHelpingNowUTC): hby.db.woobi.pin(keys=(url,), val=obr) app = falcon.App() # falcon.App instances are callable WSGI apps - endDoers = oobiing.loadEnds(app, hby=hby) + oobiing.loadEnds(app, hby=hby) limit = 2.0 tock = 0.03125 - doers = endDoers + authn.doers + doers = authn.doers doist = doing.Doist(limit=limit, tock=tock) doist.do(doers=doers) diff --git a/tests/end/test_ending.py b/tests/end/test_ending.py index 204f92914..5ce366def 100644 --- a/tests/end/test_ending.py +++ b/tests/end/test_ending.py @@ -431,11 +431,8 @@ def test_get_oobi(): rep = client.simulate_get('/oobi', ) assert rep.status == falcon.HTTP_OK serder = coring.Serder(raw=rep.text.encode("utf-8")) - assert serder.ked['t'] == coring.Ilks.rpy - assert serder.ked['r'] == "/loc/scheme" - assert serder.ked['a']['eid'] == hab.pre - assert serder.ked['a']['scheme'] == kering.Schemes.http - assert serder.ked['a']['url'] == "http://127.0.0.1:5555" + assert serder.ked['t'] == coring.Ilks.icp + assert serder.ked['i'] == "EOaICQwhOy3wMwecjAuHQTbv_Cmuu1azTMnHi4QtUmEU" """Done Test""" diff --git a/tests/vc/test_protocoling.py b/tests/vc/test_protocoling.py index c19cbee29..30ea3eb06 100644 --- a/tests/vc/test_protocoling.py +++ b/tests/vc/test_protocoling.py @@ -46,7 +46,7 @@ def test_ipex(seeder, mockCoringRandomNonce, mockHelpingNowIso8601, mockHelpingN sidRgy.processEscrows() sidExc = exchanging.Exchanger(hby=sidHby, handlers=[]) - protocoling.loadHandlers(hby=sidHby, exc=sidExc, rgy=sidRgy, notifier=notifier) + protocoling.loadHandlers(hby=sidHby, exc=sidExc, notifier=notifier) schema = "EMQWEcCnVRk1hatTNyK3sIykYSrrFvafX3bHQ9Gkk1kC" @@ -104,7 +104,7 @@ def test_ipex(seeder, mockCoringRandomNonce, mockHelpingNowIso8601, mockHelpingN # Successfully parsed credential is now saved in database. assert sidVer.reger.saved.get(keys=(creder.said,)) is not None - ipexhan = protocoling.IpexHandler(resource="/ipex/apply", hby=sidHby, rgy=sidRgy, notifier=notifier) + ipexhan = protocoling.IpexHandler(resource="/ipex/apply", hby=sidHby, notifier=notifier) apply0, apply0atc = protocoling.ipexApplyExn(sidHab, message="Please give me a credential", schema=schema, recp=redPre, attrs={})