From 9b5cab62937f74eb3de301c76ed0f7124e115fd0 Mon Sep 17 00:00:00 2001 From: iFergal Date: Tue, 30 Jul 2024 14:16:58 +0100 Subject: [PATCH 1/3] feat: multi-sig IPEX apply, offer, agree --- src/keria/app/ipexing.py | 168 ++++++++++- tests/app/test_ipexing.py | 594 +++++++++++++++++++++++++++++++++++++- 2 files changed, 742 insertions(+), 20 deletions(-) diff --git a/src/keria/app/ipexing.py b/src/keria/app/ipexing.py index 37289c2f..50c42c39 100644 --- a/src/keria/app/ipexing.py +++ b/src/keria/app/ipexing.py @@ -119,7 +119,7 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): # Have to add the atc to the end... this will be Pathed signatures for embeds if not atc: - raise falcon.HTTPBadRequest(description=f"attachment missing for ACDC, unable to process request.") + raise falcon.HTTPBadRequest(description=f"attachment missing for multi-sig admit, unable to process request.") # use that data to create th Serder and Sigers for the exn serder = serdering.SerderKERI(sad=ked) @@ -134,7 +134,6 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): # make a copy and parse agent.hby.psr.parseOne(ims=bytearray(ims)) - agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) exn, pathed = exchanging.cloneMessage(agent.hby, serder.said) if not exn: @@ -144,6 +143,8 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): if grant is None: raise falcon.HTTPBadRequest(description=f"attempt to admit an invalid grant {admitked['p']}") + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) + embeds = grant.ked['e'] acdc = embeds["acdc"] issr = acdc['i'] @@ -245,6 +246,9 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): if grant['r'] != "/ipex/grant": raise falcon.HTTPBadRequest(description=f"invalid route for embedded ipex grant {ked['r']}") + if not atc: + raise falcon.HTTPBadRequest(description=f"attachment missing for multi-sig grant, unable to process request.") + # use that data to create th Serder and Sigers for the exn serder = serdering.SerderKERI(sad=ked) sigers = [core.Siger(qb64=sig) for sig in sigs] @@ -259,18 +263,19 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): # make a copy and parse agent.hby.psr.parseOne(ims=bytearray(ims)) - agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) - holder = grant['a']['i'] exn, pathed = exchanging.cloneMessage(agent.hby, serder.said) if not exn: raise falcon.HTTPBadRequest(description=f"invalid exn request message {serder.said}") + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) + + grantRec = grant['a']['i'] serder = serdering.SerderKERI(sad=grant) ims = bytearray(serder.raw) + pathed['exn'] agent.hby.psr.parseOne(ims=ims) - agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=[holder], topic="credential")) - agent.grants.append(dict(said=grant['d'], pre=hab.pre, rec=[holder])) + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=[grantRec], topic="credential")) + agent.grants.append(dict(said=grant['d'], pre=hab.pre, rec=[grantRec])) return agent.monitor.submit(serder.pre, longrunning.OpTypes.exchange, metadata=dict(said=serder.said)) @@ -306,6 +311,7 @@ def on_post(req, rep, name): ked = httping.getRequiredParam(body, "exn") sigs = httping.getRequiredParam(body, "sigs") + atc = httping.getRequiredParam(body, "atc") rec = httping.getRequiredParam(body, "rec") route = ked['r'] @@ -313,6 +319,8 @@ def on_post(req, rep, name): match route: case "/ipex/apply": op = IpexApplyCollectionEnd.sendApply(agent, hab, ked, sigs, rec) + case "/multisig/exn": + op = IpexApplyCollectionEnd.sendMultisigExn(agent, hab, ked, sigs, atc, rec) case _: raise falcon.HTTPBadRequest(description=f"invalid message route {route}") @@ -333,7 +341,6 @@ def sendApply(agent, hab, ked, sigs, rec): kever = hab.kever seal = eventing.SealEvent(i=hab.pre, s="{:x}".format(kever.lastEst.s), d=kever.lastEst.d) - # in this case, ims is a message is a sealed and signed message - signed by Signify (KERIA can't sign anything here...) ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) # make a copy and parse @@ -342,6 +349,51 @@ def sendApply(agent, hab, ked, sigs, rec): agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) return agent.monitor.submit(serder.pre, longrunning.OpTypes.exchange, metadata=dict(said=serder.said)) + @staticmethod + def sendMultisigExn(agent, hab, ked, sigs, atc, rec): + if not isinstance(hab, habbing.SignifyGroupHab): + raise falcon.HTTPBadRequest(description=f"attempt to send multisig message with non-group AID={hab.pre}") + + for recp in rec: # Have to verify we already know all the recipients. + if recp not in agent.hby.kevers: + raise falcon.HTTPBadRequest(description=f"attempt to send to unknown AID={recp}") + + embeds = ked['e'] + applyked = embeds['exn'] + if applyked['r'] != "/ipex/apply": + raise falcon.HTTPBadRequest(description=f"invalid route for embedded ipex apply {ked['r']}") + + if not atc: + raise falcon.HTTPBadRequest(description=f"attachment missing for multi-sig apply, unable to process request.") + + # use that data to create th Serder and Sigers for the exn + serder = serdering.SerderKERI(sad=ked) + sigers = [core.Siger(qb64=sig) for sig in sigs] + + # Now create the stream to send, need the signer seal + kever = hab.mhab.kever + seal = eventing.SealEvent(i=hab.mhab.pre, s="{:x}".format(kever.lastEst.s), d=kever.lastEst.d) + + ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) + ims.extend(atc.encode("utf-8")) # add the pathed attachments + + # make a copy and parse + agent.hby.psr.parseOne(ims=bytearray(ims)) + exn, pathed = exchanging.cloneMessage(agent.hby, serder.said) + if not exn: + raise falcon.HTTPBadRequest(description=f"invalid exn request message {serder.said}") + + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) + + applyRec = applyked['a']['i'] + serder = serdering.SerderKERI(sad=applyked) + ims = bytearray(serder.raw) + pathed['exn'] + agent.hby.psr.parseOne(ims=ims) + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=[applyRec], topic="credential")) + + return agent.monitor.submit(serder.pre, longrunning.OpTypes.exchange, metadata=dict(said=serder.said)) + + class IpexOfferCollectionEnd: @staticmethod @@ -380,6 +432,8 @@ def on_post(req, rep, name): match route: case "/ipex/offer": op = IpexOfferCollectionEnd.sendOffer(agent, hab, ked, sigs, atc, rec) + case "/multisig/exn": + op = IpexOfferCollectionEnd.sendMultisigExn(agent, hab, ked, sigs, atc, rec) case _: raise falcon.HTTPBadRequest(description=f"invalid route {route}") @@ -409,6 +463,55 @@ def sendOffer(agent, hab, ked, sigs, atc, rec): agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) return agent.monitor.submit(serder.pre, longrunning.OpTypes.exchange, metadata=dict(said=serder.said)) + @staticmethod + def sendMultisigExn(agent, hab, ked, sigs, atc, rec): + if not isinstance(hab, habbing.SignifyGroupHab): + raise falcon.HTTPBadRequest(description=f"attempt to send multisig message with non-group AID={hab.pre}") + + for recp in rec: # Have to verify we already know all the recipients. + if recp not in agent.hby.kevers: + raise falcon.HTTPBadRequest(description=f"attempt to send to unknown AID={recp}") + + embeds = ked['e'] + offerked = embeds['exn'] + if offerked['r'] != "/ipex/offer": + raise falcon.HTTPBadRequest(description=f"invalid route for embedded ipex offer {ked['r']}") + + if not atc: + raise falcon.HTTPBadRequest(description=f"attachment missing for multi-sig offer, unable to process request.") + + # use that data to create th Serder and Sigers for the exn + serder = serdering.SerderKERI(sad=ked) + sigers = [core.Siger(qb64=sig) for sig in sigs] + + # Now create the stream to send, need the signer seal + kever = hab.mhab.kever + seal = eventing.SealEvent(i=hab.mhab.pre, s="{:x}".format(kever.lastEst.s), d=kever.lastEst.d) + + ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) + ims.extend(atc.encode("utf-8")) # add the pathed attachments + + # make a copy and parse + agent.hby.psr.parseOne(ims=bytearray(ims)) + exn, pathed = exchanging.cloneMessage(agent.hby, serder.said) + if not exn: + raise falcon.HTTPBadRequest(description=f"invalid exn request message {serder.said}") + + apply, _ = exchanging.cloneMessage(agent.hby, offerked['p']) + if apply is None: + raise falcon.HTTPBadRequest(description=f"attempt to offer linked to an invalid apply {offerked['p']}") + + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) + + offerRec = offerked['a']['i'] + serder = serdering.SerderKERI(sad=offerked) + ims = bytearray(serder.raw) + pathed['exn'] + agent.hby.psr.parseOne(ims=ims) + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=[offerRec], topic="credential")) + + return agent.monitor.submit(serder.pre, longrunning.OpTypes.exchange, metadata=dict(said=serder.said)) + + class IpexAgreeCollectionEnd: @staticmethod @@ -439,6 +542,7 @@ def on_post(req, rep, name): ked = httping.getRequiredParam(body, "exn") sigs = httping.getRequiredParam(body, "sigs") + atc = httping.getRequiredParam(body, "atc") rec = httping.getRequiredParam(body, "rec") route = ked['r'] @@ -446,6 +550,8 @@ def on_post(req, rep, name): match route: case "/ipex/agree": op = IpexAgreeCollectionEnd.sendAgree(agent, hab, ked, sigs, rec) + case "/multisig/exn": + op = IpexAgreeCollectionEnd.sendMultisigExn(agent, hab, ked, sigs, atc, rec) case _: raise falcon.HTTPBadRequest(description=f"invalid route {route}") @@ -473,3 +579,51 @@ def sendAgree(agent, hab, ked, sigs, rec): agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) return agent.monitor.submit(serder.pre, longrunning.OpTypes.exchange, metadata=dict(said=serder.said)) + + @staticmethod + def sendMultisigExn(agent, hab, ked, sigs, atc, rec): + if not isinstance(hab, habbing.SignifyGroupHab): + raise falcon.HTTPBadRequest(description=f"attempt to send multisig message with non-group AID={hab.pre}") + + for recp in rec: # Have to verify we already know all the recipients. + if recp not in agent.hby.kevers: + raise falcon.HTTPBadRequest(description=f"attempt to send to unknown AID={recp}") + + embeds = ked['e'] + agreeKed = embeds['exn'] + if agreeKed['r'] != "/ipex/agree": + raise falcon.HTTPBadRequest(description=f"invalid route for embedded ipex agree {ked['r']}") + + if not atc: + raise falcon.HTTPBadRequest(description=f"attachment missing for multi-sig agree, unable to process request.") + + # use that data to create th Serder and Sigers for the exn + serder = serdering.SerderKERI(sad=ked) + sigers = [core.Siger(qb64=sig) for sig in sigs] + + # Now create the stream to send, need the signer seal + kever = hab.mhab.kever + seal = eventing.SealEvent(i=hab.mhab.pre, s="{:x}".format(kever.lastEst.s), d=kever.lastEst.d) + + ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) + ims.extend(atc.encode("utf-8")) # add the pathed attachments + + # make a copy and parse + agent.hby.psr.parseOne(ims=bytearray(ims)) + exn, pathed = exchanging.cloneMessage(agent.hby, serder.said) + if not exn: + raise falcon.HTTPBadRequest(description=f"invalid exn request message {serder.said}") + + apply, _ = exchanging.cloneMessage(agent.hby, agreeKed['p']) + if apply is None: + raise falcon.HTTPBadRequest(description=f"attempt to agree linked to an invalid offer {agreeKed['p']}") + + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) + + agreeRec = agreeKed['a']['i'] + serder = serdering.SerderKERI(sad=agreeKed) + ims = bytearray(serder.raw) + pathed['exn'] + agent.hby.psr.parseOne(ims=ims) + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=[agreeRec], topic="credential")) + + return agent.monitor.submit(serder.pre, longrunning.OpTypes.exchange, metadata=dict(said=serder.said)) diff --git a/tests/app/test_ipexing.py b/tests/app/test_ipexing.py index 75b20c25..f878eaef 100644 --- a/tests/app/test_ipexing.py +++ b/tests/app/test_ipexing.py @@ -124,6 +124,28 @@ def test_ipex_admit(helpers, mockHelpingNowIso8601): assert len(agent.exchanges) == 1 assert len(agent.admits) == 1 + # Test sending embedded admit in multisig/exn message + ims = eventing.messagize(serder=admitSerder, sigers=[core.Siger(qb64=sigs[0])]) + exn, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=pre, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=exn.ked, + sigs=sigs, + atc=end.decode("utf-8"), + rec=["EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM"] + ) + + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/test/ipex/admit", body=data) + assert res.status_code == 400 + assert res.json == {'description': 'attempt to send multisig message with non-group ' + 'AID=EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'title': '400 Bad Request'} + def test_ipex_grant(helpers, mockHelpingNowIso8601, seeder): salt = b'0123456789abcdef' @@ -341,24 +363,32 @@ def test_ipex_grant(helpers, mockHelpingNowIso8601, seeder): 'title': '400 Bad Request'} -def test_multisig_grant_admit(seeder, helpers): +def test_multisig(seeder, helpers): with (helpers.openKeria(salter=core.Salter(raw=b'0123456789abcM00')) as (agency0, agent0, app0, client0), \ helpers.openKeria(salter=core.Salter(raw=b'0123456789abcM01')) as (agency1, agent1, app1, client1), \ helpers.openKeria(salter=core.Salter(raw=b'0123456789abcM02')) as (hagency0, hagent0, happ0, hclient0), \ - helpers.openKeria(salter=core.Salter(raw=b'0123456789abcM03')) as (hagency1, hagent1, happ1, hclient1)): + helpers.openKeria(salter=core.Salter(raw=b'0123456789abcM03')) as (hagency1, hagent1, happ1, hclient1), \ + helpers.openKeria(salter=core.Salter(raw=b'0123456789abcM04')) as (vagency0, vagent0, vapp0, vclient0), \ + helpers.openKeria(salter=core.Salter(raw=b'0123456789abcM05')) as (vagency1, vagent1, vapp1, vclient1)): tock = 0.03125 doist = doing.Doist(tock=tock, real=True) - deeds = doist.enter(doers=[agent0, hagent0, agent1, hagent1]) + deeds = doist.enter(doers=[agent0, hagent0, vagent0, agent1, hagent1, vagent1]) # Seed database with credential schema - for agent in [agent0, agent1, hagent0, hagent1]: + for agent in [agent0, agent1, hagent0, hagent1, vagent0, vagent1]: seeder.seedSchema(agent.hby.db) - for app in [app0, app1, happ0, happ1]: + for app in [app0, app1, happ0, happ1, vapp0, vapp1]: # Register the GRANT and ADMIT endpoints - grantAnd = ipexing.IpexGrantCollectionEnd() - app.add_route("/identifiers/{name}/ipex/grant", grantAnd) + applyEnd = ipexing.IpexApplyCollectionEnd() + app.add_route("/identifiers/{name}/ipex/apply", applyEnd) + offerEnd = ipexing.IpexOfferCollectionEnd() + app.add_route("/identifiers/{name}/ipex/offer", offerEnd) + agreeEnd = ipexing.IpexAgreeCollectionEnd() + app.add_route("/identifiers/{name}/ipex/agree", agreeEnd) + grantEnd = ipexing.IpexGrantCollectionEnd() + app.add_route("/identifiers/{name}/ipex/grant", grantEnd) admitEnd = ipexing.IpexAdmitCollectionEnd() app.add_route("/identifiers/{name}/ipex/admit", admitEnd) @@ -583,29 +613,173 @@ def test_multisig_grant_admit(seeder, helpers): res = hclient1.simulate_post(path=f"/identifiers/holder/endroles", json=body) assert res.status_code == 202 + # Create Verifier Participant 0 + vsalt0 = b'0123456789abcM04' + op = helpers.createAid(vclient0, "verifierParticipant0", vsalt0) + vaid0 = op["response"] + vpre0 = vaid0['i'] + assert vpre0 == "EIT6fOnmo9lQeb17xlPdjhfcP2Qu3MSKlvh8u73b4Yh8" + _, signers0 = helpers.incept(vsalt0, "signify:aid", pidx=0) + verifierSigner0 = signers0[0] + + # Create Verfier Participant 1 + vsalt1 = b'0123456789abcM05' + op = helpers.createAid(vclient1, "verifierParticipant1", vsalt1) + vaid1 = op["response"] + vpre1 = vaid1['i'] + assert vpre1 == "ED79UO_yvUBIQrE4s8F1qaO003isaVhW-ovK-RxjSKgY" + _, signers1 = helpers.incept(vsalt1, "signify:aid", pidx=0) + verifierSigner1 = signers1[0] + + # Get their hab dicts + vp0 = vclient0.simulate_get("/identifiers/verifierParticipant0").json + vp1 = vclient1.simulate_get("/identifiers/verifierParticipant1").json + + assert vp0["prefix"] == vpre0 + assert vp1["prefix"] == vpre1 + + # Introduce the participants to each other + v0Hab = vagent0.hby.habByName("verifierParticipant0") + ims = v0Hab.replyToOobi(v0Hab.pre, role=Roles.agent) + vagent1.parser.parse(ims=bytearray(ims)) + v1Hab = vagent1.hby.habByName("verifierParticipant1") + ims = v1Hab.replyToOobi(v1Hab.pre, role=Roles.agent) + vagent0.parser.parse(ims=bytearray(ims)) + + vkeys = [vp0['state']['k'][0], vp1['state']['k'][0]] + ndigs = [vp0['state']['n'][0], vp1['state']['n'][0]] + + # Create the Verifier multisig inception event + serder = eventing.incept(keys=vkeys, + isith="2", + nsith="2", + ndigs=ndigs, + code=coring.MtrDex.Blake3_256, + toad=0, + wits=[]) + verifierPre = serder.said + assert verifierPre == "ECDzt2_W3Mk5Nw92-kJwtVz1uV-wyJJbDnOqvMjtBH2L" + + sigers = [verifierSigner0.sign(ser=serder.raw, index=0).qb64, verifierSigner1.sign(ser=serder.raw, index=1).qb64] + states = [vp0['state'], vp1['state']] + smids = rmids = [state['i'] for state in states if 'i' in state] + + body = { + 'name': 'verifier', + 'icp': serder.ked, + 'sigs': sigers, + "smids": smids, + "rmids": rmids, + 'group': { + "mhab": vp0, + "keys": vkeys, + "ndigs": ndigs + } + } + + res = vclient0.simulate_post(path="/identifiers", body=json.dumps(body)) + assert res.status_code == 202 + + body = { + 'name': 'verifier', + 'icp': serder.ked, + 'sigs': sigers, + "smids": smids, + "rmids": rmids, + 'group': { + "mhab": vp1, + "keys": vkeys, + "ndigs": ndigs + } + } + res = vclient1.simulate_post(path="/identifiers", body=json.dumps(body)) + assert res.status_code == 202 + + while not vagent0.counselor.complete(prefixer=coring.Prefixer(qb64=serder.pre), seqner=coring.Seqner(sn=0)): + doist.recur(deeds=deeds) + + assert vagent1.counselor.complete(prefixer=coring.Prefixer(qb64=serder.pre), seqner=coring.Seqner(sn=0)) is True + + verifier = vclient0.simulate_get("/identifiers/verifier").json + assert verifier['prefix'] == verifierPre + + # Lets add both endroles for verifier multisig + rpy = helpers.endrole(verifierPre, vagent0.agentHab.pre) + sigs = [verifierSigner0.sign(ser=rpy.raw, index=0).qb64, verifierSigner1.sign(ser=rpy.raw, index=1).qb64] + body = dict(rpy=rpy.ked, sigs=sigs) + + res = vclient0.simulate_post(path=f"/identifiers/verifier/endroles", json=body) + assert res.status_code == 202 + res = vclient1.simulate_post(path=f"/identifiers/verifier/endroles", json=body) + assert res.status_code == 202 + # Introduce the multisig AIDs to each other for name, agent in [("issuer", agent0), ("issuerParticipant0", agent0), ("issuerParticipant1", agent1)]: issuerHab = agent.hby.habByName(name) ims = issuerHab.replyToOobi(issuerHab.pre, role=Roles.agent) + + # Introduce to holder hagent0.parser.parse(ims=bytearray(ims)) - hagent1.parser.parse(ims=ims) + hagent1.parser.parse(ims=bytearray(ims)) while issuerHab.pre not in hagent0.hby.kevers: doist.recur(deeds=deeds) assert issuerHab.pre in hagent1.hby.kevers + # Introduce to verifier + vagent0.parser.parse(ims=bytearray(ims)) + vagent1.parser.parse(ims=ims) + + while issuerHab.pre not in vagent0.hby.kevers: + doist.recur(deeds=deeds) + + assert issuerHab.pre in vagent1.hby.kevers + for name, agent in [("holder", hagent0), ("holderParticipant0", hagent0), ("holderParticipant1", hagent1)]: holderHab = agent.hby.habByName(name) ims = holderHab.replyToOobi(holderHab.pre, role=Roles.agent) + + # Introduce to issuer agent0.parser.parse(ims=bytearray(ims)) - agent1.parser.parse(ims=ims) + agent1.parser.parse(ims=bytearray(ims)) while holderHab.pre not in agent0.hby.kevers: doist.recur(deeds=deeds) assert holderHab.pre in agent1.hby.kevers + # Introduce to verifier + vagent0.parser.parse(ims=bytearray(ims)) + vagent1.parser.parse(ims=ims) + + while holderHab.pre not in vagent0.hby.kevers: + doist.recur(deeds=deeds) + + assert holderHab.pre in vagent1.hby.kevers + + for name, agent in [("verifier", vagent0), ("verifierParticipant0", vagent0), ("verifierParticipant1", vagent1)]: + verifierHab = agent.hby.habByName(name) + ims = verifierHab.replyToOobi(verifierHab.pre, role=Roles.agent) + + # Introduce to issuer + agent0.parser.parse(ims=bytearray(ims)) + agent1.parser.parse(ims=bytearray(ims)) + + while verifierHab.pre not in agent0.hby.kevers: + doist.recur(deeds=deeds) + + assert verifierHab.pre in agent1.hby.kevers + + # Introduce to holder + hagent0.parser.parse(ims=bytearray(ims)) + hagent1.parser.parse(ims=ims) + + while verifierHab.pre not in hagent0.hby.kevers: + doist.recur(deeds=deeds) + + assert verifierHab.pre in hagent1.hby.kevers + # Create credential registry nonce = Salter().qb64 regser = veventing.incept(issuerPre, @@ -868,6 +1042,334 @@ def test_multisig_grant_admit(seeder, helpers): # Ensure that the credential has been persisted by both agents assert hagent1.rgy.reger.saved.get(keys=(creder.said,)) is not None + # Verifier and holder go through full apply -> admit flow with the issued credential + # Verifier starts with APPLY + applySerder, end = exchanging.exchange(route="/ipex/apply", + payload={'m': 'Applying for a credential', 's': schema, 'a': {'LEI': '254900DA0GOGCFVWB618'}}, + sender=verifierPre, + embeds=dict(), + recipient=holderPre, + date=helping.nowIso8601()) + + applySigers = [verifierSigner0.sign(ser=applySerder.raw, index=0), + verifierSigner1.sign(ser=applySerder.raw, index=1)] + seal = eventing.SealEvent(i=verifierPre, s="0", d=verifierPre) + ims = eventing.messagize(serder=applySerder, sigers=applySigers, seal=seal) + + multiExnSerder0, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=vpre0, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=multiExnSerder0.ked, + sigs=[verifierSigner0.sign(ser=multiExnSerder0.raw, index=0).qb64], + atc=end.decode("utf-8"), + rec=[vpre1] + ) + + data = json.dumps(body).encode("utf-8") + res = vclient0.simulate_post(path="/identifiers/verifier/ipex/apply", body=data) + assert res.status_code == 200 + + multiExnSerder1, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=vpre1, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=multiExnSerder1.ked, + sigs=[verifierSigner1.sign(ser=multiExnSerder1.raw, index=0).qb64], + atc=end.decode("utf-8"), + rec=[vpre0] + ) + + data = json.dumps(body).encode("utf-8") + res = vclient1.simulate_post(path="/identifiers/verifier/ipex/apply", body=data) + assert res.status_code == 200 + + while vagent0.exc.complete(said=applySerder.said) is not True: + doist.recur(deeds=deeds) + + assert vagent1.exc.complete(said=applySerder.said) is True + + exn, pathed = exchanging.cloneMessage(vagent0.hby, multiExnSerder0.said) + assert exn is not None + + apply = serdering.SerderKERI(sad=exn.ked['e']['exn']) + ims = bytearray(apply.raw) + pathed['exn'] + hagent0.hby.psr.parseOne(ims=bytearray(ims)) + hagent1.hby.psr.parseOne(ims=ims) + + assert hagent0.hby.db.exns.get(keys=(apply.said,)) is not None + assert hagent1.hby.db.exns.get(keys=(apply.said,)) is not None + + # Holder replies with an OFFER + embeds = dict(acdc=acdc) + offerSerder, end = exchanging.exchange(route="/ipex/offer", + payload={'m': 'How about this'}, + sender=holderPre, + embeds=embeds, + recipient=verifierPre, + dig=apply.said, + date=helping.nowIso8601()) + + offerSigers = [holderSigner0.sign(ser=offerSerder.raw, index=0), + holderSigner1.sign(ser=offerSerder.raw, index=1)] + seal = eventing.SealEvent(i=holderPre, s="0", d=holderPre) + ims = eventing.messagize(serder=offerSerder, sigers=offerSigers, seal=seal) + + multiExnSerder0, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=hpre0, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=multiExnSerder0.ked, + sigs=[holderSigner0.sign(ser=multiExnSerder0.raw, index=0).qb64], + atc=end.decode("utf-8"), + rec=[hpre1] + ) + + data = json.dumps(body).encode("utf-8") + res = hclient0.simulate_post(path="/identifiers/holder/ipex/offer", body=data) + assert res.status_code == 200 + + multiExnSerder1, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=hpre1, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=multiExnSerder1.ked, + sigs=[holderSigner1.sign(ser=multiExnSerder1.raw, index=0).qb64], + atc=end.decode("utf-8"), + rec=[hpre0] + ) + + data = json.dumps(body).encode("utf-8") + res = hclient1.simulate_post(path="/identifiers/holder/ipex/offer", body=data) + assert res.status_code == 200 + + while hagent0.exc.complete(said=offerSerder.said) is not True: + doist.recur(deeds=deeds) + + assert hagent1.exc.complete(said=offerSerder.said) is True + + exn, pathed = exchanging.cloneMessage(hagent0.hby, multiExnSerder0.said) + assert exn is not None + + offer = serdering.SerderKERI(sad=exn.ked['e']['exn']) + ims = bytearray(offer.raw) + pathed['exn'] + vagent0.hby.psr.parseOne(ims=bytearray(ims)) + vagent1.hby.psr.parseOne(ims=ims) + + assert vagent0.hby.db.exns.get(keys=(offer.said,)) is not None + assert vagent1.hby.db.exns.get(keys=(offer.said,)) is not None + + # Verifier replies with an AGREE + agreeSerder, end = exchanging.exchange(route="/ipex/agree", + payload={'m': 'OK!'}, + sender=verifierPre, + embeds=dict(), + recipient=holderPre, + dig=offerSerder.said, + date=helping.nowIso8601()) + + agreeSigers = [verifierSigner0.sign(ser=agreeSerder.raw, index=0), + verifierSigner1.sign(ser=agreeSerder.raw, index=1)] + seal = eventing.SealEvent(i=verifierPre, s="0", d=verifierPre) + ims = eventing.messagize(serder=agreeSerder, sigers=agreeSigers, seal=seal) + + multiExnSerder0, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=vpre0, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=multiExnSerder0.ked, + sigs=[verifierSigner0.sign(ser=multiExnSerder0.raw, index=0).qb64], + atc=end.decode("utf-8"), + rec=[vpre1] + ) + + data = json.dumps(body).encode("utf-8") + res = vclient0.simulate_post(path="/identifiers/verifier/ipex/agree", body=data) + assert res.status_code == 200 + + multiExnSerder1, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=vpre1, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=multiExnSerder1.ked, + sigs=[verifierSigner1.sign(ser=multiExnSerder1.raw, index=0).qb64], + atc=end.decode("utf-8"), + rec=[vpre0] + ) + + data = json.dumps(body).encode("utf-8") + res = vclient1.simulate_post(path="/identifiers/verifier/ipex/agree", body=data) + assert res.status_code == 200 + + while vagent0.exc.complete(said=agreeSerder.said) is not True: + doist.recur(deeds=deeds) + + assert vagent1.exc.complete(said=agreeSerder.said) is True + + exn, pathed = exchanging.cloneMessage(vagent0.hby, multiExnSerder0.said) + assert exn is not None + + agree = serdering.SerderKERI(sad=exn.ked['e']['exn']) + ims = bytearray(agree.raw) + pathed['exn'] + hagent0.hby.psr.parseOne(ims=bytearray(ims)) + hagent1.hby.psr.parseOne(ims=ims) + + assert hagent0.hby.db.exns.get(keys=(agree.said,)) is not None + assert hagent1.hby.db.exns.get(keys=(agree.said,)) is not None + + # Holder presents with a GRANT, linked to the AGREE chain + embeds = dict( + acdc=acdc, + iss=iss, + anc=anc + ) + + grantSerder, end = exchanging.exchange(route="/ipex/grant", + payload=dict(), + sender=holderPre, + embeds=embeds, + dig=agreeSerder.said, + recipient=verifierPre, + date=helping.nowIso8601()) + + grantSigers = [holderSigner0.sign(ser=grantSerder.raw, index=0), + holderSigner1.sign(ser=grantSerder.raw, index=1)] + seal = eventing.SealEvent(i=holderPre, s="0", d=holderPre) + ims = eventing.messagize(serder=grantSerder, sigers=grantSigers, seal=seal) + ims += end + + multiExnSerder0, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=hpre0, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=multiExnSerder0.ked, + sigs=[holderSigner0.sign(ser=multiExnSerder0.raw, index=0).qb64], + atc=end.decode("utf-8"), + rec=[hpre1] + ) + + data = json.dumps(body).encode("utf-8") + res = hclient0.simulate_post(path="/identifiers/holder/ipex/grant", body=data) + + assert res.status_code == 200 + + multiExnSerder1, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=hpre1, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=multiExnSerder1.ked, + sigs=[holderSigner1.sign(ser=multiExnSerder1.raw, index=0).qb64], + atc=end.decode("utf-8"), + rec=[hpre0] + ) + + data = json.dumps(body).encode("utf-8") + res = hclient1.simulate_post(path="/identifiers/holder/ipex/grant", body=data) + assert res.status_code == 200 + + while hagent0.exc.complete(said=grantSerder.said) is not True: + doist.recur(deeds=deeds) + + assert hagent1.exc.complete(said=grantSerder.said) is True + + exn, pathed = exchanging.cloneMessage(hagent0.hby, multiExnSerder0.said) + assert exn is not None + + grant = serdering.SerderKERI(sad=exn.ked['e']['exn']) + ims = bytearray(grant.raw) + pathed['exn'] + vagent0.hby.psr.parseOne(ims=bytearray(ims)) + vagent1.hby.psr.parseOne(ims=ims) + + assert vagent0.hby.db.exns.get(keys=(grant.said,)) is not None + assert vagent1.hby.db.exns.get(keys=(grant.said,)) is not None + + # Verifier accepts and ADMITs presentation + admitSerder, end = exchanging.exchange(route="/ipex/admit", + payload={'m': 'Accepted'}, + sender=verifierPre, + recipient=holderPre, + dig=grant.said, + date=helping.nowIso8601()) + + admitSigers = [verifierSigner0.sign(ser=admitSerder.raw, index=0), + verifierSigner1.sign(ser=admitSerder.raw, index=1)] + seal = eventing.SealEvent(i=verifierPre, s="0", d=verifierPre) + ims = eventing.messagize(serder=admitSerder, sigers=admitSigers, seal=seal) + + multiExnSerder0, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=vpre0, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=multiExnSerder0.ked, + sigs=[verifierSigner0.sign(ser=multiExnSerder0.raw, index=0).qb64], + atc=end.decode("utf-8"), + rec=[vpre1] + ) + + data = json.dumps(body).encode("utf-8") + res = vclient0.simulate_post(path="/identifiers/verifier/ipex/admit", body=data) + assert res.status_code == 200 + + multiExnSerder1, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=vpre1, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=multiExnSerder1.ked, + sigs=[verifierSigner1.sign(ser=multiExnSerder1.raw, index=0).qb64], + atc=end.decode("utf-8"), + rec=[vpre0] + ) + + data = json.dumps(body).encode("utf-8") + res = vclient1.simulate_post(path="/identifiers/verifier/ipex/admit", body=data) + assert res.status_code == 200 + + while vagent0.exc.complete(said=admitSerder.said) is not True: + doist.recur(deeds=deeds) + + assert vagent1.exc.complete(said=admitSerder.said) is True + + exn, pathed = exchanging.cloneMessage(vagent0.hby, multiExnSerder0.said) + assert exn is not None + + admit = serdering.SerderKERI(sad=exn.ked['e']['exn']) + ims = bytearray(admit.raw) + pathed['exn'] + hagent0.hby.psr.parseOne(ims=bytearray(ims)) + hagent1.hby.psr.parseOne(ims=ims) + + assert hagent0.hby.db.exns.get(keys=(admit.said,)) is not None + assert hagent1.hby.db.exns.get(keys=(admit.said,)) is not None + # Get latest state ip0 = client0.simulate_get("/identifiers/issuerParticipant0").json ip1 = client1.simulate_get("/identifiers/issuerParticipant1").json @@ -1016,6 +1518,28 @@ def test_ipex_apply(helpers, mockHelpingNowIso8601): assert res.status_code == 200 assert len(agent.exchanges) == 1 + # Test sending embedded apply in multisig/exn message + ims = eventing.messagize(serder=applySerder, sigers=[core.Siger(qb64=sigs[0])]) + exn, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=pre, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=exn.ked, + sigs=sigs, + atc=end.decode("utf-8"), + rec=["EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM"] + ) + + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/test/ipex/apply", body=data) + assert res.status_code == 400 + assert res.json == {'description': 'attempt to send multisig message with non-group ' + 'AID=EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'title': '400 Bad Request'} + def test_ipex_offer(helpers, mockHelpingNowIso8601): with helpers.openKeria() as (_, agent, app, client): @@ -1163,6 +1687,28 @@ def test_ipex_offer(helpers, mockHelpingNowIso8601): assert res.status_code == 200 assert len(agent.exchanges) == 2 + # Test sending embedded offer in multisig/exn message + ims = eventing.messagize(serder=offer0Serder, sigers=[core.Siger(qb64=sigs[0])]) + exn, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=pre, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=exn.ked, + sigs=sigs, + atc=end.decode("utf-8"), + rec=["EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM"] + ) + + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/test/ipex/offer", body=data) + assert res.status_code == 400 + assert res.json == {'description': 'attempt to send multisig message with non-group ' + 'AID=EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'title': '400 Bad Request'} + def test_ipex_agree(helpers, mockHelpingNowIso8601): with helpers.openKeria() as (_, agent, app, client): @@ -1187,14 +1733,14 @@ def test_ipex_agree(helpers, mockHelpingNowIso8601): assert pre1 == "EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm" dig = "EB_Lr3fHezn1ygn-wbBT5JjzaCMxTmhUoegXeZzWC2eT" - offerSerder, end = exchanging.exchange(route="/ipex/agree", + agreeSerder, end = exchanging.exchange(route="/ipex/agree", payload={'m': 'Agreed'}, sender=pre, embeds=dict(), dig=dig, recipient=pre1, date=helping.nowIso8601()) - assert offerSerder.ked == {'a': {'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'm': 'Agreed'}, + assert agreeSerder.ked == {'a': {'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'm': 'Agreed'}, 'd': 'ENMBCgTGXxiMuTMcfGWp4uqnsiso1Jm3tAAn1x7ZPRox', 'dt': '2021-06-27T21:26:21.233257+00:00', 'e': {}, @@ -1209,7 +1755,7 @@ def test_ipex_agree(helpers, mockHelpingNowIso8601): sigs = ["AAAa70b4QnTOtGOsMqcezMtVzCFuRJHGeIMkWYHZ5ZxGIXM0XDVAzkYdCeadfPfzlKC6dkfiwuJ0IzLOElaanUgH"] body = dict( - exn=offerSerder.ked, + exn=agreeSerder.ked, sigs=sigs, atc="", rec=["EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM"] @@ -1224,7 +1770,7 @@ def test_ipex_agree(helpers, mockHelpingNowIso8601): 'title': '400 Bad Request'} body = dict( - exn=offerSerder.ked, + exn=agreeSerder.ked, sigs=sigs, atc="", rec=[pre1] @@ -1247,3 +1793,25 @@ def test_ipex_agree(helpers, mockHelpingNowIso8601): assert res.status_code == 200 assert len(agent.exchanges) == 1 + + # Test sending embedded agree in multisig/exn message + ims = eventing.messagize(serder=agreeSerder, sigers=[core.Siger(qb64=sigs[0])]) + exn, end = exchanging.exchange(route="/multisig/exn", + payload=dict(), + sender=pre, + embeds=dict(exn=ims), + date=helping.nowIso8601()) + + body = dict( + exn=exn.ked, + sigs=sigs, + atc=end.decode("utf-8"), + rec=["EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM"] + ) + + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/test/ipex/agree", body=data) + assert res.status_code == 400 + assert res.json == {'description': 'attempt to send multisig message with non-group ' + 'AID=EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'title': '400 Bad Request'} From ef79b098ef553303c59ed7d5486a52c2c5b67c9a Mon Sep 17 00:00:00 2001 From: iFergal Date: Tue, 30 Jul 2024 21:16:40 +0100 Subject: [PATCH 2/3] fix: atc should only be required for multi-sig --- src/keria/app/ipexing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/keria/app/ipexing.py b/src/keria/app/ipexing.py index 50c42c39..6762bd67 100644 --- a/src/keria/app/ipexing.py +++ b/src/keria/app/ipexing.py @@ -311,7 +311,6 @@ def on_post(req, rep, name): ked = httping.getRequiredParam(body, "exn") sigs = httping.getRequiredParam(body, "sigs") - atc = httping.getRequiredParam(body, "atc") rec = httping.getRequiredParam(body, "rec") route = ked['r'] @@ -320,6 +319,7 @@ def on_post(req, rep, name): case "/ipex/apply": op = IpexApplyCollectionEnd.sendApply(agent, hab, ked, sigs, rec) case "/multisig/exn": + atc = httping.getRequiredParam(body, "atc") op = IpexApplyCollectionEnd.sendMultisigExn(agent, hab, ked, sigs, atc, rec) case _: raise falcon.HTTPBadRequest(description=f"invalid message route {route}") @@ -542,7 +542,6 @@ def on_post(req, rep, name): ked = httping.getRequiredParam(body, "exn") sigs = httping.getRequiredParam(body, "sigs") - atc = httping.getRequiredParam(body, "atc") rec = httping.getRequiredParam(body, "rec") route = ked['r'] @@ -551,6 +550,7 @@ def on_post(req, rep, name): case "/ipex/agree": op = IpexAgreeCollectionEnd.sendAgree(agent, hab, ked, sigs, rec) case "/multisig/exn": + atc = httping.getRequiredParam(body, "atc") op = IpexAgreeCollectionEnd.sendMultisigExn(agent, hab, ked, sigs, atc, rec) case _: raise falcon.HTTPBadRequest(description=f"invalid route {route}") From 91532ab3ca21d2202eff957846d7b5e51a0a3003 Mon Sep 17 00:00:00 2001 From: iFergal Date: Tue, 20 Aug 2024 11:35:52 +0100 Subject: [PATCH 3/3] build: lock keripy version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 06b12350..c85d7947 100644 --- a/setup.py +++ b/setup.py @@ -76,7 +76,7 @@ python_requires='>=3.12.2', install_requires=[ 'hio>=0.6.14', - 'keri>=1.2.0.dev11', + 'keri==1.2.0.dev11', 'mnemonic>=0.21', 'multicommand>=1.0.0', 'falcon>=3.1.3',