diff --git a/scripts/list_notifications.py b/scripts/list_notifications.py index 75d26f1..e1460be 100644 --- a/scripts/list_notifications.py +++ b/scripts/list_notifications.py @@ -6,17 +6,13 @@ Testing clienting with integration tests that require a running KERIA Cloud Agent """ -import json -from time import sleep - -from keri.app.keeping import Algos from keri.core.coring import Tiers from signify.app.clienting import SignifyClient def list_notifications(): url = "http://localhost:3901" - bran = b'9876543210abcdefghijk' + bran = b'Pwt6yLXRSs7IjZ23tRHIV' tier = Tiers.low client = SignifyClient(passcode=bran, tier=tier, url=url) diff --git a/scripts/multisig-holder.py b/scripts/multisig-holder.py new file mode 100644 index 0000000..84d84b9 --- /dev/null +++ b/scripts/multisig-holder.py @@ -0,0 +1,286 @@ +# -*- encoding: utf-8 -*- +""" +SIGNIFY +signify.app.clienting module + +Testing clienting with integration tests that require a running KERIA Cloud Agent +""" +import json +from time import sleep + +import requests +from keri import kering +from keri.app import signing +from keri.app.keeping import Algos +from keri.core import coring, eventing +from keri.core.coring import Tiers +from keri.help import helping + +from signify.app.clienting import SignifyClient + + +def multisig_holder(): + print("Creating issuer agent") + client0 = create_agent(b'Dmopaoe5tANSD8A5rwIhW', + "EGTZsyZyREvrD-swB4US5n-1r7h-40sVPIrmS14ixuoJ", + "EPkVulMF7So04EJqUDmmHu6SkllpbOt-KJOnSwckmXwz") + + print("Creating issuer AID") + create_aid(client0, "issuer", "W1OnK0b5rKq6TcKBWhsQa", "ELTkSY_C70Qj8SbPh7F121Q3iA_zNlt8bS-pzOMiCBgG") + add_end_role(client0, "issuer") + + print("Creating holder1 agent") + client1 = create_agent(b'PoLT1X6fDQliXyCuzCVuv', + "EBqP5_kfQIsBWPWSKOL0iiaDv-nwVvNsN0YHP7SYKK2u", + "ENEDfnaIJyB-ITwEZGv559Mzdk0lNng3UaQKJWzFoTK0") + + print("Creating holder1 AID") + create_aid(client1, "holder1", "B-GzoqRMFLGtV0Zy0Jajw", "ENIatcaOLTJ3AMCbv0ZiTXR-2HGrJAwsyXVKhQpwuaIq") + add_end_role(client1, "holder1") + + print("Creating holder2 agent") + client2 = create_agent(b'Pwt6yLXRSs7IjZ23tRHIV', + "EA-SUezF76zn7zF7so-T-DF8FsvI9vO1mtOhWjbdRsqK", + "EBn32S-PTYCVZWIhE4jT0l9-23suzNs2z7raYf0YpOSb") + print("Creating holder2 AID") + create_aid(client2, "holder2", "AAuXz_5CvLOXMCtZ1prCS", "EBlzZyyDM2wBzPLPKO0RiMGbYJ1PuryD1-zQOr9fKctV") + add_end_role(client2, "holder2") + + print("Resolving OOBIs") + holder2 = resolve_oobi(client1, "holder2", + "http://127.0.0.1:3902/oobi/EBlzZyyDM2wBzPLPKO0RiMGbYJ1PuryD1-zQOr9fKctV/agent/" + "EBn32S-PTYCVZWIhE4jT0l9-23suzNs2z7raYf0YpOSb") + resolve_oobi(client1, "issuer", + "http://127.0.0.1:3902/oobi/ELTkSY_C70Qj8SbPh7F121Q3iA_zNlt8bS-pzOMiCBgG/agent/" + "EPkVulMF7So04EJqUDmmHu6SkllpbOt-KJOnSwckmXwz") + + holder1 = resolve_oobi(client2, "holder1", + "http://127.0.0.1:3902/oobi/ENIatcaOLTJ3AMCbv0ZiTXR-2HGrJAwsyXVKhQpwuaIq/agent/" + "ENEDfnaIJyB-ITwEZGv559Mzdk0lNng3UaQKJWzFoTK0") + resolve_oobi(client2, "issuer", + "http://127.0.0.1:3902/oobi/ELTkSY_C70Qj8SbPh7F121Q3iA_zNlt8bS-pzOMiCBgG/agent/" + "EPkVulMF7So04EJqUDmmHu6SkllpbOt-KJOnSwckmXwz") + + resolve_oobi(client0, "vc", "http://127.0.0.1:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao") + resolve_oobi(client1, "vc", "http://127.0.0.1:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao") + resolve_oobi(client2, "vc", "http://127.0.0.1:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao") + + words = client1.challenges().generate() + print(f"Challenging holder1 with {words}") + client2.challenges().respond("holder2", holder1['i'], words) + + op = client1.challenges().verify("holder1", holder2['i'], words) + while not op["done"]: + op = client1.operations().get(op['name']) + sleep(1) + + exn = coring.Serder(ked=op["response"]['exn']) + print(f"Challenge signed in {exn.said}") + client1.challenges().responded(holder2['i'], exn.said) + + states = [holder1, holder2] + + member1 = get_aid(client1, "holder1") + member2 = get_aid(client2, "holder2") + + op1 = create_multisig(client1, "holder", member1, states) + op2 = create_multisig(client2, "holder", member2, states) + + gaid1 = wait_on_operation(client1, op1) + print(f"{gaid1['i']} created for holder1") + gaid2 = wait_on_operation(client2, op2) + print(f"{gaid2['i']} created for holder2") + + ghab1 = client1.identifiers().get("holder") + ghab2 = client2.identifiers().get("holder") + + stamp = helping.nowIso8601() + add_end_role_multisig(client1, "holder", ghab1, member1, client1.agent.pre, stamp=stamp) + op1 = add_end_role_multisig(client2, "holder", ghab2, member2, client1.agent.pre, stamp=stamp) + add_end_role_multisig(client1, "holder", ghab1, member1, client2.agent.pre, stamp=stamp) + op2 = add_end_role_multisig(client2, "holder", ghab2, member2, client2.agent.pre, stamp=stamp) + + while not op1["done"]: + op1 = client1.operations().get(op1['name']) + sleep(1) + + while not op2["done"]: + op2 = client1.operations().get(op2['name']) + sleep(1) + + holder = resolve_oobi(client0, "holder", "http://127.0.0.1:3902/oobi/EH_axvx0v0gwQaCawqem5u8ZeDKx9TUWKsowTa_xj0yb") + + print(holder) + create_credential(client0, holder) + + +def create_agent(bran, controller, agent): + url = "http://localhost:3901" + tier = Tiers.low + client = SignifyClient(passcode=bran, tier=tier) + assert client.controller == controller + + evt, siger = client.ctrl.event() + + res = requests.post(url="http://localhost:3903/boot", + json=dict( + icp=evt.ked, + sig=siger.qb64, + stem=client.ctrl.stem, + pidx=1, + tier=client.ctrl.tier)) + + if res.status_code != requests.codes.accepted: + raise kering.AuthNError(f"unable to initialize cloud agent connection, {res.status_code}, {res.text}") + + client.connect(url=url, ) + assert client.agent is not None + print("Agent created:") + print(f" Agent: {client.agent.pre} Controller: {client.agent.delpre}") + assert client.agent.pre == agent + assert client.agent.delpre == controller + return client + + +def create_aid(client, name, bran, expected): + identifiers = client.identifiers() + (_, _, op) = identifiers.create(name, bran=bran) + icp = op["response"] + serder = coring.Serder(ked=icp) + assert serder.pre == expected + print(f"AID Created: {serder.pre}") + + +def resolve_oobi(client, alias, url): + oobis = client.oobis() + operations = client.operations() + + op = oobis.resolve(oobi=url, + alias=alias) + + print(f"resolving oobi for {alias}") + while not op["done"]: + op = operations.get(op["name"]) + sleep(1) + + print("... done") + return op["response"] + + +def create_multisig(client, name, member, states): + identifiers = client.identifiers() + exchanges = client.exchanges() + + icp, isigs, op = identifiers.create(name, algo=Algos.group, mhab=member, + isith=["1/2", "1/2"], nsith=["1/2", "1/2"], + states=states, + rstates=states) + + smids = [state['i'] for state in states] + recps = [x['i'] for x in states if x['i'] != member['prefix']] + + embeds = dict( + icp=eventing.messagize(serder=icp, sigers=[coring.Siger(qb64=sig) for sig in isigs]) + ) + + exchanges.send(member['name'], "multisig", sender=member, route="/multisig/icp", + payload=dict(gid=icp.pre, smids=smids, rmids=smids), + embeds=embeds, recipients=recps) + + return op + + +def get_aid(client, name): + identifiers = client.identifiers() + return identifiers.get(name) + + +def wait_on_operation(client, op): + operations = client.operations() + while not op["done"]: + op = operations.get(op["name"]) + sleep(1) + + return op["response"] + + +def add_end_role(client, name): + identifiers = client.identifiers() + identifiers.addEndRole(name, eid=client.agent.pre) + + +def add_end_role_multisig(client, name, ghab, m, eid, stamp=None): + exchanges = client.exchanges() + identifiers = client.identifiers() + + rpy, sigs, op = identifiers.addEndRole(name, eid=eid, stamp=stamp) + + gstate = ghab["state"] + seal = eventing.SealEvent(i=ghab["prefix"], s=gstate["ee"]["s"], d=gstate["ee"]["d"]) + ims = eventing.messagize(serder=rpy, sigers=[coring.Siger(qb64=sig) for sig in sigs], seal=seal) + embeds = dict( + rpy=ims + ) + + members = identifiers.members(name) + recps = [] + for member in members['signing']: + recp = member['aid'] + if recp == m['prefix']: + continue + + recps.append(recp) + + exn, _, _ = exchanges.send(m['name'], "multisig", sender=m, route="/multisig/rpy", + payload=dict(gid=ghab['prefix']), + embeds=embeds, recipients=recps) + + return op + + +def create_credential(client, holder): + registries = client.registries() + identifiers = client.identifiers() + credentials = client.credentials() + operations = client.operations() + ipex = client.ipex() + exchanges = client.exchanges() + + issuer = identifiers.get("issuer") + + print("Creating vLEI Registry") + _, _, _, op = registries.create(hab=issuer, registryName="vLEI") + while not op["done"]: + op = operations.get(op["name"]) + sleep(1) + print("... created") + + issuer = identifiers.get("issuer") + registry = registries.get(name="issuer", registryName="vLEI") + data = { + "LEI": "5493001KJTIIGC8Y1R17" + } + creder, iserder, anc, sigs, op = credentials.create(issuer, registry, data=data, + schema="EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao", + recipient=holder['i']) + print(f"Creating credential {creder.said}") + while not op["done"]: + op = operations.get(op["name"]) + sleep(1) + print("... created") + + prefixer = coring.Prefixer(qb64=iserder.pre) + seqner = coring.Seqner(sn=iserder.sn) + acdc = signing.serialize(creder, prefixer, seqner, iserder.saider) + iss = registries.serialize(iserder, anc) + + grant, sigs, end = ipex.grant(issuer, recp=holder['i'], acdc=acdc, + iss=iss, message="", + anc=eventing.messagize(serder=anc, sigers=[coring.Siger(qb64=sig) for sig in sigs])) + print(f"Sending grant {grant.said}") + exchanges.sendFromEvents("issuer", "credential", grant, sigs, end, [holder['i']]) + print("... sent") + + +if __name__ == "__main__": + multisig_holder() diff --git a/src/signify/app/aiding.py b/src/signify/app/aiding.py index 2cf01e6..98b0817 100644 --- a/src/signify/app/aiding.py +++ b/src/signify/app/aiding.py @@ -196,7 +196,7 @@ def addEndRole(self, name, *, role=Roles.agent, eid=None, stamp=None): ) res = self.client.post(f"/identifiers/{name}/endroles", json=json) - return res.json() + return rpy, sigs, res.json() def sign(self, name, ser): hab = self.get(name) diff --git a/src/signify/app/challenging.py b/src/signify/app/challenging.py new file mode 100644 index 0000000..66a2983 --- /dev/null +++ b/src/signify/app/challenging.py @@ -0,0 +1,77 @@ +# -*- encoding: utf-8 -*- +""" +SIGNIFY +signify.app.challenging module + +""" +from signify.app.clienting import SignifyClient + + +class Challenges: + """ Challenges domain object """ + + def __init__(self, client: SignifyClient): + """ Create domain class for working with credentials for a single AID + + Parameters: + client (SignifyClient): Signify client class for access resources on a KERIA service instance + + """ + self.client = client + + def generate(self): + """ Request 12 random word challenge phrase from server + + Returns: + list: array of 12 random words + + """ + + res = self.client.get("/challenges") + resp = res.json() + return resp["words"] + + def respond(self, name, recp, words): + hab = self.client.identifiers().get(name) + exchanges = self.client.exchanges() + + _, _, res = exchanges.send(name, "challenge", sender=hab, route="/challenge/response", + payload=dict(words=words), + embeds=dict(), recipients=[recp]) + + return res + + def verify(self, name, source, words): + """ Ask Agent to verify a given sender signed the provided words + + Parameters: + name (str): human readable name of AID environment + source(str): qb64 AID of source of challenge response to check for + words(list): list of challenge words to check for + """ + + json = dict( + words=words + ) + + res = self.client.post(f"/challenges/{name}/verify/{source}", json=json) + return res.json() + + def responded(self, name, source, said): + """ Mark challenge response as signed and accepted + + Parameters: + name (str): human readable name of AID environment + source (str): qb64 AID of signer + said (str): qb64 AID of exn message representing the signed response + + Returns: + bool: True means successful + + """ + json = dict( + said=said + ) + + res = self.client.post(f"/challenges/{name}/verify/{source}", json=json) + return res.json() diff --git a/src/signify/app/clienting.py b/src/signify/app/clienting.py index 68cf612..cd4cf0c 100644 --- a/src/signify/app/clienting.py +++ b/src/signify/app/clienting.py @@ -255,6 +255,10 @@ def ipex(self): from signify.app.credentialing import Ipex return Ipex(client=self) + def challenges(self): + from signify.app.challenging import Challenges + return Challenges(client=self) + @staticmethod def raiseForStatus(res): try: diff --git a/src/signify/peer/exchanging.py b/src/signify/peer/exchanging.py index 981a422..693d2eb 100644 --- a/src/signify/peer/exchanging.py +++ b/src/signify/peer/exchanging.py @@ -46,11 +46,13 @@ def send(self, name, topic, sender, route, payload, embeds, recipients): tpc=topic, exn=exn.ked, sigs=sigs, - atc=atc.decode("utf-8"), + atc=atc, rec=recipients ) - return exn, sigs, self.client.post(f"/identifiers/{name}/exchanges", json=body) + res = self.client.post(f"/identifiers/{name}/exchanges", json=body) + + return exn, sigs, res.json() def createExchangeMessage(self, sender, route, payload, embeds, dt=None): """ Create exn message from parameters and return Serder with signatures and additional attachments. @@ -78,7 +80,7 @@ def createExchangeMessage(self, sender, route, payload, embeds, dt=None): sigs = keeper.sign(ser=exn.raw) - return exn, sigs, bytes(end) + return exn, sigs, bytes(end).decode("utf-8") def sendFromEvents(self, name, topic, exn, sigs, atc, recipients): """ Send precreated exn message to recipients @@ -104,5 +106,6 @@ def sendFromEvents(self, name, topic, exn, sigs, atc, recipients): rec=recipients ) - self.client.post(f"/identifiers/{name}/exchanges", json=body) + res = self.client.post(f"/identifiers/{name}/exchanges", json=body) + return res.json() diff --git a/tests/app/test_aiding.py b/tests/app/test_aiding.py index 4e1d137..6b4510b 100644 --- a/tests/app/test_aiding.py +++ b/tests/app/test_aiding.py @@ -9,6 +9,7 @@ import pytest from mockito import mock, verify, verifyNoUnwantedInteractions, unstub, expect + def test_aiding_list(): from signify.app.clienting import SignifyClient mock_client = mock(spec=SignifyClient, strict=True) @@ -376,7 +377,9 @@ def test_aiding_add_end_role(): expect(mock_client, times=1).post('/identifiers/aid1/endroles', json=expected_data).thenReturn(mock_response) expect(mock_response, times=1).json().thenReturn({'success': 'yay'}) - out = ids.addEndRole('aid1') + serder, sig, out = ids.addEndRole('aid1') + assert serder == mock_serder + assert sig == ['a signature'] assert out['success'] == 'yay' verifyNoUnwantedInteractions() diff --git a/tests/app/test_challenging.py b/tests/app/test_challenging.py new file mode 100644 index 0000000..210f4c6 --- /dev/null +++ b/tests/app/test_challenging.py @@ -0,0 +1,115 @@ +# -*- encoding: utf-8 -*- +""" +SIGNIFY +signify.app.test_challenging module + +Testing challenge with unit tests +""" + +import pytest +from mockito import mock, verify, verifyNoUnwantedInteractions, unstub, expect + + +def test_challenges_generate(): + from signify.app.clienting import SignifyClient + mock_client = mock(spec=SignifyClient, strict=True) + + from signify.app.challenging import Challenges + chas = Challenges(client=mock_client) # type: ignore + + from requests import Response + mock_response = mock({}, spec=Response, strict=True) + expect(mock_client, times=1).get('/challenges').thenReturn(mock_response) + expect(mock_response, times=1).json().thenReturn( + {"words": ["word", "one", "two", "three"]} + ) + + out = chas.generate() + assert out == ["word", "one", "two", "three"] + + verifyNoUnwantedInteractions() + unstub() + + +def test_challenge_verify(): + from signify.app.clienting import SignifyClient + mock_client = mock(spec=SignifyClient, strict=True) + + from signify.app.challenging import Challenges + chas = Challenges(client=mock_client) # type: ignore + + name = "test" + source = "E123" + words = ["word", "one", "two", "three"] + from requests import Response + mock_response = mock({}, spec=Response, strict=True) + expect(mock_client, times=1).post(f'/challenges/{name}/verify/{source}', + json=dict(words=words)).thenReturn(mock_response) + expect(mock_response, times=1).json().thenReturn( + {"done": False} + ) + + out = chas.verify(name, source, words) + assert out["done"] is False + + verifyNoUnwantedInteractions() + unstub() + + +def test_challenge_responded(): + from signify.app.clienting import SignifyClient + mock_client = mock(spec=SignifyClient, strict=True) + + from signify.app.challenging import Challenges + chas = Challenges(client=mock_client) # type: ignore + + name = "test" + source = "E123" + said = "E456" + from requests import Response + mock_response = mock({}, spec=Response, strict=True) + expect(mock_client, times=1).post(f'/challenges/{name}/verify/{source}', + json=dict(said=said)).thenReturn(mock_response) + expect(mock_response, times=1).json().thenReturn( + {"done": False} + ) + + out = chas.responded(name, source, said) + assert out["done"] is False + + verifyNoUnwantedInteractions() + unstub() + + +def test_challenge_respond(): + from signify.app.clienting import SignifyClient + mock_client = mock(spec=SignifyClient, strict=True) + + from signify.app.aiding import Identifiers + mock_ids = Identifiers(client=mock_client) # type: ignore + + from signify.peer.exchanging import Exchanges + mock_exc = Exchanges(client=mock_client) # type: ignore + + from signify.app.challenging import Challenges + chas = Challenges(client=mock_client) # type: ignore + + mock_hab = {} + name = "test" + recp = "E123" + words = ["word", "one", "two", "three"] + from requests import Response + mock_response = mock({}, spec=Response, strict=True) + expect(mock_client, times=1).identifiers().thenReturn(mock_ids) + expect(mock_ids, times=1).get(name).thenReturn(mock_hab) + expect(mock_client, times=1).exchanges().thenReturn(mock_exc) + expect(mock_exc, times=1).send(name, "challenge", sender=mock_hab, route="/challenge/response", + payload=dict(words=words), + embeds=dict(), + recipients=[recp]).thenReturn((None, None, mock_response)) + + out = chas.respond(name, recp, words) + assert out == mock_response + + verifyNoUnwantedInteractions() + unstub() diff --git a/tests/app/test_credentialing.py b/tests/app/test_credentialing.py index 2c30689..3a21345 100644 --- a/tests/app/test_credentialing.py +++ b/tests/app/test_credentialing.py @@ -163,7 +163,7 @@ def test_ipex(): mock_anc = {} mock_grant = {} mock_gsigs = [] - mock_end = b'' + mock_end = "" expect(mock_client, times=1).exchanges().thenReturn(mock_excs) expect(mock_excs).createExchangeMessage(sender=mock_hab, route="/ipex/grant", payload={'m': 'this is a test',