From ccae22a4a1e1501f2f7e60ee0aa70c90c2031627 Mon Sep 17 00:00:00 2001 From: pfeairheller Date: Tue, 19 Sep 2023 11:50:46 -0700 Subject: [PATCH 1/5] Multisig credential creation and IPEX interactions for GRANT and ADMIT Signed-off-by: pfeairheller --- .../multisig-issuer-interactive.sh | 98 +++++++ scripts/demo/credentials/multisig-issuer.sh | 45 ++-- src/keri/app/agenting.py | 19 +- src/keri/app/cli/commands/escrow.py | 1 - src/keri/app/cli/commands/ipex/admit.py | 28 +- src/keri/app/cli/commands/ipex/grant.py | 2 +- src/keri/app/cli/commands/ipex/list.py | 3 +- src/keri/app/cli/commands/multisig/join.py | 244 ++++++++++++++++-- src/keri/app/cli/commands/passcode/remove.py | 1 - src/keri/app/cli/commands/passcode/set.py | 1 - src/keri/app/cli/commands/vc/create.py | 58 ++++- src/keri/app/cli/commands/vc/list.py | 3 - .../app/cli/commands/vc/registry/incept.py | 14 +- src/keri/app/cli/commands/vc/registry/list.py | 1 - src/keri/app/grouping.py | 37 ++- src/keri/core/eventing.py | 2 - src/keri/core/parsing.py | 1 - src/keri/peer/exchanging.py | 57 +++- src/keri/vc/protocoling.py | 4 +- src/keri/vdr/credentialing.py | 68 ++--- src/keri/vdr/eventing.py | 9 +- src/keri/vdr/verifying.py | 2 +- 22 files changed, 570 insertions(+), 128 deletions(-) create mode 100755 scripts/demo/credentials/multisig-issuer-interactive.sh diff --git a/scripts/demo/credentials/multisig-issuer-interactive.sh b/scripts/demo/credentials/multisig-issuer-interactive.sh new file mode 100755 index 000000000..9c3615ea7 --- /dev/null +++ b/scripts/demo/credentials/multisig-issuer-interactive.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# To run this script you need to run the following command in separate terminals: +# > kli witness demo +# and from the vLEI repo run: +# > vLEI-server -s ./schema/acdc -c ./samples/acdc/ -o ./samples/oobis/ +# + +# Create local environments for multisig group +kli init --name multisig1 --salt 0ACDEyMzQ1Njc4OWxtbm9aBc --nopasscode --config-dir ${KERI_SCRIPT_DIR} --config-file demo-witness-oobis +kli incept --name multisig1 --alias multisig1 --file ${KERI_DEMO_SCRIPT_DIR}/data/multisig-1-sample.json + +# Incept both local identifiers for multisig group +kli init --name multisig2 --salt 0ACDEyMzQ1Njc4OWdoaWpsaw --nopasscode --config-dir ${KERI_SCRIPT_DIR} --config-file demo-witness-oobis +kli incept --name multisig2 --alias multisig2 --file ${KERI_DEMO_SCRIPT_DIR}/data/multisig-2-sample.json + +# Exchange OOBIs between multisig group +kli oobi resolve --name multisig1 --oobi-alias multisig2 --oobi http://127.0.0.1:5642/oobi/EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1/witness +kli oobi resolve --name multisig2 --oobi-alias multisig1 --oobi http://127.0.0.1:5642/oobi/EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4/witness + +# Create the identifier to which the credential will be issued +kli init --name holder --salt 0ACDEyMzQ1Njc4OWxtbm9qWc --nopasscode --config-dir ${KERI_SCRIPT_DIR} --config-file demo-witness-oobis +kli incept --name holder --alias holder --file ${KERI_DEMO_SCRIPT_DIR}/data/gleif-sample.json + +# Introduce multisig to Holder +kli oobi resolve --name holder --oobi-alias multisig2 --oobi http://127.0.0.1:5642/oobi/EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1/witness +kli oobi resolve --name holder --oobi-alias multisig1 --oobi http://127.0.0.1:5642/oobi/EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4/witness + +# Introduce the holder to all participants in the multisig group +kli oobi resolve --name multisig1 --oobi-alias holder --oobi http://127.0.0.1:5642/oobi/ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k/witness +kli oobi resolve --name multisig2 --oobi-alias holder --oobi http://127.0.0.1:5642/oobi/ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k/witness + +# Load Data OOBI for schema of credential to issue +kli oobi resolve --name multisig1 --oobi-alias vc --oobi http://127.0.0.1:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao +kli oobi resolve --name multisig2 --oobi-alias vc --oobi http://127.0.0.1:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao +kli oobi resolve --name holder --oobi-alias vc --oobi http://127.0.0.1:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao + +# Run the follow in parallel and wait for the group to be created: +kli multisig incept --name multisig1 --alias multisig1 --group multisig --file ${KERI_DEMO_SCRIPT_DIR}/data/multisig-sample.json & +pid=$! +PID_LIST+=" $pid" + +kli multisig incept --name multisig2 --alias multisig2 --group multisig --file ${KERI_DEMO_SCRIPT_DIR}/data/multisig-sample.json & +pid=$! +PID_LIST+=" $pid" + +wait $PID_LIST +kli oobi resolve --name holder --oobi-alias multisig --oobi http://127.0.0.1:5642/oobi/EC61gZ9lCKmHAS7U5ehUfEbGId5rcY0D7MirFZHDQcE2/witness + +# Create a credential registry owned by the multisig issuer +kli vc registry incept --name multisig1 --alias multisig --registry-name vLEI --usage "Issue vLEIs" --nonce AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s & +pid=$! +PID_LIST=" $pid" + +kli vc registry incept --name multisig2 --alias multisig --registry-name vLEI --usage "Issue vLEIs" --nonce AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s & +pid=$! +PID_LIST+=" $pid" + +wait $PID_LIST + +# Issue Credential +kli vc create --name multisig1 --alias multisig --registry-name vLEI --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k --data @${KERI_DEMO_SCRIPT_DIR}/data/credential-data.json & +pid=$! +PID_LIST+=" $pid" + +# Wait for 3 seconds to allow credential.json to be created, but still launch in parallel because they will wait for each other +echo "Multisig2 looking to join" +kli multisig join --name multisig2 + +wait $PID_LIST + +SAID=$(kli vc list --name multisig1 --alias multisig --issued --said) + +kli ipex grant --name multisig1 --alias multisig --said "${SAID}" --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k & +pid=$! +PID_LIST+=" $pid" + +kli multisig join --name multisig2 + +echo "Polling for holder's IPEX message..." +SAID=$(kli ipex list --name holder --alias holder --poll --said) + +echo "Admitting GRANT ${SAID}" +kli ipex admit --name holder --alias holder --said "${SAID}" + +kli vc list --name holder --alias holder --poll + +exit 0 +SAID=$(kli vc list --name holder --alias holder --said --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao) + +echo "Revoking ${SAID}..." +kli vc revoke --name multisig2 --alias multisig --registry-name vLEI --said "${SAID}"& +pid=$! +PID_LIST+=" $pid" + +kli multisig join --name multisig2 + +wait $PID_LIST +kli vc list --name holder --alias holder --poll diff --git a/scripts/demo/credentials/multisig-issuer.sh b/scripts/demo/credentials/multisig-issuer.sh index fd2cad9e8..30f6f2e6d 100755 --- a/scripts/demo/credentials/multisig-issuer.sh +++ b/scripts/demo/credentials/multisig-issuer.sh @@ -47,43 +47,56 @@ wait $PID_LIST kli oobi resolve --name holder --oobi-alias multisig --oobi http://127.0.0.1:5642/oobi/EC61gZ9lCKmHAS7U5ehUfEbGId5rcY0D7MirFZHDQcE2/witness # Create a credential registry owned by the multisig issuer -kli vc registry incept --name multisig1 --alias multisig --registry-name vLEI --nonce AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s & +kli vc registry incept --name multisig1 --alias multisig --registry-name vLEI --usage "Issue vLEIs" --nonce AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s & pid=$! PID_LIST=" $pid" -kli vc registry incept --name multisig2 --alias multisig --registry-name vLEI --nonce AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s & +kli vc registry incept --name multisig2 --alias multisig --registry-name vLEI --usage "Issue vLEIs" --nonce AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s & pid=$! PID_LIST+=" $pid" wait $PID_LIST -# Rotate multisig keys: -kli multisig rotate --name multisig1 --alias multisig & -pid=$! -PID_LIST=" $pid" -kli multisig rotate --name multisig2 --alias multisig & +# Lets not complicate things with rotation just yet +## Rotate multisig keys: +#kli multisig rotate --name multisig1 --alias multisig & +#pid=$! +#PID_LIST=" $pid" +# +#kli multisig rotate --name multisig2 --alias multisig & +#pid=$! +#PID_LIST+=" $pid" +# +#wait $PID_LIST + +# Issue Credential +kli vc create --name multisig1 --alias multisig --registry-name vLEI --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k --data @${KERI_DEMO_SCRIPT_DIR}/data/credential-data.json & pid=$! PID_LIST+=" $pid" +# Wait for 3 seconds to allow credential.json to be created, but still launch in parallel because they will wait for each other +echo "Multisig2 looking to join" +kli multisig join --name multisig2 + wait $PID_LIST +SAID=$(kli vc list --name multisig1 --alias multisig --issued --said) -# Issue Credential -kli vc issue --name multisig1 --alias multisig --registry-name vLEI --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k --data @${KERI_DEMO_SCRIPT_DIR}/data/credential-data.json & +kli ipex grant --name multisig1 --alias multisig --said "${SAID}" --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k & pid=$! PID_LIST+=" $pid" -# Wait for 3 seconds to allow credential.json to be created, but still launch in parallel because they will wait for each other -sleep 3 -kli vc issue --name multisig2 --alias multisig --credential @./credential.json & -pid=$! -PID_LIST+=" $pid" +kli multisig join --name multisig2 -wait $PID_LIST +echo "Polling for holder's IPEX message..." +SAID=$(kli ipex list --name holder --alias holder --poll --said) + +echo "Admitting GRANT ${SAID}" +kli ipex admit --name holder --alias holder --said "${SAID}" kli vc list --name holder --alias holder --poll -SAID=`kli vc list --name holder --alias holder --said --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao` +SAID=$(kli vc list --name holder --alias holder --said --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao) echo "Revoking ${SAID}..." TIME=$(date -Iseconds -u) diff --git a/src/keri/app/agenting.py b/src/keri/app/agenting.py index d2f08dd79..0ca1b98e0 100644 --- a/src/keri/app/agenting.py +++ b/src/keri/app/agenting.py @@ -456,7 +456,7 @@ def msgDo(self, tymth=None, tock=1.0, **opts): src = evt["src"] r = evt["r"] q = evt["q"] - wits = evt["wits"] + wits = evt["wits"] if "wits" in evt else None if "hab" in evt: hab = evt["hab"] @@ -534,9 +534,9 @@ def query(self, pre, r="logs", sn=0, src=None, hab=None, anchor=None, wits=None, self.msgs.append(msg) - def telquery(self, src, ri, i=None, r="tels", **kwa): + def telquery(self, src, ri, i=None, r="tels", wits=None, **kwa): qry = dict(ri=ri) - self.msgs.append(dict(src=src, pre=i, r=r, q=qry)) + self.msgs.append(dict(src=src, pre=i, r=r, wits=wits, q=qry)) class WitnessPublisher(doing.DoDoer): @@ -611,6 +611,19 @@ def sendDo(self, tymth=None, tock=0.0, **opts): yield self.tock + def sent(self, said): + """ Check if message with given SAID was sent + + Parameters: + said (str): qb64 SAID of message to check for + """ + + for cue in self.cues: + if cue["said"] == said: + return True + + return False + class TCPMessenger(doing.DoDoer): """ Send events to witnesses for receipting using TCP direct connection diff --git a/src/keri/app/cli/commands/escrow.py b/src/keri/app/cli/commands/escrow.py index e562fe161..e64e8dd4e 100644 --- a/src/keri/app/cli/commands/escrow.py +++ b/src/keri/app/cli/commands/escrow.py @@ -155,6 +155,5 @@ def escrows(tymth, tock=0.0, **opts): pass except ConfigurationError as e: - print(e) print(f"identifier prefix for {name} does not exist, incept must be run first", ) return -1 diff --git a/src/keri/app/cli/commands/ipex/admit.py b/src/keri/app/cli/commands/ipex/admit.py index fb574a4b9..a06618874 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 +from keri.app import forwarding, connecting, habbing, grouping, indirecting, agenting from keri.app.cli.common import existing from keri.app.notifying import Notifier from keri.core import parsing, coring, eventing @@ -53,12 +53,13 @@ def __init__(self, name, alias, base, bran, said, message): 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) - kvy = eventing.Kevery(db=self.hby.db) - tvy = teventing.Tevery(db=self.hby.db, reger=self.rgy.reger) - vry = verifying.Verifier(hby=self.hby, reger=self.rgy.reger) + self.kvy = eventing.Kevery(db=self.hby.db) + self.tvy = teventing.Tevery(db=self.hby.db, reger=self.rgy.reger) + self.vry = verifying.Verifier(hby=self.hby, reger=self.rgy.reger) - self.psr = parsing.Parser(kvy=kvy, tvy=tvy, vry=vry) + self.psr = parsing.Parser(kvy=self.kvy, tvy=self.tvy, vry=self.vry) notifier = Notifier(self.hby) mux = grouping.Multiplexor(self.hby, notifier=notifier) @@ -69,9 +70,9 @@ def __init__(self, name, alias, base, bran, said, message): mbx = indirecting.MailboxDirector(hby=self.hby, topics=["/receipt", "/multisig", "/replay", "/credential"], - exc=self.exc) + exc=self.exc, kvy=self.kvy, tvy=self.tvy, verifier=self.vry) - self.toRemove = [self.postman, mbx] + self.toRemove = [self.postman, mbx, self.witq] super(AdmitDoer, self).__init__(doers=self.toRemove + [doing.doify(self.admitDo)]) def admitDo(self, tymth, tock=0.0): @@ -99,6 +100,13 @@ def admitDo(self, tymth, tock=0.0): raise ValueError(f"exn said={self.said} is not a grant message, route={route}") embeds = grant.ked['e'] + acdc = embeds["acdc"] + issr = acdc['i'] + + # Lets get the latest KEL and Registry if needed + self.witq.query(src=self.hab.pre, pre=issr) + if "ri" in acdc: + self.witq.telquery(src=self.hab.pre, wits=self.hab.kevers[issr].wits, ri=acdc["ri"], i=acdc["d"]) for label in ("anc", "iss", "acdc"): ked = embeds[label] @@ -106,7 +114,7 @@ def admitDo(self, tymth, tock=0.0): ims = bytearray(sadder.raw) + pathed[label] self.psr.parseOne(ims=ims) - said = embeds["acdc"]["d"] + said = acdc["d"] while not self.rgy.reger.saved.get(keys=said): yield self.tock @@ -134,14 +142,14 @@ def admitDo(self, tymth, tock=0.0): yield self.tock if self.exc.lead(self.hab, said=exn.said): - print("Sending admit message...") + print(f"Sending admit message to {recp}") self.postman.send(src=self.hab.pre, dest=recp, topic="credential", serder=exn, attachment=atc) - while not self.postman.cues: + while not self.postman.sent(exn.said): 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 751e9d00f..ebf135b16 100644 --- a/src/keri/app/cli/commands/ipex/grant.py +++ b/src/keri/app/cli/commands/ipex/grant.py @@ -113,7 +113,7 @@ def grantDo(self, tymth, tock=0.0): anchor=dict(i=iserder.pre, s=seqner.snh, d=iserder.said)) anc = self.hby.db.cloneEvtMsg(pre=serder.pre, fn=0, dig=serder.said) - exn, atc = protocoling.ipexGrantExn(hab=self.hab, message=self.message, acdc=acdc, iss=iss, anc=anc) + exn, atc = protocoling.ipexGrantExn(hab=self.hab, recp=recp, message=self.message, acdc=acdc, iss=iss, anc=anc) msg = bytearray(exn.raw) msg.extend(atc) diff --git a/src/keri/app/cli/commands/ipex/list.py b/src/keri/app/cli/commands/ipex/list.py index 049a8b45d..b26ce7534 100644 --- a/src/keri/app/cli/commands/ipex/list.py +++ b/src/keri/app/cli/commands/ipex/list.py @@ -241,8 +241,7 @@ def admit(self, note, exn, pathed): print(f"Credential {sad['d']}:") print(f" Type: {schemer.sed['title']}") - print( - f" Status: Accepted {terming.Colors.OKGREEN}{terming.Symbols.CHECKMARK}{terming.Colors.ENDC}") + print(f" Status: Accepted {terming.Colors.OKGREEN}{terming.Symbols.CHECKMARK}{terming.Colors.ENDC}") def deleteNote(self, keys): yn = input(f"\n Delete the notification [Y|n]?") diff --git a/src/keri/app/cli/commands/multisig/join.py b/src/keri/app/cli/commands/multisig/join.py index 86a9db4e4..0ae83030c 100644 --- a/src/keri/app/cli/commands/multisig/join.py +++ b/src/keri/app/cli/commands/multisig/join.py @@ -10,11 +10,13 @@ from hio.base import doing from prettytable import PrettyTable -from keri import help -from keri.app import habbing, indirecting, agenting, notifying, grouping, connecting +from keri import help, kering +from keri.app import habbing, indirecting, agenting, notifying, grouping, connecting, forwarding from keri.app.cli.common import existing, displaying -from keri.core import coring, eventing +from keri.core import coring, eventing, scheming, parsing from keri.peer import exchanging +from keri.vc import proving +from keri.vdr import verifying, credentialing logger = help.ogler.getLogger() @@ -58,23 +60,32 @@ def __init__(self, name, base, bran): bran (str): passcode to unlock keystore """ - hby = existing.setupHby(name=name, base=base, bran=bran) - self.hbyDoer = habbing.HaberyDoer(habery=hby) # setup doer - self.witq = agenting.WitnessInquisitor(hby=hby) - self.org = connecting.Organizer(hby=hby) - self.notifier = notifying.Notifier(hby=hby) - self.exc = exchanging.Exchanger(hby=hby, handlers=[]) - mux = grouping.Multiplexor(hby=hby, notifier=self.notifier) + self.hby = existing.setupHby(name=name, base=base, bran=bran) + self.rgy = credentialing.Regery(hby=self.hby, name=name, base=base) + self.hbyDoer = habbing.HaberyDoer(habery=self.hby) # setup doer + self.witq = agenting.WitnessInquisitor(hby=self.hby) + self.org = connecting.Organizer(hby=self.hby) + self.notifier = notifying.Notifier(hby=self.hby) + self.exc = exchanging.Exchanger(hby=self.hby, handlers=[]) + self.verifier = verifying.Verifier(hby=self.hby, reger=self.rgy.reger) + self.psr = parsing.Parser(kvy=self.hby.kvy, tvy=self.rgy.tvy, vry=self.verifier, exc=self.exc) + + mux = grouping.Multiplexor(hby=self.hby, notifier=self.notifier) grouping.loadHandlers(exc=self.exc, mux=mux) - self.counselor = grouping.Counselor(hby=hby) - self.mbx = indirecting.MailboxDirector(hby=hby, exc=self.exc, topics=['/receipt', '/multisig', '/replay', - '/delegate']) + self.counselor = grouping.Counselor(hby=self.hby) - doers = [self.hbyDoer, self.witq, self.mbx, self.counselor] + self.registrar = credentialing.Registrar(hby=self.hby, rgy=self.rgy, counselor=self.counselor) + self.credentialer = credentialing.Credentialer(hby=self.hby, rgy=self.rgy, registrar=self.registrar, + verifier=self.verifier) + + self.mbx = indirecting.MailboxDirector(hby=self.hby, exc=self.exc, topics=['/receipt', '/multisig', '/replay', + '/delegate']) + self.postman = forwarding.Poster(hby=self.hby) + + doers = [self.hbyDoer, self.witq, self.mbx, self.counselor, self.registrar, self.credentialer, self.postman] self.toRemove = list(doers) doers.extend([doing.doify(self.confirmDo)]) - self.hby = hby super(ConfirmDoer, self).__init__(doers=doers) def confirmDo(self, tymth, tock=0.0): @@ -140,6 +151,28 @@ def confirmDo(self, tymth, tock=0.0): if delete in ("Y", "y"): self.notifier.noter.notes.rem(keys=keys) + elif route == '/multisig/iss': + + done = yield from self.iss(attrs) + if done: + self.notifier.noter.notes.rem(keys=keys) + + else: + delete = input(f"\nDelete event [Y|n]? ") + if delete in ("Y", "y"): + self.notifier.noter.notes.rem(keys=keys) + + elif route == '/multisig/exn': + + done = yield from self.exn(attrs) + if done: + self.notifier.noter.notes.rem(keys=keys) + + else: + delete = input(f"\nDelete event [Y|n]? ") + if delete in ("Y", "y"): + self.notifier.noter.notes.rem(keys=keys) + yield self.tock yield self.tock @@ -200,14 +233,13 @@ def incept(self, attrs): prefixer = coring.Prefixer(qb64=ghab.pre) seqner = coring.Seqner(sn=0) saider = coring.Saider(qb64=prefixer.qb64) - yield from self.startCounselor(smids, rmids, ghab, prefixer, seqner, saider) + yield from self.startCounselor(ghab, prefixer, seqner, saider) print() displaying.printIdentifier(self.hby, ghab.pre) return True - def interact(self, attrs): pre = attrs["gid"] smids = attrs["aids"] # change attrs["aids"]" to "smids" @@ -335,7 +367,7 @@ def rotate(self, attrs): prefixer = coring.Prefixer(qb64=ghab.pre) seqner = coring.Seqner(sn=serder.sn) - yield from self.startCounselor(smids, rmids, ghab, prefixer, seqner, serder.saider) + yield from self.startCounselor(ghab, prefixer, seqner, serder.saider) print() displaying.printIdentifier(self.hby, ghab.pre) @@ -376,7 +408,6 @@ def showRotation(self, hab, smids, rmids, ked): print(tab) - def printMemberTable(self, mids, hab, thold): tab = PrettyTable() fields = ["Local", "Name", "AID"] @@ -415,4 +446,179 @@ def rpy(self, attrs): """ ked = attrs["ked"] + def iss(self, attrs): + """ Handle issue messages + + Parameters: + attrs (dict): attributes of the reply message + + Returns: + + """ + said = attrs["d"] + exn, pathed = exchanging.cloneMessage(self.hby, said=said) + + sender = exn.ked['i'] + + contact = self.org.get(sender) + senderAlias = contact['alias'] + + embeds = exn.ked['e'] + acdc = embeds["acdc"] + schema = acdc['s'] + scraw = self.verifier.resolver.resolve(schema) + if not scraw: + raise kering.ConfigurationError("Credential schema {} not found".format(schema)) + + schemer = scheming.Schemer(raw=scraw) + + issr = acdc["i"] + hab = self.hby.habs[issr] if issr in self.hby.habs else None + if hab is None: + raise ValueError(f"credential issuer not a valid AID={issr}") + + print(f"\nGroup Credential Issuance Proposed (from {senderAlias}):") + print(f"Credential {acdc['d']}:") + print(f" Type: {schemer.sed['title']}") + print(f" Issued By: {hab.name} ({hab.pre})") + + if "i" in acdc["a"]: + isse = acdc['a']['i'] + contact = self.org.get(isse) + if contact is not None and "alias" in contact: + print(f" Issued To: {contact['alias']} ({isse})") + else: + print(f" Issued To: Unknown AID ({isse})") + + print(" Data:") + for k, v in acdc['a'].items(): + if k not in ('d', 'i'): + print(f" {k}: {v}") + + yn = input(f"\nApprove [Y|n]? ") + approve = yn in ('', 'y', 'Y') + + if approve: + # Create and parse the event with "their" signatures + anc = embeds["anc"] + aserder = coring.Serder(ked=anc) + anc = bytearray(aserder.raw) + pathed["anc"] + self.psr.parseOne(ims=bytes(anc)) + + # Now sign the event and parse it with our signatures + sigers = hab.sign(aserder.raw) + anc = eventing.messagize(serder=aserder, sigers=sigers) + self.psr.parseOne(ims=bytes(anc)) + + iss = embeds["iss"] + iserder = coring.Serder(ked=iss) + try: + self.rgy.tvy.processEvent(serder=iserder) + except kering.MissingAnchorError: + pass + + acdc = embeds["acdc"] + creder = proving.Creder(ked=acdc) + acdc = bytearray(creder.raw) + pathed["acdc"] + self.psr.parseOne(ims=bytes(acdc)) + + self.credentialer.issue(creder, iserder) + self.registrar.issue(creder, iserder, aserder) + + smids = hab.db.signingMembers(pre=hab.pre) + smids.remove(hab.mhab.pre) + + for recp in smids: # this goes to other participants only as a signaling mechanism + exn, atc = grouping.multisigIssueExn(ghab=hab, acdc=acdc, iss=iserder.raw, anc=anc) + self.postman.send(src=hab.mhab.pre, + dest=recp, + topic="multisig", + serder=exn, + attachment=atc) + + while not self.credentialer.complete(said=creder.said): + self.rgy.processEscrows() + self.verifier.processEscrows() + yield self.tock + + print(f"Credential {creder.said} complete.") + + yield self.tock + + def exn(self, attrs): + """ Handle exn messages + + Parameters: + attrs (dict): attributes of the reply message + + Returns: + + """ + said = attrs["d"] + exn, pathed = exchanging.cloneMessage(self.hby, said=said) + embeds = exn.ked['e'] + sender = exn.ked['i'] + + contact = self.org.get(sender) + senderAlias = contact['alias'] + + eexn = embeds['exn'] + + group = eexn["i"] + hab = self.hby.habs[group] if group in self.hby.habs else None + if hab is None: + raise ValueError(f"message sender not a valid AID={group}") + + print(f"Group Peer-2-Peer Message proposal (from {senderAlias}):") + print(f" Message Type: {eexn['r']}") + print(f" Sending From: {hab.name} ({hab.pre})") + recp = eexn['a']['i'] + contact = self.org.get(recp) + if contact is not None and "alias" in contact: + print(f" Sending To: {contact['alias']} ({recp})") + else: + print(f" Sending To: Unknown AID ({recp})") + + yn = input(f"\nApprove [Y|n]? ") + approve = yn in ('', 'y', 'Y') + + if approve: + eserder = coring.Serder(ked=eexn) + anc = bytearray(eserder.raw) + pathed["exn"] + self.psr.parseOne(ims=bytes(anc)) + + msg = hab.endorse(serder=eserder, last=False, pipelined=False) + msg = msg + pathed["exn"] + self.psr.parseOne(ims=bytes(msg)) + + smids = hab.db.signingMembers(pre=hab.pre) + smids.remove(hab.mhab.pre) + + for smid in smids: # this goes to other participants only as a signaling mechanism + rexn, atc = grouping.multisigExn(ghab=hab, exn=msg) + self.postman.send(src=hab.mhab.pre, + dest=smid, + topic="multisig", + serder=rexn, + attachment=atc) + + while not self.exc.complete(said=eserder.said): + self.exc.processEscrow() + yield self.tock + + if self.exc.lead(hab.mhab, said=exn.said): + print(f"Sending grant message {eserder.said} to {recp}") + atc = exchanging.serializeMessage(self.hby, eserder.said) + del atc[:eserder.size] + self.postman.send(src=hab.mhab.pre, + dest=recp, + topic="credential", + serder=eserder, + attachment=atc) + + while not self.postman.sent(said=eserder.said): + yield self.tock + + print("... grant message sent") + yield self.tock diff --git a/src/keri/app/cli/commands/passcode/remove.py b/src/keri/app/cli/commands/passcode/remove.py index 31f96e093..82b6b04f7 100644 --- a/src/keri/app/cli/commands/passcode/remove.py +++ b/src/keri/app/cli/commands/passcode/remove.py @@ -47,6 +47,5 @@ def remove(tymth, tock=0.0, **opts): print("Passcode removed and keystore unencrypted.") except ConfigurationError as e: - print(e) print(f"identifier prefix for {name} does not exist, incept must be run first", ) return -1 diff --git a/src/keri/app/cli/commands/passcode/set.py b/src/keri/app/cli/commands/passcode/set.py index 287cf9336..a31f978bb 100644 --- a/src/keri/app/cli/commands/passcode/set.py +++ b/src/keri/app/cli/commands/passcode/set.py @@ -70,6 +70,5 @@ def set_passcode(tymth, tock=0.0, **opts): print("Passcode reset and keystore re-encrypted.") except ConfigurationError as e: - print(e) print(f"identifier prefix for {name} does not exist, incept must be run first", ) return -1 diff --git a/src/keri/app/cli/commands/vc/create.py b/src/keri/app/cli/commands/vc/create.py index bb2299b3f..fc9a4d5eb 100644 --- a/src/keri/app/cli/commands/vc/create.py +++ b/src/keri/app/cli/commands/vc/create.py @@ -5,8 +5,11 @@ from hio.base import doing from keri import kering -from keri.app import indirecting, habbing, grouping, connecting +from keri.app import indirecting, habbing, grouping, connecting, forwarding, signing, notifying from keri.app.cli.common import existing +from keri.core import coring, eventing +from keri.help import helping +from keri.peer import exchanging from keri.vc import proving from keri.vdr import credentialing, verifying @@ -127,17 +130,26 @@ def __init__(self, name, alias, base, bran, registryName=None, schema=None, edge """ self.name = name - self.alias = alias + self.registryName = registryName self.hby = existing.setupHby(name=name, base=base, bran=bran) + self.hab = self.hby.habByName(alias) + if self.hab is None: + raise ValueError(f"invalid alias {alias}") + self.rgy = credentialing.Regery(hby=self.hby, name=name, base=base) self.hbyDoer = habbing.HaberyDoer(habery=self.hby) # setup doer self.counselor = grouping.Counselor(hby=self.hby) self.registrar = credentialing.Registrar(hby=self.hby, rgy=self.rgy, counselor=self.counselor) self.org = connecting.Organizer(hby=self.hby) + self.postman = forwarding.Poster(hby=self.hby) + notifier = notifying.Notifier(self.hby) + mux = grouping.Multiplexor(self.hby, notifier=notifier) + exc = exchanging.Exchanger(hby=self.hby, handlers=[]) + grouping.loadHandlers(exc, mux) self.verifier = verifying.Verifier(hby=self.hby, reger=self.rgy.reger) mbx = indirecting.MailboxDirector(hby=self.hby, topics=["/receipt", "/multisig", "/credential"], - verifier=self.verifier) + verifier=self.verifier, exc=exc) self.credentialer = credentialing.Credentialer(hby=self.hby, rgy=self.rgy, registrar=self.registrar, verifier=self.verifier) @@ -164,13 +176,11 @@ def __init__(self, name, alias, base, bran, registryName=None, schema=None, edge self.creder = proving.Creder(ked=credential) self.credentialer.validate(creder=self.creder) - self.credentialer.issue(creder=self.creder) - except kering.ConfigurationError as e: print(f"error issuing credential {e}") return - doers = [self.hbyDoer, mbx, self.counselor, self.registrar, self.credentialer] + doers = [self.hbyDoer, mbx, self.counselor, self.registrar, self.credentialer, self.postman] self.toRemove = list(doers) doers.extend([doing.doify(self.createDo)]) @@ -189,6 +199,42 @@ def createDo(self, tymth, tock=0.0): self.tock = tock _ = (yield self.tock) + registry = self.rgy.registryByName(self.registryName) + hab = registry.hab + + dt = self.creder.subject["dt"] if "dt" in self.creder.subject else helping.nowIso8601() + iserder = registry.issue(said=self.creder.said, dt=dt) + + vcid = iserder.ked["i"] + rseq = coring.Seqner(snh=iserder.ked["s"]) + rseal = eventing.SealEvent(vcid, rseq.snh, iserder.said) + rseal = dict(i=rseal.i, s=rseal.s, d=rseal.d) + + if registry.estOnly: + anc = hab.rotate(data=[rseal]) + + else: + anc = hab.interact(data=[rseal]) + + aserder = coring.Serder(raw=anc) + self.credentialer.issue(self.creder, iserder) + self.registrar.issue(self.creder, iserder, aserder) + + acdc = signing.serialize(self.creder, coring.Prefixer(qb64=iserder.pre), coring.Seqner(sn=iserder.sn), + iserder.saider) + + if isinstance(self.hab, habbing.GroupHab): + smids = self.hab.db.signingMembers(pre=self.hab.pre) + smids.remove(self.hab.mhab.pre) + + for recp in smids: # this goes to other participants only as a signaling mechanism + exn, atc = grouping.multisigIssueExn(ghab=self.hab, acdc=acdc, iss=iserder.raw, anc=anc) + self.postman.send(src=self.hab.mhab.pre, + dest=recp, + topic="multisig", + serder=exn, + attachment=atc) + while not self.credentialer.complete(said=self.creder.said): self.rgy.processEscrows() yield self.tock diff --git a/src/keri/app/cli/commands/vc/list.py b/src/keri/app/cli/commands/vc/list.py index 2b65cbe98..ee50bb12a 100644 --- a/src/keri/app/cli/commands/vc/list.py +++ b/src/keri/app/cli/commands/vc/list.py @@ -150,7 +150,4 @@ def listDo(self, tymth, tock=0.0): for line in bsad.splitlines(): print(f"\t{line}") - else: - print("None\n") - self.remove([self.mbx]) diff --git a/src/keri/app/cli/commands/vc/registry/incept.py b/src/keri/app/cli/commands/vc/registry/incept.py index 4c869bbea..e18f5b951 100644 --- a/src/keri/app/cli/commands/vc/registry/incept.py +++ b/src/keri/app/cli/commands/vc/registry/incept.py @@ -32,6 +32,9 @@ parser.add_argument('--alias', '-a', help='human readable alias for the new identifier prefix', required=True) parser.add_argument('--passcode', '-p', help='22 character encryption passcode for keystore (is not saved)', dest="bran", default=None) # passcode => bran +parser.add_argument('--usage', '-u', help='For multisig issuers, a message to other participants about how this' + ' registry is to be used', + default=None) def registryIncept(args): @@ -44,13 +47,14 @@ def registryIncept(args): estOnly = args.establishment_only noBackers = args.no_backers backers = args.backers + usage = args.usage if noBackers and backers: print("--no-backers and --backers can not both be provided") return -1 icpDoer = RegistryInceptor(name=name, base=base, alias=alias, bran=bran, registryName=registryName, - nonce=nonce, estOnly=estOnly, noBackers=noBackers, baks=backers) + nonce=nonce, estOnly=estOnly, noBackers=noBackers, baks=backers, usage=usage) doers = [icpDoer] return doers @@ -61,7 +65,7 @@ class RegistryInceptor(doing.DoDoer): """ - def __init__(self, name, base, alias, bran, registryName, **kwa): + def __init__(self, name, base, alias, bran, registryName, usage, **kwa): """ Create RegistryIncepter to pass message and process cues Parameters: @@ -77,6 +81,7 @@ def __init__(self, name, base, alias, bran, registryName, **kwa): self.name = name self.alias = alias self.registryName = registryName + self.usage = usage self.hby = existing.setupHby(name=name, base=base, bran=bran) self.rgy = credentialing.Regery(hby=self.hby, name=name, base=base) self.hbyDoer = habbing.HaberyDoer(habery=self.hby) # setup doer @@ -118,7 +123,10 @@ def inceptDo(self, tymth, tock=0.0, **kwa): registry, ixn = self.registrar.incept(name=self.registryName, pre=hab.pre, conf=kwa) if isinstance(hab, GroupHab): - usage = input(f"Please enter a description of the credential registry: ") + usage = self.usage + if usage is None: + usage = input(f"Please enter a description of the credential registry: ") + smids = hab.db.signingMembers(pre=hab.pre) smids.remove(hab.mhab.pre) diff --git a/src/keri/app/cli/commands/vc/registry/list.py b/src/keri/app/cli/commands/vc/registry/list.py index 8d7fc987b..3c299895a 100644 --- a/src/keri/app/cli/commands/vc/registry/list.py +++ b/src/keri/app/cli/commands/vc/registry/list.py @@ -50,6 +50,5 @@ def registries(tymth, tock=0.0, **opts): print(registry.name, ":", registry.regk, ":", registry.hab.pre) except ConfigurationError as e: - print(e) print(f"identifier prefix for {name} does not exist, incept must be run first", ) return -1 diff --git a/src/keri/app/grouping.py b/src/keri/app/grouping.py index e9a0a9cbc..346f58587 100644 --- a/src/keri/app/grouping.py +++ b/src/keri/app/grouping.py @@ -260,6 +260,7 @@ def loadHandlers(exc, mux): exc.addHandler(MultisigNotificationHandler(resource="/multisig/vcp", mux=mux)) exc.addHandler(MultisigNotificationHandler(resource="/multisig/iss", mux=mux)) exc.addHandler(MultisigNotificationHandler(resource="/multisig/rvk", mux=mux)) + exc.addHandler(MultisigNotificationHandler(resource="/multisig/exn", mux=mux)) def multisigInceptExn(hab, smids, rmids, icp, delegator=None): @@ -392,6 +393,37 @@ def multisigRegistryInceptExn(ghab, usage, vcp, ixn=None, rot=None): return exn, atc +def multisigIssueExn(ghab, acdc, iss, anc): + """ Create a peer to peer message to propose a credential registry inception from a multisig group identifier + + Either rot or ixn are required but not both + + Parameters: + ghab (GroupHab): identifier Hab for ensorsing the message to send + acdc (bytes): serialized Credential + iss (bytes): CESR stream of serialized and TEL issuance event + anc (bytes): CESR stream of serialized and signed anchoring event anchoring registry inception event + + Returns: + tuple: (Serder, bytes): Serder of exn message and CESR attachments + + """ + + embeds = dict( + acdc=acdc, + iss=iss, + anc=anc + ) + + exn, end = exchanging.exchange(route="/multisig/iss", payload={'gid': ghab.pre}, + sender=ghab.mhab.pre, embeds=embeds) + evt = ghab.mhab.endorse(serder=exn, last=False, pipelined=False) + atc = bytearray(evt[exn.size:]) + atc.extend(end) + + return exn, atc + + def multisigExn(ghab, exn): """ Create a peer to peer message to propose a credential issuance from a multisig group identifier @@ -409,8 +441,9 @@ def multisigExn(ghab, exn): exn=exn ) - wexn, end = exchanging.exchange(route="/multisig/exn", payload={'gid': ghab.pre}, sender=ghab.mhab.pre, embeds=embeds) - evt = ghab.mhab.endorse(serder=exn, last=False, pipelined=False) + wexn, end = exchanging.exchange(route="/multisig/exn", payload={'gid': ghab.pre}, sender=ghab.mhab.pre, + embeds=embeds) + evt = ghab.mhab.endorse(serder=wexn, last=False, pipelined=False) atc = bytearray(evt[wexn.size:]) atc.extend(end) diff --git a/src/keri/core/eventing.py b/src/keri/core/eventing.py index f68b29f09..a0357c93c 100644 --- a/src/keri/core/eventing.py +++ b/src/keri/core/eventing.py @@ -1370,8 +1370,6 @@ def bare(route="", return Serder(ked=sad) # return serialized Self-Addressed Data (SAD) - - def messagize(serder, *, sigers=None, seal=None, wigers=None, cigars=None, pipelined=False): """ diff --git a/src/keri/core/parsing.py b/src/keri/core/parsing.py index a2c09aa57..177495226 100644 --- a/src/keri/core/parsing.py +++ b/src/keri/core/parsing.py @@ -1116,7 +1116,6 @@ def msgParsator(self, ims=None, framed=True, pipeline=False, tvy.processEvent(serder=serder, seqner=seqner, saider=saider, wigers=wigers) except AttributeError as e: - print(e) raise kering.ValidationError("No tevery to process so dropped msg" "= {}.".format(serder.pretty())) else: diff --git a/src/keri/peer/exchanging.py b/src/keri/peer/exchanging.py index 1f1825d35..fb75b5860 100644 --- a/src/keri/peer/exchanging.py +++ b/src/keri/peer/exchanging.py @@ -72,10 +72,6 @@ def processEvent(self, serder, tsgs=None, cigars=None, **kwargs): sender = serder.ked["i"] pathed = kwargs["pathed"] if "pathed" in kwargs else [] - if route not in self.routes: - raise AttributeError("unregistered route {} for exchange message = {}" - "".format(route, serder.pretty())) - behavior = self.routes[route] if route in self.routes else None if tsgs is not None: for prefixer, seqner, ssaider, sigers in tsgs: # iterate over each tsg @@ -229,6 +225,7 @@ def logEvent(self, serder, pathed=None, tsgs=None, cigars=None): self.hby.db.erts.add(keys=(route,), val=saider) if pdig: self.hby.db.erpy.pin(keys=(pdig,), val=saider) + self.hby.db.exns.put(keys=(dig,), val=serder) def lead(self, hab, said): @@ -381,6 +378,56 @@ def cloneMessage(hby, said): return exn, pathed +def serializeMessage(hby, said, pipelined=False): + atc = bytearray() + + exn = hby.db.exns.get(keys=(said,)) + if exn is None: + return None, None + + atc.extend(exn.raw) + + tsgs, cigars = verify(hby=hby, serder=exn) + + if len(tsgs) > 0: + for (prefixer, seqner, saider, sigers) in tsgs: + atc.extend(coring.Counter(coring.CtrDex.TransIdxSigGroups, count=1).qb64b) + atc.extend(prefixer.qb64b) + atc.extend(seqner.qb64b) + atc.extend(saider.qb64b) + + atc.extend(coring.Counter(code=coring.CtrDex.ControllerIdxSigs, count=len(sigers)).qb64b) + for siger in sigers: + atc.extend(siger.qb64b) + + if len(cigars) > 0: + atc.extend(coring.Counter(code=coring.CtrDex.NonTransReceiptCouples, count=len(cigars)).qb64b) + for cigar in cigars: + if cigar.verfer.code not in coring.NonTransDex: + raise ValueError("Attempt to use tranferable prefix={} for " + "receipt.".format(cigar.verfer.qb64)) + atc.extend(cigar.verfer.qb64b) + atc.extend(cigar.qb64b) + + # Smash the pathed components on the end + for p in hby.db.epath.get(keys=(exn.said,)): + atc.extend(coring.Counter(code=coring.CtrDex.PathedMaterialQuadlets, + count=(len(p) // 4)).qb64b) + atc.extend(p.encode("utf-8")) + + msg = bytearray() + + if pipelined: + if len(atc) % 4: + raise ValueError("Invalid attachments size={}, nonintegral" + " quadlets.".format(len(atc))) + msg.extend(coring.Counter(code=coring.CtrDex.AttachedMaterialQuadlets, + count=(len(atc) // 4)).qb64b) + + msg.extend(atc) + return msg + + def nesting(paths, acc, val): if len(paths) == 0: return val @@ -447,4 +494,4 @@ def verify(hby, serder): if not accepted: raise MissingSignatureError(f"No valid signatures stored for evt = {serder.ked}") - + return tsgs, cigars diff --git a/src/keri/vc/protocoling.py b/src/keri/vc/protocoling.py index 2d7cc942d..c8f5e051a 100644 --- a/src/keri/vc/protocoling.py +++ b/src/keri/vc/protocoling.py @@ -219,11 +219,12 @@ def ipexAgreeExn(hab, message, offer): return exn, ims -def ipexGrantExn(hab, message, acdc, iss, anc, agree=None): +def ipexGrantExn(hab, recp, message, acdc, iss, anc, agree=None): """ Disclose an ACDC Parameters: hab(Hab): identifier environment for issuer of credential + recp (str) qb64 AID of recipient of GRANT message message(str): Human readable message regarding the credential disclosure acdc (bytes): CESR stream of serialized ACDC with attachments iss (bytes): serialized TEL issuance event @@ -237,6 +238,7 @@ def ipexGrantExn(hab, message, acdc, iss, anc, agree=None): """ data = dict( m=message, + i=recp, ) embeds = dict( diff --git a/src/keri/vdr/credentialing.py b/src/keri/vdr/credentialing.py index ee4adab34..acef2a40d 100644 --- a/src/keri/vdr/credentialing.py +++ b/src/keri/vdr/credentialing.py @@ -10,7 +10,7 @@ from keri.vdr import viring from .. import kering, help -from ..app import agenting, signing +from ..app import agenting from ..app.habbing import GroupHab from ..core import parsing, coring, scheming from ..core.coring import Seqner, MtrDex, Serder @@ -527,35 +527,24 @@ def incept(self, name, pre, conf=None): return registry, evt - def issue(self, regk, said, dt=None, smids=None, rmids=None): + def issue(self, creder, iserder, anc): """ Create and process the credential issuance TEL events on the given registry Parameters: - regk (str): qb64 identifier prefix of the credential registry - said (str): qb64 SAID of the credential to issue - dt (str): iso8601 formatted date string of issuance date - smids (list): group signing member ids qb64 in the anchoring event - need to contribute current signing key - rmids (list): group rotating member ids qb64 in the anchoring event - need to contribute digest of next rotating key + creder (Creder): credential to issue + iserder (Serder): Serder object of TEL iss event + anc (Serder): Serder object of anchoring event + """ + regk = creder.status registry = self.rgy.regs[regk] hab = registry.hab - iserder = registry.issue(said=said, dt=dt) - vcid = iserder.ked["i"] rseq = coring.Seqner(snh=iserder.ked["s"]) - rseal = SealEvent(vcid, rseq.snh, iserder.said) - rseal = dict(i=rseal.i, s=rseal.s, d=rseal.d) if not isinstance(hab, GroupHab): # not a multisig group - if registry.estOnly: - hab.rotate(data=[rseal]) - else: - hab.interact(data=[rseal]) - seqner = coring.Seqner(sn=hab.kever.sner.num) saider = hab.kever.serder.saider registry.anchorMsg(pre=vcid, regd=iserder.said, seqner=seqner, saider=saider) @@ -564,15 +553,19 @@ def issue(self, regk, said, dt=None, smids=None, rmids=None): self.witDoer.msgs.append(dict(pre=hab.pre, sn=seqner.sn)) self.rgy.reger.tpwe.add(keys=(vcid, rseq.qb64), val=(hab.kever.prefixer, seqner, saider)) - return vcid, rseq.sn, iserder.said else: # multisig group hab - serder, prefixer, seqner, saider = self.multisigIxn(hab, rseal) + sn = anc.sn + said = anc.said + + prefixer = coring.Prefixer(qb64=hab.pre) + seqner = coring.Seqner(sn=sn) + saider = coring.Saider(qb64=said) + self.counselor.start(prefixer=prefixer, seqner=seqner, saider=saider, ghab=hab) print(f"Waiting for TEL iss event mulisig anchoring event {seqner.sn}") self.rgy.reger.tmse.add(keys=(vcid, rseq.qb64, iserder.said), val=(prefixer, seqner, saider)) - return vcid, rseq.sn, iserder.said def revoke(self, regk, said, dt=None, smids=None, rmids=None): """ @@ -641,7 +634,7 @@ def multisigIxn(hab, rseal): def complete(self, pre, sn=0): seqner = coring.Seqner(sn=sn) said = self.rgy.reger.ctel.get(keys=(pre, seqner.qb64)) - return said is not None + return said is not None and self.witPub.sent(said=pre) def escrowDo(self, tymth, tock=1.0): """ Process escrows of group multisig identifiers waiting to be compeleted. @@ -750,7 +743,7 @@ def processDiseminationEscrow(self): print(f"Sending TEL events to witnesses") # Fire and forget the TEL event to the witnesses. Consumers will have to query # to determine when the Witnesses have received the TEL events. - self.witPub.msgs.append(dict(pre=prefixer.qb64, msg=tevt)) + self.witPub.msgs.append(dict(pre=prefixer.qb64, said=regk, msg=tevt)) self.rgy.reger.ctel.put(keys=(regk, rseq.qb64), val=saider) # idempotent @@ -825,43 +818,28 @@ def validate(self, creder): return True - def issue(self, creder, smids=None, rmids=None): + def issue(self, creder, serder): """ Issue the credential creder and handle witness propagation and communication Args: creder (Creder): Credential object to issue - smids (list[str] | None): optional group signing member ids for multisig - need to contributed current signing key - rmids (list[str] | None): optional group rotating member ids for multisig + serder (Serder): KEL or TEL anchoring event need to contribute digest of next rotating key """ - regk = creder.crd["ri"] - registry = self.rgy.regs[regk] - hab = registry.hab - if isinstance(hab, GroupHab): - smids = smids if smids is not None else hab.smids - rmids = rmids if rmids is not None else hab.rmids - - dt = creder.subject["dt"] if "dt" in creder.subject else None - - vcid, seq, said = self.registrar.issue(regk=registry.regk, said=creder.said, - dt=dt, smids=smids, rmids=rmids) - - prefixer = coring.Prefixer(qb64=creder.said) - rseq = coring.Seqner(sn=seq) - saider = coring.Saider(qb64=said) # escrow waiting for other signatures - self.rgy.reger.cmse.put(keys=(creder.said, rseq.qb64), val=creder) + prefixer = coring.Prefixer(qb64=serder.pre) + seqner = coring.Seqner(sn=serder.sn) + + self.rgy.reger.cmse.put(keys=(creder.said, seqner.qb64), val=creder) try: - self.verifier.processCredential(creder=creder, prefixer=prefixer, seqner=rseq, saider=saider) + self.verifier.processCredential(creder=creder, prefixer=prefixer, seqner=seqner, saider=serder.saider) except (kering.MissingRegistryError, kering.MissingSchemaError): pass def processCredentialMissingSigEscrow(self): for (said, snq), creder in self.rgy.reger.cmse.getItemIter(): rseq = coring.Seqner(qb64=snq) - if not self.registrar.complete(pre=said, sn=rseq.sn): continue diff --git a/src/keri/vdr/eventing.py b/src/keri/vdr/eventing.py index f1de202fb..66e2fd982 100644 --- a/src/keri/vdr/eventing.py +++ b/src/keri/vdr/eventing.py @@ -1665,19 +1665,20 @@ def processQuery(self, serder, source=None, sigers=None, cigars=None): if route == "tels": mgmt = qry["ri"] + src = qry["src"] cloner = self.reger.clonePreIter(pre=mgmt, fn=0) # create iterator at 0 - msgs = bytearray() # outgoing messages + msgs = list() # outgoing messages for msg in cloner: - msgs.extend(msg) + msgs.append(msg) if vci := qry["i"]: cloner = self.reger.clonePreIter(pre=vci, fn=0) # create iterator at 0 for msg in cloner: - msgs.extend(msg) + msgs.append(msg) if msgs: - self.cues.append(dict(kin="replay", dest=source, msgs=msgs)) + self.cues.append(dict(kin="replay", src=src, dest=source.qb64, msgs=msgs)) elif route == "tsn": ri = qry["ri"] if ri in self.tevers: diff --git a/src/keri/vdr/verifying.py b/src/keri/vdr/verifying.py index a69567ffe..afcfcd405 100644 --- a/src/keri/vdr/verifying.py +++ b/src/keri/vdr/verifying.py @@ -106,7 +106,7 @@ def processCredential(self, creder, prefixer, seqner, saider): if regk not in self.tevers: # registry event not found yet if self.escrowMRE(creder, prefixer, seqner, saider): - self.cues.append(dict(kin="telquery", q=dict(ri=regk, i=vcid))) + self.cues.append(dict(kin="telquery", q=dict(ri=regk, i=vcid, issr=creder.issuer))) raise kering.MissingRegistryError("registry identifier {} not in Tevers".format(regk)) state = self.tevers[regk].vcState(vcid) From f10aac9e0cc05d742db8beaf5bd58dd38aa7564e Mon Sep 17 00:00:00 2001 From: pfeairheller Date: Wed, 20 Sep 2023 12:13:01 -0700 Subject: [PATCH 2/5] Interactive join support for credential registry creation. Signed-off-by: pfeairheller --- .../multisig-issuer-interactive.sh | 22 +- src/keri/app/cli/commands/ipex/grant.py | 5 +- src/keri/app/cli/commands/multisig/join.py | 272 ++++++++++++++---- .../app/cli/commands/vc/registry/incept.py | 17 +- src/keri/app/cli/commands/vc/revoke.py | 92 +++--- src/keri/app/grouping.py | 47 ++- src/keri/app/habbing.py | 16 ++ src/keri/app/indirecting.py | 2 + src/keri/vdr/credentialing.py | 103 +++---- 9 files changed, 402 insertions(+), 174 deletions(-) diff --git a/scripts/demo/credentials/multisig-issuer-interactive.sh b/scripts/demo/credentials/multisig-issuer-interactive.sh index 9c3615ea7..281d1bb6b 100755 --- a/scripts/demo/credentials/multisig-issuer-interactive.sh +++ b/scripts/demo/credentials/multisig-issuer-interactive.sh @@ -51,9 +51,12 @@ kli vc registry incept --name multisig1 --alias multisig --registry-name vLEI -- pid=$! PID_LIST=" $pid" -kli vc registry incept --name multisig2 --alias multisig --registry-name vLEI --usage "Issue vLEIs" --nonce AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s & -pid=$! -PID_LIST+=" $pid" +#kli vc registry incept --name multisig2 --alias multisig --registry-name vLEI --usage "Issue vLEIs" --nonce AHSNDV3ABI6U8OIgKaj3aky91ZpNL54I5_7-qwtC6q2s & +#pid=$! +#PID_LIST+=" $pid" + +echo "Multisig2 looking to join credential registry creation" +kli multisig join --name multisig2 wait $PID_LIST @@ -63,7 +66,7 @@ pid=$! PID_LIST+=" $pid" # Wait for 3 seconds to allow credential.json to be created, but still launch in parallel because they will wait for each other -echo "Multisig2 looking to join" +echo "Multisig2 looking to join credential creation" kli multisig join --name multisig2 wait $PID_LIST @@ -72,25 +75,26 @@ SAID=$(kli vc list --name multisig1 --alias multisig --issued --said) kli ipex grant --name multisig1 --alias multisig --said "${SAID}" --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k & pid=$! -PID_LIST+=" $pid" +PID_LIST="$pid" kli multisig join --name multisig2 +wait ${PID_LIST} + echo "Polling for holder's IPEX message..." SAID=$(kli ipex list --name holder --alias holder --poll --said) echo "Admitting GRANT ${SAID}" kli ipex admit --name holder --alias holder --said "${SAID}" -kli vc list --name holder --alias holder --poll +kli vc list --name holder --alias holder -exit 0 SAID=$(kli vc list --name holder --alias holder --said --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao) echo "Revoking ${SAID}..." -kli vc revoke --name multisig2 --alias multisig --registry-name vLEI --said "${SAID}"& +kli vc revoke --name multisig1 --alias multisig --registry-name vLEI --said "${SAID}"& pid=$! -PID_LIST+=" $pid" +PID_LIST="$pid" kli multisig join --name multisig2 diff --git a/src/keri/app/cli/commands/ipex/grant.py b/src/keri/app/cli/commands/ipex/grant.py index ebf135b16..6c32ae345 100644 --- a/src/keri/app/cli/commands/ipex/grant.py +++ b/src/keri/app/cli/commands/ipex/grant.py @@ -125,14 +125,14 @@ def grantDo(self, tymth, tock=0.0): smids = self.hab.db.signingMembers(pre=self.hab.pre) smids.remove(self.hab.mhab.pre) - for recp in smids: # this goes to other participants only as a signaling mechanism + for recp in smids: # this goes to other participants self.postman.send(src=self.hab.mhab.pre, dest=recp, topic="multisig", serder=wexn, attachment=watc) - while not self.exc.complete(said=wexn.said): + while not self.exc.complete(said=exn.said): yield self.tock if self.exc.lead(self.hab, said=exn.said): @@ -148,4 +148,5 @@ def grantDo(self, tymth, tock=0.0): yield self.tock print("... grant message sent") + self.remove(self.toRemove) diff --git a/src/keri/app/cli/commands/multisig/join.py b/src/keri/app/cli/commands/multisig/join.py index 0ae83030c..8a964a332 100644 --- a/src/keri/app/cli/commands/multisig/join.py +++ b/src/keri/app/cli/commands/multisig/join.py @@ -109,70 +109,34 @@ def confirmDo(self, tymth, tock=0.0): attrs = notice.attrs route = attrs['r'] - if route == '/multisig/icp/init': - done = yield from self.incept(attrs) - if done: - self.notifier.noter.notes.rem(keys=keys) - - else: - delete = input(f"\nDelete event [Y|n]? ") - if delete in ("Y", "y"): - self.notifier.noter.notes.rem(keys=keys) - - elif route == '/multisig/ixn': - done = yield from self.interact(attrs) - if done: - self.notifier.noter.notes.rem(keys=keys) - - else: - delete = input(f"\nDelete event [Y|n]? ") - if delete in ("Y", "y"): - self.notifier.noter.notes.rem(keys=keys) - - elif route == '/multisig/rot': - - done = yield from self.rotate(attrs) - if done: - self.notifier.noter.notes.rem(keys=keys) - - else: - delete = input(f"\nDelete event [Y|n]? ") - if delete in ("Y", "y"): - self.notifier.noter.notes.rem(keys=keys) - - elif route == '/multisig/rpy': - - done = yield from self.rpy(attrs) - if done: - self.notifier.noter.notes.rem(keys=keys) - - else: - delete = input(f"\nDelete event [Y|n]? ") - if delete in ("Y", "y"): - self.notifier.noter.notes.rem(keys=keys) - - elif route == '/multisig/iss': - - done = yield from self.iss(attrs) - if done: - self.notifier.noter.notes.rem(keys=keys) - - else: - delete = input(f"\nDelete event [Y|n]? ") - if delete in ("Y", "y"): - self.notifier.noter.notes.rem(keys=keys) + match route: + case '/multisig/icp': + done = yield from self.incept(attrs) + case '/multisig/ixn': + done = yield from self.interact(attrs) + case '/multisig/rot': + done = yield from self.rotate(attrs) + case '/multisig/rpy': + done = yield from self.rpy(attrs) + case '/multisig/vcp': + done = yield from self.vcp(attrs) + case '/multisig/iss': + done = yield from self.iss(attrs) + case '/multisig/rev': + done = yield from self.rev(attrs) + case '/multisig/exn': + done = yield from self.exn(attrs) + case _: + continue + + if done: + self.notifier.noter.notes.rem(keys=keys) - elif route == '/multisig/exn': - - done = yield from self.exn(attrs) - if done: + else: + delete = input(f"\nDelete event [Y|n]? ") + if delete in ("Y", "y"): self.notifier.noter.notes.rem(keys=keys) - else: - delete = input(f"\nDelete event [Y|n]? ") - if delete in ("Y", "y"): - self.notifier.noter.notes.rem(keys=keys) - yield self.tock yield self.tock @@ -180,6 +144,9 @@ def incept(self, attrs): """ Incept group multisig """ + if True: + return True + smids = attrs["smids"] # change body mids for group member ids rmids = attrs["rmids"] if "rmids" in attrs else None ked = attrs["ked"] @@ -446,6 +413,79 @@ def rpy(self, attrs): """ ked = attrs["ked"] + def vcp(self, attrs): + """ Handle issue messages + + Parameters: + attrs (dict): attributes of the reply message + + Returns: + + """ + said = attrs["d"] + exn, pathed = exchanging.cloneMessage(self.hby, said=said) + + sender = exn.ked['i'] + payload = exn.ked['a'] + usage = payload["usage"] + gid = payload["gid"] + hab = self.hby.habs[gid] if gid in self.hby.habs else None + if hab is None: + raise ValueError(f"credential issuer not a valid AID={gid}") + + contact = self.org.get(sender) + senderAlias = contact['alias'] + + embeds = exn.ked['e'] + print(f"\nGroup Credential Regitry Creation (from {senderAlias}):") + print(f"Usage: {usage}:\n") + + yn = input(f"\nApprove [Y|n]? ") + approve = yn in ('', 'y', 'Y') + + if approve: + # Create and parse the event with "their" signatures + registryName = input("Name for Registry: ") + anc = embeds["anc"] + aserder = coring.Serder(ked=anc) + anc = bytearray(aserder.raw) + pathed["anc"] + self.psr.parseOne(ims=bytes(anc)) + + # Now sign the event and parse it with our signatures + sigers = hab.sign(aserder.raw) + anc = eventing.messagize(serder=aserder, sigers=sigers) + self.psr.parseOne(ims=bytes(anc)) + + vcp = embeds["vcp"] + vserder = coring.Serder(ked=vcp) + try: + self.rgy.tvy.processEvent(serder=vserder) + except kering.MissingAnchorError: + pass + + self.rgy.makeRegistry(name=registryName, prefix=hab.pre, vcp=vserder) + self.registrar.incept(vserder, aserder) + + smids = hab.db.signingMembers(pre=hab.pre) + smids.remove(hab.mhab.pre) + + for recp in smids: # this goes to other participants only as a signaling mechanism + exn, atc = grouping.multisigRegistryInceptExn(ghab=hab, vcp=vserder.raw, anc=anc, usage=usage) + self.postman.send(src=hab.mhab.pre, + dest=recp, + topic="multisig", + serder=exn, + attachment=atc) + + while not self.registrar.complete(vserder.pre, sn=0): + self.rgy.processEscrows() + self.verifier.processEscrows() + yield self.tock + + print(f"Registry {vserder.pre} created.") + + yield self.tock + def iss(self, attrs): """ Handle issue messages @@ -545,6 +585,116 @@ def iss(self, attrs): yield self.tock + def rev(self, attrs): + """ Handle revocation messages + + Parameters: + attrs (dict): attributes of the reply message + + Returns: + + """ + said = attrs["d"] + exn, pathed = exchanging.cloneMessage(self.hby, said=said) + + sender = exn.ked['i'] + payload = exn.ked['a'] + said = payload['said'] + + creder = self.verifier.reger.creds.get(keys=(said,)) + if creder is None: + print(f"invalid credential SAID {said}") + return + + contact = self.org.get(sender) + senderAlias = contact['alias'] + + embeds = exn.ked['e'] + scraw = self.verifier.resolver.resolve(creder.schema) + if not scraw: + raise kering.ConfigurationError("Credential schema {} not found".format(creder.schema)) + + schemer = scheming.Schemer(raw=scraw) + + hab = self.hby.habs[creder.issuer] + if hab is None: + raise ValueError(f"credential issuer not a valid AID={creder.issuer}") + + print(f"\nGroup Credential Revocation Proposed (from {senderAlias}):") + print(f"Credential {creder.said}:") + print(f" Type: {schemer.sed['title']}") + print(f" Issued By: {hab.name} ({hab.pre})") + + if "i" in creder.subject: + isse = creder.subject['i'] + contact = self.org.get(isse) + if contact is not None and "alias" in contact: + print(f" Issued To: {contact['alias']} ({isse})") + else: + print(f" Issued To: Unknown AID ({isse})") + + yn = input(f"\nApprove Revocation [Y|n]? ") + approve = yn in ('', 'y', 'Y') + + if approve: + # Create and parse the event with "their" signatures + anc = embeds["anc"] + aserder = coring.Serder(ked=anc) + anc = bytearray(aserder.raw) + pathed["anc"] + self.psr.parseOne(ims=bytes(anc)) + + # Now sign the event and parse it with our signatures + sigers = hab.sign(aserder.raw) + anc = eventing.messagize(serder=aserder, sigers=sigers) + self.psr.parseOne(ims=bytes(anc)) + + rev = embeds["rev"] + rserder = coring.Serder(ked=rev) + try: + self.rgy.tvy.processEvent(serder=rserder) + except kering.MissingAnchorError: + pass + + self.registrar.revoke(creder, rserder, aserder) + + smids = hab.db.signingMembers(pre=hab.pre) + smids.remove(hab.mhab.pre) + + for recp in smids: # this goes to other participants only as a signaling mechanism + exn, atc = grouping.multisigRevokeExn(ghab=hab, said=creder.said, rev=rserder.raw, anc=anc) + self.postman.send(src=hab.mhab.pre, + dest=recp, + topic="multisig", + serder=exn, + attachment=atc) + + while not self.registrar.complete(creder.said, sn=1): + self.rgy.processEscrows() + yield self.tock + + print(f"Credential {creder.said} revoked.") + if hab.witnesser() and 'i' in creder.subject: + recp = creder.subject['i'] + msgs = [] + for msg in self.hby.db.clonePreIter(pre=creder.issuer): + serder = coring.Serder(raw=msg) + atc = msg[serder.size:] + msgs.append((serder, atc)) + for msg in self.rgy.reger.clonePreIter(pre=creder.said): + serder = coring.Serder(raw=msg) + atc = msg[serder.size:] + msgs.append((serder, atc)) + + for (serder, atc) in msgs: + self.postman.send(src=hab.mhab.pre, dest=recp, topic="credential", serder=serder, + attachment=atc) + + last = msgs[-1][0] + while not self.postman.sent(said=last.said): + yield self.tock + + yield self.tock + def exn(self, attrs): """ Handle exn messages diff --git a/src/keri/app/cli/commands/vc/registry/incept.py b/src/keri/app/cli/commands/vc/registry/incept.py index e18f5b951..8775a3242 100644 --- a/src/keri/app/cli/commands/vc/registry/incept.py +++ b/src/keri/app/cli/commands/vc/registry/incept.py @@ -7,6 +7,8 @@ from keri.app.cli.common import existing from keri.app.habbing import GroupHab from keri.app.notifying import Notifier +from keri.core import coring +from keri.core.eventing import SealEvent from keri.peer import exchanging from keri.vdr import credentialing @@ -120,7 +122,18 @@ def inceptDo(self, tymth, tock=0.0, **kwa): if hab is None: raise ValueError(f"{self.alias} is not a valid AID alias") - registry, ixn = self.registrar.incept(name=self.registryName, pre=hab.pre, conf=kwa) + estOnly = "estOnly" in kwa and kwa["estOnly"] + registry = self.rgy.makeRegistry(name=self.registryName, prefix=hab.pre, **kwa) + + rseal = SealEvent(registry.regk, "0", registry.regd) + rseal = dict(i=rseal.i, s=rseal.s, d=rseal.d) + if estOnly: + anc = hab.rotate(data=[rseal]) + else: + anc = hab.interact(data=[rseal]) + + aserder = coring.Serder(raw=bytes(anc)) + self.registrar.incept(iserder=registry.vcp, anc=aserder) if isinstance(hab, GroupHab): usage = self.usage @@ -131,7 +144,7 @@ def inceptDo(self, tymth, tock=0.0, **kwa): smids.remove(hab.mhab.pre) for recp in smids: # this goes to other participants only as a signaling mechanism - exn, atc = grouping.multisigRegistryInceptExn(ghab=hab, vcp=registry.vcp.raw, ixn=ixn, usage=usage) + exn, atc = grouping.multisigRegistryInceptExn(ghab=hab, vcp=registry.vcp.raw, anc=anc, usage=usage) self.postman.send(src=hab.mhab.pre, dest=recp, topic="multisig", diff --git a/src/keri/app/cli/commands/vc/revoke.py b/src/keri/app/cli/commands/vc/revoke.py index a664a2898..af6d945a1 100644 --- a/src/keri/app/cli/commands/vc/revoke.py +++ b/src/keri/app/cli/commands/vc/revoke.py @@ -8,10 +8,12 @@ from hio.base import doing from keri import kering -from keri.app import indirecting, habbing, grouping, forwarding, connecting +from keri.app import indirecting, habbing, grouping, forwarding, connecting, notifying from keri.app.cli.common import existing from keri.app.habbing import GroupHab from keri.core import coring +from keri.core.eventing import SealEvent +from keri.peer import exchanging from keri.vdr import credentialing, verifying parser = argparse.ArgumentParser(description='Revoke a verifiable credential') @@ -56,27 +58,32 @@ def __init__(self, name, alias, said, base, bran, registryName, send, timestamp, self.registrar = credentialing.Registrar(hby=self.hby, rgy=self.rgy, counselor=self.counselor) self.verifier = verifying.Verifier(hby=self.hby, reger=self.rgy.reger) self.postman = forwarding.Poster(hby=self.hby) + notifier = notifying.Notifier(self.hby) + mux = grouping.Multiplexor(self.hby, notifier=notifier) + exc = exchanging.Exchanger(hby=self.hby, handlers=[]) + grouping.loadHandlers(exc, mux) mbx = indirecting.MailboxDirector(hby=self.hby, topics=["/receipt", "/multisig", "/credential"], - verifier=self.verifier) + verifier=self.verifier, exc=exc) doers = [self.hbyDoer, mbx, self.counselor, self.registrar, self.postman] self.toRemove = list(doers) doers.extend([doing.doify(self.revokeDo)]) super(RevokeDoer, self).__init__(doers=doers, **kwa) - def revokeDo(self, tymth, tock=0.0, **opts): - """ + def revokeDo(self, tymth, tock=0.0): + """ Revoke Credential doer method - Parameters: - tymth: - tock: - **opts: - Returns: + 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 """ - yield self.tock + self.wind(tymth) + self.tock = tock + _ = (yield self.tock) registry = self.rgy.registryByName(self.registryName) if registry is None: @@ -93,18 +100,47 @@ def revokeDo(self, tymth, tock=0.0, **opts): if self.timestamp is not None: kwargs['dt'] = self.timestamp - self.registrar.revoke(regk=registry.regk, said=creder.said, **kwargs) + registry = self.rgy.regs[registry.regk] + hab = registry.hab - while not self.registrar.complete(creder.said, sn=1): - yield self.tock + state = registry.tever.vcState(vci=creder.said) + if state is None or state.ked["et"] not in (coring.Ilks.iss, coring.Ilks.rev): + raise kering.ValidationError(f"credential {creder.said} not is correct state for revocation") + + rserder = registry.revoke(said=creder.said, **kwargs) + + vcid = rserder.ked["i"] + rseq = coring.Seqner(snh=rserder.ked["s"]) + rseal = SealEvent(vcid, rseq.snh, rserder.said) + rseal = dict(i=rseal.i, s=rseal.s, d=rseal.d) - recps = [creder.subject['i']] if 'i' in creder.subject else [] - if self.send is not None: - recps.extend(self.send) + if registry.estOnly: + anc = hab.rotate(data=[rseal]) + else: + anc = hab.interact(data=[rseal]) - senderHab = self.hab.mhab if isinstance(self.hab, GroupHab) else self.hab + aserder = coring.Serder(raw=bytes(anc)) + self.registrar.revoke(creder, rserder, aserder) - if len(recps) > 0: + senderHab = self.hab + if isinstance(self.hab, GroupHab): + senderHab = self.hab.mhab + smids = self.hab.db.signingMembers(pre=self.hab.pre) + smids.remove(self.hab.mhab.pre) + + for recp in smids: # this goes to other participants only as a signaling mechanism + exn, atc = grouping.multisigRevokeExn(ghab=self.hab, said=creder.said, rev=rserder.raw, anc=anc) + self.postman.send(src=self.hab.mhab.pre, + dest=recp, + topic="multisig", + serder=exn, + attachment=atc) + + while not self.registrar.complete(creder.said, sn=1): + yield self.tock + + if self.hab.witnesser() and 'i' in creder.subject: + recp = creder.subject['i'] msgs = [] for msg in self.hby.db.clonePreIter(pre=creder.issuer): serder = coring.Serder(raw=msg) @@ -115,20 +151,12 @@ def revokeDo(self, tymth, tock=0.0, **opts): atc = msg[serder.size:] msgs.append((serder, atc)) - sent = 0 - for send in recps: - if send in self.hby.kevers: - recp = send - else: - recp = self.org.find("alias", send) - if len(recp) != 1: - raise ValueError(f"invalid recipient {send}") - recp = recp[0]['id'] - for (serder, atc) in msgs: - self.postman.send(src=senderHab.pre, dest=recp, topic="credential", serder=serder, attachment=atc) - sent += 1 - - while not len(self.postman.cues) == sent: + for (serder, atc) in msgs: + self.postman.send(src=senderHab.pre, dest=recp, topic="credential", serder=serder, + attachment=atc) + + last = msgs[-1][0] + while not self.postman.sent(said=last.said): yield self.tock except kering.ValidationError as ex: diff --git a/src/keri/app/grouping.py b/src/keri/app/grouping.py index 346f58587..773e6d299 100644 --- a/src/keri/app/grouping.py +++ b/src/keri/app/grouping.py @@ -259,7 +259,7 @@ def loadHandlers(exc, mux): exc.addHandler(MultisigNotificationHandler(resource="/multisig/ixn", mux=mux)) exc.addHandler(MultisigNotificationHandler(resource="/multisig/vcp", mux=mux)) exc.addHandler(MultisigNotificationHandler(resource="/multisig/iss", mux=mux)) - exc.addHandler(MultisigNotificationHandler(resource="/multisig/rvk", mux=mux)) + exc.addHandler(MultisigNotificationHandler(resource="/multisig/rev", mux=mux)) exc.addHandler(MultisigNotificationHandler(resource="/multisig/exn", mux=mux)) @@ -358,7 +358,7 @@ def multisigInteractExn(ghab, aids, ixn): return exn, atc -def multisigRegistryInceptExn(ghab, usage, vcp, ixn=None, rot=None): +def multisigRegistryInceptExn(ghab, usage, vcp, anc): """ Create a peer to peer message to propose a credential registry inception from a multisig group identifier Either rot or ixn are required but not both @@ -367,8 +367,7 @@ def multisigRegistryInceptExn(ghab, usage, vcp, ixn=None, rot=None): ghab (GroupHab): identifier Hab for ensorsing the message to send usage (str): human readable reason for creating the credential registry vcp (bytes): serialized Credentials registry inception event - ixn (bytes): CESR stream of serialized and signed interaction event anchoring registry inception event - rot (bytes): CESR stream of serialized and signed rotation event anchoring registry inception event + anc (bytes): CESR stream of serialized and signed event anchoring registry inception event Returns: tuple: (Serder, bytes): Serder of exn message and CESR attachments @@ -377,13 +376,9 @@ def multisigRegistryInceptExn(ghab, usage, vcp, ixn=None, rot=None): embeds = dict( vcp=vcp, + anc=anc ) - if rot is not None: - embeds["rot"] = rot - elif ixn is not None: - embeds['ixn'] = ixn - exn, end = exchanging.exchange(route="/multisig/vcp", payload={'gid': ghab.pre, 'usage': usage}, sender=ghab.mhab.pre, embeds=embeds) evt = ghab.mhab.endorse(serder=exn, last=False, pipelined=False) @@ -394,7 +389,7 @@ def multisigRegistryInceptExn(ghab, usage, vcp, ixn=None, rot=None): def multisigIssueExn(ghab, acdc, iss, anc): - """ Create a peer to peer message to propose a credential registry inception from a multisig group identifier + """ Create a peer to peer message to propose a credential creation from a multisig group identifier Either rot or ixn are required but not both @@ -402,7 +397,7 @@ def multisigIssueExn(ghab, acdc, iss, anc): ghab (GroupHab): identifier Hab for ensorsing the message to send acdc (bytes): serialized Credential iss (bytes): CESR stream of serialized and TEL issuance event - anc (bytes): CESR stream of serialized and signed anchoring event anchoring registry inception event + anc (bytes): CESR stream of serialized and signed anchoring event anchoring creation Returns: tuple: (Serder, bytes): Serder of exn message and CESR attachments @@ -424,6 +419,36 @@ def multisigIssueExn(ghab, acdc, iss, anc): return exn, atc +def multisigRevokeExn(ghab, said, rev, anc): + """ Create a peer to peer message to propose a credential revocation from a multisig group identifier + + Either rot or ixn are required but not both + + Parameters: + ghab (GroupHab): identifier Hab for ensorsing the message to send + said (str): qb64 SAID of credential being revoked + rev (bytes): CESR stream of serialized and TEL revocation event + anc (bytes): CESR stream of serialized and signed anchoring event anchoring revocation + + Returns: + tuple: (Serder, bytes): Serder of exn message and CESR attachments + + """ + + embeds = dict( + rev=rev, + anc=anc + ) + + exn, end = exchanging.exchange(route="/multisig/rev", payload={'gid': ghab.pre, 'said': said}, + sender=ghab.mhab.pre, embeds=embeds) + evt = ghab.mhab.endorse(serder=exn, last=False, pipelined=False) + atc = bytearray(evt[exn.size:]) + atc.extend(end) + + return exn, atc + + def multisigExn(ghab, exn): """ Create a peer to peer message to propose a credential issuance from a multisig group identifier diff --git a/src/keri/app/habbing.py b/src/keri/app/habbing.py index 7c7d240b1..95bd3b247 100644 --- a/src/keri/app/habbing.py +++ b/src/keri/app/habbing.py @@ -2045,6 +2045,9 @@ def processCuesIter(self, cues): msg = self.reply(data=data, route=route) yield msg + def witnesser(self): + return True + class Hab(BaseHab): """ @@ -2752,3 +2755,16 @@ def query(self, pre, src, query=None, **kwa): serder = eventing.query(query=query, **kwa) return self.mhab.endorse(serder, last=True) + + def witnesser(self): + kever = self.kever + keys = [verfer.qb64 for verfer in kever.verfers] + sigs = self.db.getSigs(dbing.dgKey(self.pre, kever.serder.saidb)) + if not sigs: # otherwise its a list of sigs + return False + + sigers = [coring.Siger(qb64b=bytes(sig)) for sig in sigs] + windex = min([siger.index for siger in sigers]) + + # True if Elected to perform delegation and witnessing + return self.mhab.kever.verfers[0].qb64 == keys[windex] diff --git a/src/keri/app/indirecting.py b/src/keri/app/indirecting.py index 854066d17..d1b482d7e 100644 --- a/src/keri/app/indirecting.py +++ b/src/keri/app/indirecting.py @@ -698,6 +698,8 @@ def escrowDo(self, tymth=None, tock=0.0): while True: self.kvy.processEscrows() self.rvy.processEscrowReply() + if self.exchanger is not None: + self.exchanger.processEscrow() if self.tvy is not None: self.tvy.processEscrows() if self.verifier is not None: diff --git a/src/keri/vdr/credentialing.py b/src/keri/vdr/credentialing.py index acef2a40d..7440284db 100644 --- a/src/keri/vdr/credentialing.py +++ b/src/keri/vdr/credentialing.py @@ -259,7 +259,7 @@ class Registry(BaseRegistry): """ - def make(self, *, nonce=None, noBackers=True, baks=None, toad=None, estOnly=False): + def make(self, *, nonce=None, noBackers=True, baks=None, toad=None, estOnly=False, vcp=None): """ Delayed initialization of Issuer. Actual initialization of Issuer from properties or loaded from .reger. Should @@ -271,22 +271,27 @@ def make(self, *, nonce=None, noBackers=True, baks=None, toad=None, estOnly=Fals baks (list): initial list of backer prefixes qb64 for VCs in the Registry toad (str): hex of witness threshold estOnly (boolean): True for forcing rotation events for every TEL event. + vcp (Serder): optional vcp event serder if configured outside the Registry """ - baks = baks if baks is not None else [] + pre = self.hab.pre - self.cnfg = [TraitDex.NoBackers] if noBackers else [] - if estOnly: - self.cnfg.append(TraitDex.EstOnly) + if vcp is None: + baks = baks if baks is not None else [] - pre = self.hab.pre + self.cnfg = [TraitDex.NoBackers] if noBackers else [] + if estOnly: + self.cnfg.append(TraitDex.EstOnly) + + self.vcp = eventing.incept(pre, + baks=baks, + toad=toad, + nonce=nonce, + cnfg=self.cnfg, + code=MtrDex.Blake3_256) + else: + self.vcp = vcp - self.vcp = eventing.incept(pre, - baks=baks, - toad=toad, - nonce=nonce, - cnfg=self.cnfg, - code=MtrDex.Blake3_256) self.regk = self.vcp.pre self.regd = self.vcp.said self.registries.add(self.regk) @@ -482,51 +487,44 @@ def __init__(self, hby, rgy, counselor): super(Registrar, self).__init__(doers=doers) - def incept(self, name, pre, conf=None): + def incept(self, iserder, anc): """ Parameters: - name (str): human readable name for the registry - pre (str): qb64 identifier prefix of issuing identifier in control of this registry - conf (dict): configuration information for the registry (noBackers, estOnly) + iserder (Serder): Serder object of TEL iss event + anc (Serder): Serder object of anchoring event Returns: Registry: created registry """ - conf = conf if conf is not None else {} # default config if none specified - estOnly = "estOnly" in conf and conf["estOnly"] - hab = self.hby.habs[pre] - - registry = self.rgy.makeRegistry(name=name, prefix=pre, **conf) - + registry = self.rgy.regs[iserder.pre] + hab = registry.hab rseq = coring.Seqner(sn=0) - rseal = SealEvent(registry.regk, "0", registry.regd) - rseal = dict(i=rseal.i, s=rseal.s, d=rseal.d) - if not isinstance(hab, GroupHab): - if estOnly: - evt = hab.rotate(data=[rseal]) - else: - evt = hab.interact(data=[rseal]) + if not isinstance(hab, GroupHab): # not a multisig group seqner = coring.Seqner(sn=hab.kever.sner.num) saider = hab.kever.serder.saider - registry.anchorMsg(pre=registry.regk, regd=registry.regd, seqner=seqner, saider=saider) + registry.anchorMsg(pre=iserder.pre, regd=iserder.said, seqner=seqner, saider=saider) print("Waiting for TEL event witness receipts") - self.witDoer.msgs.append(dict(pre=pre, sn=seqner.sn)) + self.witDoer.msgs.append(dict(pre=anc.pre, sn=seqner.sn)) self.rgy.reger.tpwe.add(keys=(registry.regk, rseq.qb64), val=(hab.kever.prefixer, seqner, saider)) else: - evt, prefixer, seqner, saider = self.multisigIxn(hab, rseal) + sn = anc.sn + said = anc.said + + prefixer = coring.Prefixer(qb64=hab.pre) + seqner = coring.Seqner(sn=sn) + saider = coring.Saider(qb64=said) + self.counselor.start(prefixer=prefixer, seqner=seqner, saider=saider, ghab=hab) print("Waiting for TEL registry vcp event mulisig anchoring event") self.rgy.reger.tmse.add(keys=(registry.regk, rseq.qb64, registry.regd), val=(prefixer, seqner, saider)) - return registry, evt - def issue(self, creder, iserder, anc): """ Create and process the credential issuance TEL events on the given registry @@ -567,39 +565,24 @@ def issue(self, creder, iserder, anc): print(f"Waiting for TEL iss event mulisig anchoring event {seqner.sn}") self.rgy.reger.tmse.add(keys=(vcid, rseq.qb64, iserder.said), val=(prefixer, seqner, saider)) - def revoke(self, regk, said, dt=None, smids=None, rmids=None): + def revoke(self, creder, rserder, anc): """ Create and process the credential revocation TEL events on the given registry Parameters: - regk (str): qb64 identifier prefix of the credential registry - said (str): qb64 SAID of the credential to issue - dt (str): iso8601 formatted date string of issuance date - smids (list): group signing member ids (multisig) in the anchoring event - need to contribute digest of current signing key - rmids (list | None): group rotating member ids (multisig) in the anchoring event - need to contribute digest of next rotating key + creder (Creder): credential to issue + rserder (Serder): Serder object of TEL rev event + anc (Serder): Serder object of anchoring event """ + + regk = creder.status registry = self.rgy.regs[regk] hab = registry.hab - state = registry.tever.vcState(vci=said) - if state is None or state.ked["et"] not in (coring.Ilks.iss, coring.Ilks.rev): - raise kering.ValidationError(f"credential {said} not is correct state for revocation") - - rserder = registry.revoke(said=said, dt=dt) - vcid = rserder.ked["i"] rseq = coring.Seqner(snh=rserder.ked["s"]) - rseal = SealEvent(vcid, rseq.snh, rserder.said) - rseal = dict(i=rseal.i, s=rseal.s, d=rseal.d) - - if not isinstance(hab, GroupHab): - if registry.estOnly: - hab.rotate(data=[rseal]) - else: - hab.interact(data=[rseal]) + if not isinstance(hab, GroupHab): # not a multisig group seqner = coring.Seqner(sn=hab.kever.sner.num) saider = hab.kever.serder.saider registry.anchorMsg(pre=vcid, regd=rserder.said, seqner=seqner, saider=saider) @@ -610,7 +593,13 @@ def revoke(self, regk, said, dt=None, smids=None, rmids=None): self.rgy.reger.tpwe.add(keys=(vcid, rseq.qb64), val=(hab.kever.prefixer, seqner, saider)) return vcid, rseq.sn else: - serder, prefixer, seqner, saider = self.multisigIxn(hab, rseal) + sn = anc.sn + said = anc.said + + prefixer = coring.Prefixer(qb64=hab.pre) + seqner = coring.Seqner(sn=sn) + saider = coring.Saider(qb64=said) + self.counselor.start(prefixer=prefixer, seqner=seqner, saider=saider, ghab=hab) print(f"Waiting for TEL rev event mulisig anchoring event {seqner.sn}") From 3d9e7756376ef179ac32008bc93acfb6c598a17d Mon Sep 17 00:00:00 2001 From: pfeairheller Date: Wed, 20 Sep 2023 14:08:24 -0700 Subject: [PATCH 3/5] Non-interactive mode for multisig revoke credential and IPEX grant. Signed-off-by: pfeairheller --- scripts/demo/credentials/multisig-issuer.sh | 36 ++++++++++++--------- src/keri/app/cli/commands/ipex/grant.py | 21 +++++++----- src/keri/app/cli/commands/multisig/join.py | 2 +- src/keri/app/cli/commands/vc/create.py | 11 ++++--- src/keri/core/eventing.py | 2 +- src/keri/peer/exchanging.py | 7 ++-- src/keri/vc/protocoling.py | 5 +-- 7 files changed, 49 insertions(+), 35 deletions(-) diff --git a/scripts/demo/credentials/multisig-issuer.sh b/scripts/demo/credentials/multisig-issuer.sh index 30f6f2e6d..7c74889dd 100755 --- a/scripts/demo/credentials/multisig-issuer.sh +++ b/scripts/demo/credentials/multisig-issuer.sh @@ -57,36 +57,40 @@ PID_LIST+=" $pid" wait $PID_LIST -# Lets not complicate things with rotation just yet ## Rotate multisig keys: -#kli multisig rotate --name multisig1 --alias multisig & -#pid=$! -#PID_LIST=" $pid" -# -#kli multisig rotate --name multisig2 --alias multisig & -#pid=$! -#PID_LIST+=" $pid" -# -#wait $PID_LIST +kli multisig rotate --name multisig1 --alias multisig --smids EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1 --smids EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4 & +pid=$! +PID_LIST=" $pid" + +kli multisig rotate --name multisig2 --alias multisig --smids EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1 --smids EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4 & +pid=$! +PID_LIST+=" $pid" + +wait $PID_LIST # Issue Credential -kli vc create --name multisig1 --alias multisig --registry-name vLEI --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k --data @${KERI_DEMO_SCRIPT_DIR}/data/credential-data.json & +TIME=$(date -Iseconds -u) +kli vc create --name multisig1 --alias multisig --registry-name vLEI --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k --data @${KERI_DEMO_SCRIPT_DIR}/data/credential-data.json --time "${TIME}" & pid=$! PID_LIST+=" $pid" -# Wait for 3 seconds to allow credential.json to be created, but still launch in parallel because they will wait for each other -echo "Multisig2 looking to join" -kli multisig join --name multisig2 +kli vc create --name multisig2 --alias multisig --registry-name vLEI --schema EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k --data @${KERI_DEMO_SCRIPT_DIR}/data/credential-data.json --time "${TIME}" & +pid=$! +PID_LIST+=" $pid" wait $PID_LIST SAID=$(kli vc list --name multisig1 --alias multisig --issued --said) -kli ipex grant --name multisig1 --alias multisig --said "${SAID}" --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k & +kli ipex grant --name multisig1 --alias multisig --said "${SAID}" --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k --time "${TIME}" & pid=$! PID_LIST+=" $pid" -kli multisig join --name multisig2 +kli ipex grant --name multisig2 --alias multisig --said "${SAID}" --recipient ELjSFdrTdCebJlmvbFNX9-TLhR2PO0_60al1kQp5_e6k --time "${TIME}" & +pid=$! +PID_LIST+=" $pid" + +wait $PID_LIST echo "Polling for holder's IPEX message..." SAID=$(kli ipex list --name holder --alias holder --poll --said) diff --git a/src/keri/app/cli/commands/ipex/grant.py b/src/keri/app/cli/commands/ipex/grant.py index 6c32ae345..87909d3ee 100644 --- a/src/keri/app/cli/commands/ipex/grant.py +++ b/src/keri/app/cli/commands/ipex/grant.py @@ -31,6 +31,7 @@ parser.add_argument("--said", "-s", help="SAID of the credential to grant", required=True) parser.add_argument("--message", "-m", help="optional human readable message to " "send to recipient", required=False, default="") +parser.add_argument("--time", help="timestamp for the revocation", required=False, default=None) def handler(args): @@ -40,16 +41,18 @@ def handler(args): bran=args.bran, said=args.said, recp=args.recipient, - message=args.message) + message=args.message, + timestamp=args.time) return [ed] class GrantDoer(doing.DoDoer): - def __init__(self, name, alias, base, bran, said, recp, message): + def __init__(self, name, alias, base, bran, said, recp, message, timestamp): self.said = said self.recp = recp self.message = message + self.timestamp = timestamp self.hby = existing.setupHby(name=name, base=base, bran=bran) self.hab = self.hby.habByName(alias) self.rgy = credentialing.Regery(hby=self.hby, name=name, base=base) @@ -113,7 +116,8 @@ def grantDo(self, tymth, tock=0.0): anchor=dict(i=iserder.pre, s=seqner.snh, d=iserder.said)) anc = self.hby.db.cloneEvtMsg(pre=serder.pre, fn=0, dig=serder.said) - exn, atc = protocoling.ipexGrantExn(hab=self.hab, recp=recp, message=self.message, acdc=acdc, iss=iss, anc=anc) + exn, atc = protocoling.ipexGrantExn(hab=self.hab, recp=recp, message=self.message, acdc=acdc, iss=iss, anc=anc, + dt=self.timestamp) msg = bytearray(exn.raw) msg.extend(atc) @@ -125,9 +129,9 @@ def grantDo(self, tymth, tock=0.0): smids = self.hab.db.signingMembers(pre=self.hab.pre) smids.remove(self.hab.mhab.pre) - for recp in smids: # this goes to other participants + for part in smids: # this goes to other participants self.postman.send(src=self.hab.mhab.pre, - dest=recp, + dest=part, topic="multisig", serder=wexn, attachment=watc) @@ -136,9 +140,10 @@ def grantDo(self, tymth, tock=0.0): yield self.tock if self.exc.lead(self.hab, said=exn.said): - print(f"Sending grant message {exn.said}...") - credentialing.sendRegistry(self.hby, self.rgy.reger, self.postman, creder, self.hab.pre, recp) - self.postman.send(src=self.hab.pre, + print(f"Sending message {exn.said} to {recp}") + atc = exchanging.serializeMessage(self.hby, exn.said) + del atc[:exn.size] + self.postman.send(src=self.hab.mhab.pre, dest=recp, topic="credential", serder=exn, diff --git a/src/keri/app/cli/commands/multisig/join.py b/src/keri/app/cli/commands/multisig/join.py index 8a964a332..ad399dd25 100644 --- a/src/keri/app/cli/commands/multisig/join.py +++ b/src/keri/app/cli/commands/multisig/join.py @@ -757,7 +757,7 @@ def exn(self, attrs): yield self.tock if self.exc.lead(hab.mhab, said=exn.said): - print(f"Sending grant message {eserder.said} to {recp}") + print(f"Sending message {eserder.said} to {recp}") atc = exchanging.serializeMessage(self.hby, eserder.said) del atc[:eserder.size] self.postman.send(src=hab.mhab.pre, diff --git a/src/keri/app/cli/commands/vc/create.py b/src/keri/app/cli/commands/vc/create.py index fc9a4d5eb..e8e7f09a1 100644 --- a/src/keri/app/cli/commands/vc/create.py +++ b/src/keri/app/cli/commands/vc/create.py @@ -32,8 +32,6 @@ parser.add_argument('--data', '-d', help='Credential data, \'@\' allowed', default=None, action="store", required=False) parser.add_argument('--credential', help='Full credential, \'@\' allowed', default=None, action="store", required=False) -parser.add_argument('--out', '-o', help='Name of file for credential output', default="credential.json", action="store", - required=False) 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', required=True) @@ -41,6 +39,7 @@ action="store_true") parser.add_argument('--passcode', '-p', help='22 character encryption passcode for keystore (is not saved)', dest="bran", default=None) # passcode => bran +parser.add_argument("--time", help="timestamp for the credential creation", required=False, default=None) def issueCredential(args): @@ -100,7 +99,7 @@ def issueCredential(args): edges=edges, rules=rules, credential=credential, - out=args.out, + timestamp=args.time, private=args.private) doers = [issueDoer] @@ -114,7 +113,7 @@ class CredentialIssuer(doing.DoDoer): """ def __init__(self, name, alias, base, bran, registryName=None, schema=None, edges=None, recipient=None, data=None, - rules=None, credential=None, out=None, private=False): + rules=None, credential=None, timestamp=None, private=False): """ Create DoDoer for issuing a credential and managing the processes needed to complete issuance Parameters: @@ -131,6 +130,7 @@ def __init__(self, name, alias, base, bran, registryName=None, schema=None, edge """ self.name = name self.registryName = registryName + self.timestamp = timestamp self.hby = existing.setupHby(name=name, base=base, bran=bran) self.hab = self.hby.habByName(alias) if self.hab is None: @@ -165,6 +165,9 @@ def __init__(self, name, alias, base, bran, registryName=None, schema=None, edge raise ValueError(f"invalid recipient {recipient}") recp = recp[0]['id'] + if self.timestamp is not None: + data["dt"] = self.timestamp + self.creder = self.credentialer.create(regname=registryName, recp=recp, schema=schema, diff --git a/src/keri/core/eventing.py b/src/keri/core/eventing.py index a0357c93c..5d6a060c2 100644 --- a/src/keri/core/eventing.py +++ b/src/keri/core/eventing.py @@ -542,7 +542,7 @@ def fetchTsgs(db, saider, snh=None): prefixer (Prefixer): instance trans signer aid, seqner (Seqner): of sn of trans signer key state est event diger (Diger): of digest of trans signer key state est event - signers (list): of Siger instances of indexed signatures + sigers (list): of Siger instances of indexed signatures Parameters: db: (Cesr diff --git a/src/keri/peer/exchanging.py b/src/keri/peer/exchanging.py index fb75b5860..d02a0a51f 100644 --- a/src/keri/peer/exchanging.py +++ b/src/keri/peer/exchanging.py @@ -245,13 +245,14 @@ def lead(self, hab, said): return True keys = [verfer.qb64 for verfer in hab.kever.verfers] - sigers = self.hby.db.esigs.get(keys=(said,)) - if not sigers: # otherwise its a list of sigs + tsgs = eventing.fetchTsgs(self.hby.db.esigs, coring.Saider(qb64=said)) + if not tsgs: # otherwise it contains a list of sigs return False + (_, _, _, sigers) = tsgs[0] windex = min([siger.index for siger in sigers]) - # True if Elected to perform delegation and witnessing + # True if Elected to send an EXN to its recipient return hab.mhab.kever.verfers[0].qb64 == keys[windex] def complete(self, said): diff --git a/src/keri/vc/protocoling.py b/src/keri/vc/protocoling.py index c8f5e051a..77226211e 100644 --- a/src/keri/vc/protocoling.py +++ b/src/keri/vc/protocoling.py @@ -219,7 +219,7 @@ def ipexAgreeExn(hab, message, offer): return exn, ims -def ipexGrantExn(hab, recp, message, acdc, iss, anc, agree=None): +def ipexGrantExn(hab, recp, message, acdc, iss, anc, agree=None, dt=None): """ Disclose an ACDC Parameters: @@ -230,6 +230,7 @@ def ipexGrantExn(hab, recp, message, acdc, iss, anc, agree=None): iss (bytes): serialized TEL issuance event anc (bytes): serialized anchoring event in the KEL, either ixn or rot agree (Serder): optional IPEX exn agree message that this grant is response to. + dt (str): Iso8601 formatted date string to use for this request Returns: Serder: credential issuance exn peer to peer message @@ -251,7 +252,7 @@ def ipexGrantExn(hab, recp, message, acdc, iss, anc, agree=None): if agree is not None: kwa['dig'] = agree.said - exn, end = exchanging.exchange(route="/ipex/grant", payload=data, sender=hab.pre, embeds=embeds, **kwa) + exn, end = exchanging.exchange(route="/ipex/grant", payload=data, sender=hab.pre, embeds=embeds, date=dt, **kwa) ims = hab.endorse(serder=exn, last=False, pipelined=False) del ims[:exn.size] ims.extend(end) From 5767ae608192ea38b9a0c6bef06bffb5706f2027 Mon Sep 17 00:00:00 2001 From: pfeairheller Date: Wed, 20 Sep 2023 14:26:44 -0700 Subject: [PATCH 4/5] Adding in a group rotation to the non-interactive mutlsig issue script Signed-off-by: pfeairheller --- scripts/demo/credentials/multisig-issuer.sh | 9 +++++++-- src/keri/app/cli/commands/multisig/rotate.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/demo/credentials/multisig-issuer.sh b/scripts/demo/credentials/multisig-issuer.sh index 7c74889dd..5a66b0729 100755 --- a/scripts/demo/credentials/multisig-issuer.sh +++ b/scripts/demo/credentials/multisig-issuer.sh @@ -58,11 +58,16 @@ PID_LIST+=" $pid" wait $PID_LIST ## Rotate multisig keys: -kli multisig rotate --name multisig1 --alias multisig --smids EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1 --smids EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4 & +kli rotate --name multisig1 --alias multisig1 +kli query --name multisig2 --alias multisig2 --prefix EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4 +kli rotate --name multisig2 --alias multisig2 +kli query --name multisig1 --alias multisig1 --prefix EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1 + +kli multisig rotate --name multisig1 --alias multisig --smids EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1:1 --smids EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4:1 & pid=$! PID_LIST=" $pid" -kli multisig rotate --name multisig2 --alias multisig --smids EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1 --smids EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4 & +kli multisig rotate --name multisig2 --alias multisig --smids EJccSRTfXYF6wrUVuenAIHzwcx3hJugeiJsEKmndi5q1:1 --smids EKYLUMmNPZeEs77Zvclf0bSN5IN-mLfLpx2ySb-HDlk4:1 & pid=$! PID_LIST+=" $pid" diff --git a/src/keri/app/cli/commands/multisig/rotate.py b/src/keri/app/cli/commands/multisig/rotate.py index d2e498818..7bf025c54 100644 --- a/src/keri/app/cli/commands/multisig/rotate.py +++ b/src/keri/app/cli/commands/multisig/rotate.py @@ -188,7 +188,7 @@ def rotateDo(self, tymth, tock=0.0, **opts): raise kering.ConfigurationError(f"non-existant event {sn} for rotation member {mid}") evt = self.hby.db.getEvt(dbing.dgKey(mid, bytes(dig))) - serder = coring.Serder(raw=evt) + serder = coring.Serder(raw=bytes(evt)) if not serder.est: raise kering.ConfigurationError(f"invalid event {sn} for rotation member {mid}") From 1cdd2edb39ca8df3530219f6db04b451b2018c4c Mon Sep 17 00:00:00 2001 From: pfeairheller Date: Wed, 20 Sep 2023 14:57:49 -0700 Subject: [PATCH 5/5] Update tests. Signed-off-by: pfeairheller --- tests/app/test_grouping.py | 15 +++--- tests/vc/test_protocoling.py | 88 ++++++++++++++++++------------------ 2 files changed, 51 insertions(+), 52 deletions(-) diff --git a/tests/app/test_grouping.py b/tests/app/test_grouping.py index 5cfdae98e..8865147fc 100644 --- a/tests/app/test_grouping.py +++ b/tests/app/test_grouping.py @@ -3,10 +3,8 @@ tests.app.grouping module """ -import time from contextlib import contextmanager -from hio.base import doing, tyming from keri.app import habbing, grouping, notifying from keri.core import coring, eventing, parsing @@ -697,24 +695,23 @@ def test_multisig_interact(mockHelpingNowUTC): def test_multisig_registry_incept(mockHelpingNowUTC, mockCoringRandomNonce): with openMultiSig(prefix="test") as ((hby1, ghab1), (_, _), (_, _)): - recipient = "EL-f5D0esAFbZTzK9W3wtTgDmncye9IOnF0Z8gRdICIU" vcp = veventing.incept(ghab1.pre) ixn = ghab1.mhab.interact(data=[dict(i=vcp.pre, s="0", d=vcp.said)]) - exn, atc = grouping.multisigRegistryInceptExn(ghab=ghab1, vcp=vcp.raw, ixn=ixn, + exn, atc = grouping.multisigRegistryInceptExn(ghab=ghab1, vcp=vcp.raw, anc=ixn, usage="Issue vLEI Credentials") assert exn.ked["r"] == '/multisig/vcp' - assert exn.saidb == b'EOEQNt4iBGCkQkHQqYkade56WIfg148W8jAo8xunAsxq' + assert exn.saidb == b'ECKiNFo7fpG4vS5tUeja3EvOqT8ctq4AW8E3HKsP7dJo' assert atc == (b'-FABEH__mobl7NDyyQCB1DoLK-OPSueraPtZAlWEjfOYkaba0AAAAAAAAAAAAAAA' - b'AAAAAAAAEH__mobl7NDyyQCB1DoLK-OPSueraPtZAlWEjfOYkaba-AABAAChpUup' - b'y7Wq39vSN3Y2H7aw59WMnFv5QGqMAebjkSKN9gXtcr9Z0uCM5J2I7x1dcvjKgMxO' - b'IKpVB62iizfygDsP-LAa5AACAA-e-ixn-AABAAD2mK9ICW9x1-0NZGkEDOcAbZ58' + b'AAAAAAAAEH__mobl7NDyyQCB1DoLK-OPSueraPtZAlWEjfOYkaba-AABAABh6d0m' + b'lebT57L8o2si7DfEvPCoXJP0ekPiBqkzQns3-P7dz36MPXhjNFW6xRRdUstDLAZe' + b'BEqBxBCltMpTZGsD-LAa5AACAA-e-anc-AABAAD2mK9ICW9x1-0NZGkEDOcAbZ58' b'VWK9LOTwyN2lSfHr2zY638P1SBStoh8mjgy7nOTGMyujOXMKvF_ZDeQ_ISYA') data = exn.ked["a"] assert data == {'gid': 'EERn_laF0qwP8zTBGL86LbF84J0Yh2IvQSRskH3BZZiy', 'usage': 'Issue vLEI Credentials'} assert "vcp" in exn.ked["e"] - assert "ixn" in exn.ked["e"] + assert "anc" in exn.ked["e"] def test_multisig_incept_handler(mockHelpingNowUTC): diff --git a/tests/vc/test_protocoling.py b/tests/vc/test_protocoling.py index ce9d9f503..a527343ba 100644 --- a/tests/vc/test_protocoling.py +++ b/tests/vc/test_protocoling.py @@ -7,7 +7,6 @@ from keri.app import habbing, notifying from keri.core import coring, scheming, parsing from keri.core.eventing import SealEvent -from keri.help import helping from keri.peer import exchanging from keri.vc import protocoling from keri.vc.proving import credential @@ -201,24 +200,26 @@ def test_ipex(seeder, mockCoringRandomNonce, mockHelpingNowIso8601, mockHelpingN # First try a bare grant (no prior agree) anc = sidHab.makeOwnEvent(sn=2) - grant0, grant0atc = protocoling.ipexGrantExn(sidHab, "Here's a credential", acdc=msg, iss=iss.raw, anc=anc) - assert grant0.raw == (b'{"v":"KERI10JSON0004fe_","t":"exn","d":"EE6csOli0VeioLJH5YtmU8U3fGIT4Id0J9xF' - b'NPZ4oURv","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","p":"","dt":"20' + grant0, grant0atc = protocoling.ipexGrantExn(sidHab, message="Here's a credential", recp=sidHab.pre, + acdc=msg, iss=iss.raw, anc=anc) + assert grant0.raw == (b'{"v":"KERI10JSON000531_","t":"exn","d":"EJxM3em5fSpAIQsyXYovrr0UjblWLtmbTnFp' + b'xAUqnwG-","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","p":"","dt":"20' b'21-06-27T21:26:21.233257+00:00","r":"/ipex/grant","q":{},"a":{"m":"Here\'' - b's a credential"},"e":{"acdc":{"v":"ACDC10JSON000197_","d":"EDkftEwWBpohjTpem' - b'h_6xkaGNuoDsRU3qwvHdlvgfOyG","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDj' - b'I3","ri":"EO0_SyqPS1-EVYSITakYpUHaUZZpZGsjaXFOaO_kCfS4","s":"EMQWEcCnVRk1hat' - b'TNyK3sIykYSrrFvafX3bHQ9Gkk1kC","a":{"d":"EF2__B6DiLQHpdJZ_C0bddxy2o6nXIHEwch' - b'O9yylr3xx","dt":"2021-06-27T21:26:21.233257+00:00","i":"EIaGMMWJFPmtXznY1IIi' - b'KDIrg-vIyge6mBl2QV8dDjI3","LEI":"254900OPPU84GM83MG36"}},"iss":{"v":"KERI10J' - b'SON0000ed_","t":"iss","d":"EK2WxcpF3oL1yqS3Z8i08WDYkHDcYhJL9afqdCIZjMy3","i"' - b':"EDkftEwWBpohjTpemh_6xkaGNuoDsRU3qwvHdlvgfOyG","s":"0","ri":"EO0_SyqPS1-EVY' - b'SITakYpUHaUZZpZGsjaXFOaO_kCfS4","dt":"2021-06-27T21:26:21.233257+00:00"},"an' - b'c":{"v":"KERI10JSON00013a_","t":"ixn","d":"EOjAxp-AMLzicGz2h-DxvMK9kicajpZEw' - b'dN8-8k54hvz","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","s":"2","p":' - b'"EGKglEgIpdHuhuwl-IiSDG9x094gMrRxVaXGgXvCzCYM","a":[{"i":"EDkftEwWBpohjTpemh' - b'_6xkaGNuoDsRU3qwvHdlvgfOyG","s":"0","d":"EK2WxcpF3oL1yqS3Z8i08WDYkHDcYhJL9af' - b'qdCIZjMy3"}]},"d":"EI5mZXZ84Su4DrEUOxtl-NaUURQtTJeAn12xf146beg3"}}') + b's a credential","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3"},"e":{"ac' + b'dc":{"v":"ACDC10JSON000197_","d":"EDkftEwWBpohjTpemh_6xkaGNuoDsRU3qwvHdlvgfO' + b'yG","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","ri":"EO0_SyqPS1-EVYS' + b'ITakYpUHaUZZpZGsjaXFOaO_kCfS4","s":"EMQWEcCnVRk1hatTNyK3sIykYSrrFvafX3bHQ9Gk' + b'k1kC","a":{"d":"EF2__B6DiLQHpdJZ_C0bddxy2o6nXIHEwchO9yylr3xx","dt":"2021-06-' + b'27T21:26:21.233257+00:00","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3"' + b',"LEI":"254900OPPU84GM83MG36"}},"iss":{"v":"KERI10JSON0000ed_","t":"iss","d"' + b':"EK2WxcpF3oL1yqS3Z8i08WDYkHDcYhJL9afqdCIZjMy3","i":"EDkftEwWBpohjTpemh_6xka' + b'GNuoDsRU3qwvHdlvgfOyG","s":"0","ri":"EO0_SyqPS1-EVYSITakYpUHaUZZpZGsjaXFOaO_' + b'kCfS4","dt":"2021-06-27T21:26:21.233257+00:00"},"anc":{"v":"KERI10JSON00013a' + b'_","t":"ixn","d":"EOjAxp-AMLzicGz2h-DxvMK9kicajpZEwdN8-8k54hvz","i":"EIaGMMW' + b'JFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","s":"2","p":"EGKglEgIpdHuhuwl-IiSDG9x' + b'094gMrRxVaXGgXvCzCYM","a":[{"i":"EDkftEwWBpohjTpemh_6xkaGNuoDsRU3qwvHdlvgfOy' + b'G","s":"0","d":"EK2WxcpF3oL1yqS3Z8i08WDYkHDcYhJL9afqdCIZjMy3"}]},"d":"EI5mZX' + b'Z84Su4DrEUOxtl-NaUURQtTJeAn12xf146beg3"}}') assert ipexhan.verify(serder=grant0) is True @@ -231,9 +232,9 @@ def test_ipex(seeder, mockCoringRandomNonce, mockHelpingNowIso8601, mockHelpingN # Let's see if we can spurn a message we previously accepted. spurn1, spurn1atc = protocoling.ipexSpurnExn(sidHab, "I reject you", spurned=grant0) - assert spurn1.raw == (b'{"v":"KERI10JSON00011d_","t":"exn","d":"EIIYc4NMfFjConU2eacUDljTxsKJ77biwkMw' - b'AfzkF_Yr","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","p":"EE6csOli0V' - b'eioLJH5YtmU8U3fGIT4Id0J9xFNPZ4oURv","dt":"2021-06-27T21:26:21.233257+00:00",' + assert spurn1.raw == (b'{"v":"KERI10JSON00011d_","t":"exn","d":"EEs0bIGplWsjSOw5BMhAdFmgv-jm3-4nPgcK' + b'-LDv8tdB","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","p":"EJxM3em5fS' + b'pAIQsyXYovrr0UjblWLtmbTnFpxAUqnwG-","dt":"2021-06-27T21:26:21.233257+00:00",' b'"r":"/ipex/spurn","q":{},"a":{"m":"I reject you"},"e":{}}') smsg = bytearray(spurn1.raw) smsg.extend(spurn1atc) @@ -242,26 +243,27 @@ def test_ipex(seeder, mockCoringRandomNonce, mockHelpingNowIso8601, mockHelpingN assert serder.ked == spurn1.ked # This credential grant has been spurned and not accepted into database # Now we'll run a grant pointing back to the agree all the way to the database - grant1, grant1atc = protocoling.ipexGrantExn(sidHab, "Here's a credential", acdc=msg, iss=iss.raw, anc=anc, - agree=agree) - assert grant1.raw == (b'{"v":"KERI10JSON00052a_","t":"exn","d":"EKn9k1v27ZK8TyS-kEMzHNfpbRV1d-tUMbaZ' - b'Mtmx0aeF","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","p":"ECDIZYM_le' + grant1, grant1atc = protocoling.ipexGrantExn(sidHab, message="Here's a credential", acdc=msg, iss=iss.raw, + recp=sidHab.pre, anc=anc, agree=agree) + assert grant1.raw == (b'{"v":"KERI10JSON00055d_","t":"exn","d":"EF3SGMz-op7KoPEhDLTJsxHMKS5VaEFqN_z2' + b'EchbLW48","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","p":"ECDIZYM_le' b'19AYxRef_jfkfHsdrlsiLWofA7LHrpFR43","dt":"2021-06-27T21:26:21.233257+00:00",' - b'"r":"/ipex/grant","q":{},"a":{"m":"Here\'s a credential"},"e":{"acdc":{"v' - b'":"ACDC10JSON000197_","d":"EDkftEwWBpohjTpemh_6xkaGNuoDsRU3qwvHdlvgfOyG","i"' - b':"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","ri":"EO0_SyqPS1-EVYSITakYpU' - b'HaUZZpZGsjaXFOaO_kCfS4","s":"EMQWEcCnVRk1hatTNyK3sIykYSrrFvafX3bHQ9Gkk1kC","' - b'a":{"d":"EF2__B6DiLQHpdJZ_C0bddxy2o6nXIHEwchO9yylr3xx","dt":"2021-06-27T21:2' - b'6:21.233257+00:00","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","LEI":' - b'"254900OPPU84GM83MG36"}},"iss":{"v":"KERI10JSON0000ed_","t":"iss","d":"EK2Wx' - b'cpF3oL1yqS3Z8i08WDYkHDcYhJL9afqdCIZjMy3","i":"EDkftEwWBpohjTpemh_6xkaGNuoDsR' - b'U3qwvHdlvgfOyG","s":"0","ri":"EO0_SyqPS1-EVYSITakYpUHaUZZpZGsjaXFOaO_kCfS4",' - b'"dt":"2021-06-27T21:26:21.233257+00:00"},"anc":{"v":"KERI10JSON00013a_","t":' - b'"ixn","d":"EOjAxp-AMLzicGz2h-DxvMK9kicajpZEwdN8-8k54hvz","i":"EIaGMMWJFPmtXz' - b'nY1IIiKDIrg-vIyge6mBl2QV8dDjI3","s":"2","p":"EGKglEgIpdHuhuwl-IiSDG9x094gMrR' - b'xVaXGgXvCzCYM","a":[{"i":"EDkftEwWBpohjTpemh_6xkaGNuoDsRU3qwvHdlvgfOyG","s":' - b'"0","d":"EK2WxcpF3oL1yqS3Z8i08WDYkHDcYhJL9afqdCIZjMy3"}]},"d":"EI5mZXZ84Su4D' - b'rEUOxtl-NaUURQtTJeAn12xf146beg3"}}') + b'"r":"/ipex/grant","q":{},"a":{"m":"Here\'s a credential","i":"EIaGMMWJFPm' + b'tXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3"},"e":{"acdc":{"v":"ACDC10JSON000197_","d"' + b':"EDkftEwWBpohjTpemh_6xkaGNuoDsRU3qwvHdlvgfOyG","i":"EIaGMMWJFPmtXznY1IIiKDI' + b'rg-vIyge6mBl2QV8dDjI3","ri":"EO0_SyqPS1-EVYSITakYpUHaUZZpZGsjaXFOaO_kCfS4","' + b's":"EMQWEcCnVRk1hatTNyK3sIykYSrrFvafX3bHQ9Gkk1kC","a":{"d":"EF2__B6DiLQHpdJZ' + b'_C0bddxy2o6nXIHEwchO9yylr3xx","dt":"2021-06-27T21:26:21.233257+00:00","i":"E' + b'IaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","LEI":"254900OPPU84GM83MG36"}},' + b'"iss":{"v":"KERI10JSON0000ed_","t":"iss","d":"EK2WxcpF3oL1yqS3Z8i08WDYkHDcYh' + b'JL9afqdCIZjMy3","i":"EDkftEwWBpohjTpemh_6xkaGNuoDsRU3qwvHdlvgfOyG","s":"0","' + b'ri":"EO0_SyqPS1-EVYSITakYpUHaUZZpZGsjaXFOaO_kCfS4","dt":"2021-06-27T21:26:21' + b'.233257+00:00"},"anc":{"v":"KERI10JSON00013a_","t":"ixn","d":"EOjAxp-AMLzicG' + b'z2h-DxvMK9kicajpZEwdN8-8k54hvz","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8' + b'dDjI3","s":"2","p":"EGKglEgIpdHuhuwl-IiSDG9x094gMrRxVaXGgXvCzCYM","a":[{"i":' + b'"EDkftEwWBpohjTpemh_6xkaGNuoDsRU3qwvHdlvgfOyG","s":"0","d":"EK2WxcpF3oL1yqS3' + b'Z8i08WDYkHDcYhJL9afqdCIZjMy3"}]},"d":"EI5mZXZ84Su4DrEUOxtl-NaUURQtTJeAn12xf1' + b'46beg3"}}') assert ipexhan.verify(serder=grant1) is True gmsg = bytearray(grant1.raw) @@ -272,9 +274,9 @@ def test_ipex(seeder, mockCoringRandomNonce, mockHelpingNowIso8601, mockHelpingN # And now the last... admit the granted credential to complete the full flow admit0, admit0atc = protocoling.ipexAdmitExn(sidHab, "Thanks for the credential", grant=grant1) - assert admit0.raw == (b'{"v":"KERI10JSON00012a_","t":"exn","d":"EHdoJ4nxDPOcOPKkEvo5DO7zMECsnPcdw9iB' - b'uwh9YVNN","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","p":"EKn9k1v27Z' - b'K8TyS-kEMzHNfpbRV1d-tUMbaZMtmx0aeF","dt":"2021-06-27T21:26:21.233257+00:00",' + assert admit0.raw == (b'{"v":"KERI10JSON00012a_","t":"exn","d":"EB4c-ygyLg4Q0g9hpE15_DE-nfXyH46AC2zE' + b'-c3L0cB0","i":"EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3","p":"EF3SGMz-op' + b'7KoPEhDLTJsxHMKS5VaEFqN_z2EchbLW48","dt":"2021-06-27T21:26:21.233257+00:00",' b'"r":"/ipex/admit","q":{},"a":{"m":"Thanks for the credential"},"e":{}}') assert ipexhan.verify(serder=admit0) is True