From e003059c0b83b0390b64c1f1dff2e87cc05f3091 Mon Sep 17 00:00:00 2001 From: Kent Bull Date: Wed, 25 Mar 2026 16:22:21 -0600 Subject: [PATCH] Fix Randy impl: nxts->nnxts bug, encrypt/decrypt params, add tests Coded by Codex with prompts from Kent Bull --- src/signify/core/authing.py | 2 +- src/signify/core/keeping.py | 4 +- tests/app/test_aiding.py | 101 ++++++++++++++++++ tests/core/test_authing.py | 85 +++++++-------- tests/core/test_keeping.py | 6 +- .../test_provisioning_and_identifiers.py | 55 ++++++++++ 6 files changed, 204 insertions(+), 49 deletions(-) diff --git a/src/signify/core/authing.py b/src/signify/core/authing.py index 70d47b4..5aa459a 100644 --- a/src/signify/core/authing.py +++ b/src/signify/core/authing.py @@ -237,7 +237,7 @@ def rotate(self, nbran, aids): for nxt in nxts: nnxts.append(self.recrypt(nxt, decrypter, encrypter)) - keys[pre] = dict(prxs=nprxs, nxts=nxts) + keys[pre] = dict(prxs=nprxs, nxts=nnxts) data["keys"] = keys return data diff --git a/src/signify/core/keeping.py b/src/signify/core/keeping.py index ba4b8be..92ec73b 100644 --- a/src/signify/core/keeping.py +++ b/src/signify/core/keeping.py @@ -349,13 +349,13 @@ def rotate(self, ncodes, transferable, **_): return verfers, digers def sign(self, ser, indexed=True, indices=None, ondices=None, **_): - signers = [self.decrypter.decrypt(ser=signing.Cipher(qb64=prx).qb64b, transferable=self.transferable) + signers = [self.decrypter.decrypt(cipher=signing.Cipher(qb64=prx), transferable=self.transferable) for prx in self.prxs] return self.__sign__(ser, signers=signers, indexed=indexed, indices=indices, ondices=ondices) def signers(self): """Return signer objects decrypted from the keeper's current key set.""" - return [self.decrypter.decrypt(ser=signing.Cipher(qb64=prx).qb64b, transferable=self.transferable) + return [self.decrypter.decrypt(cipher=signing.Cipher(qb64=prx), transferable=self.transferable) for prx in self.prxs] diff --git a/tests/app/test_aiding.py b/tests/app/test_aiding.py index d0f9d0d..f25e068 100644 --- a/tests/app/test_aiding.py +++ b/tests/app/test_aiding.py @@ -103,6 +103,54 @@ def test_aiding_create(): unstub() +def test_aiding_create_randy(): + from signify.core import keeping + mock_keeper = mock( + {'params': lambda: {'prxs': ['enc current'], 'nxts': ['enc next'], 'transferable': True}}, + spec=keeping.RandyKeeper, + strict=True, + ) + mock_manager = mock(spec=keeping.Manager, strict=True) + + from mockito import kwargs + expect(mock_manager, times=1).new('randy', 0, **kwargs).thenReturn(mock_keeper) + + keys = ['a signer verfer qb64'] + ndigs = ['next signer digest'] + + expect(mock_keeper, times=1).incept(transferable=True).thenReturn((keys, ndigs)) + + from keri.core import serdering + mock_serder = mock({'raw': b'raw bytes', 'ked': {'a': 'key event dictionary'}}, spec=serdering.SerderKERI, + strict=True) + + from keri.core import eventing + expect(eventing, times=1).incept(keys=keys, isith='1', nsith='1', ndigs=ndigs, code='E', wits=[], toad='0', + cnfg=[], data=[]).thenReturn(mock_serder) + expect(mock_keeper, times=1).sign(mock_serder.raw).thenReturn(['a signature']) + + from signify.app.clienting import SignifyClient + mock_client = mock({'pidx': 0}, spec=SignifyClient, strict=True) + mock_client.manager = mock_manager # type: ignore + + from signify.app.aiding import Identifiers + ids = Identifiers(client=mock_client) # type: ignore + + from requests import Response + resp = mock({'json': lambda: {'post': 'success'}}, spec=Response, strict=True) + expect(mock_client, times=1).post('/identifiers', json={'name': 'new_aid', 'icp': {'a': 'key event dictionary'}, + 'sigs': ['a signature'], 'proxy': None, + 'randy': {'prxs': ['enc current'], 'nxts': ['enc next'], + 'transferable': True}}).thenReturn(resp) + + ids.create(name='new_aid', algo='randy') + + assert mock_client.pidx == 1 + + verifyNoUnwantedInteractions() + unstub() + + def test_aiding_create_cnfg(): from signify.core import keeping mock_keeper = mock({'params': lambda: {'keeper': 'params'}}, spec=keeping.SaltyKeeper, strict=True) @@ -468,6 +516,59 @@ def test_aiding_rotate(): unstub() +def test_aiding_rotate_randy(): + from signify.app.clienting import SignifyClient + mock_client = mock(spec=SignifyClient, strict=True) + + from signify.core import keeping + mock_manager = mock(spec=keeping.Manager, strict=True) + mock_client.manager = mock_manager # type: ignore + + from signify.app.aiding import Identifiers + ids = Identifiers(client=mock_client) # type: ignore + + mock_hab = {'prefix': 'hab prefix', 'name': 'aid1', + 'state': {'s': '0', 'd': 'hab digest', 'b': ['wit1', 'wit2', 'wit3'], 'k': ['key1']}} + expect(ids, times=1).get('aid1').thenReturn(mock_hab) + + mock_keeper = mock( + {'algo': 'randy', 'params': lambda: {'prxs': ['enc current'], 'nxts': ['enc next'], 'transferable': True}}, + spec=keeping.RandyKeeper, + strict=True, + ) + expect(mock_manager, times=1).get(mock_hab).thenReturn(mock_keeper) + + keys = ['key1'] + ndigs = ['ndig1'] + expect(mock_keeper, times=1).rotate(ncodes=['A'], transferable=True, states=None, rstates=None).thenReturn( + (keys, ndigs) + ) + + from keri.core import serdering + mock_serder = mock({'ked': {'a': 'key event dictionary'}, 'raw': b'serder raw bytes'}, spec=serdering.SerderKERI, + strict=True) + + from keri.core import eventing + expect(eventing, times=1).rotate(pre='hab prefix', keys=['key1'], dig='hab digest', sn=1, isith='1', nsith='1', + ndigs=['ndig1'], toad=None, wits=['wit1', 'wit2', 'wit3'], + cuts=[], adds=[], data=[]).thenReturn(mock_serder) + + expect(mock_keeper, times=1).sign(ser=mock_serder.raw).thenReturn(['a signature']) + + from requests import Response + mock_response = mock(spec=Response, strict=True) + expected_data = {'rot': {'a': 'key event dictionary'}, 'sigs': ['a signature'], + 'randy': {'prxs': ['enc current'], 'nxts': ['enc next'], 'transferable': True}} + expect(mock_client, times=1).post('/identifiers/aid1/events', json=expected_data).thenReturn(mock_response) + expect(mock_response, times=1).json().thenReturn({'success': 'yay'}) + + _, _, out = ids.rotate(name='aid1') + assert out['success'] == 'yay' + + verifyNoUnwantedInteractions() + unstub() + + def test_aiding_add_end_role(): from signify.app.clienting import SignifyClient mock_client = mock(spec=SignifyClient, strict=True) diff --git a/tests/core/test_authing.py b/tests/core/test_authing.py index 2f34027..8197a3e 100644 --- a/tests/core/test_authing.py +++ b/tests/core/test_authing.py @@ -249,47 +249,44 @@ def test_controller_rotate_salty(): assert 'sxlt' in out['keys']['ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK'] # type: ignore assert out['keys']['ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK']['sxlt'] != "1AAH2R_SPhr_5vIBGGtyVamaGVDQAcYlgmwDOkJwM-q6Qw8K5NT7jLzJ0k6_7sa3oyKK33ym8JX1Il4MoUiy8ixYwsVWYhaU3sMT" # type: ignore -# def test_controller_rotate_randy(): -# from keri.core.coring import Tiers -# from signify.core.authing import Controller -# ctrl = Controller(bran="abcdefghijklmnop01234", tier=Tiers.low) - -# from keri.core.signing import Salter -# mock_salter = mock({'qb64': 'salter qb64'}, spec=Salter, strict=True) -# ctrl.salter = mock_salter - -# from signify.core.keeping import SaltyCreator -# mock_creator = mock({'create': lambda ridx, tier : None}, spec=SaltyCreator, strict=True) -# from signify.core import keeping -# when(keeping).SaltyCreator(salt='salter qb64', stem='signify:controller', tier=Tiers.low).thenReturn(mock_creator) - -# from keri.core.signing import Signer -# mock_signer = mock(spec=Signer, strict=True) -# ctrl.signer = mock_signer -# mock_nsigner = mock(spec=Signer, strict=True) -# ctrl.nsigner = mock_nsigner -# ctrl.keys = ['a key'] -# from keri.core.coring import Diger -# mock_diger = mock(spec=Diger, strict=True) -# ctrl.ndigs = [mock_diger] - -# from keri.core.serdering import Serder -# mock_serder = mock(spec=Serder, strict=True) -# ctrl.serder = mock_serder - -# # end controller mock setup -# assert ctrl.bran == '0AAabcdefghijklmnop01234' - -# # mocks for rotate -# when(mock_salter).signer(transferable=False).thenReturn(mock_nsigner) -# mock_nsalter = mock(spec=Salter, strict=True) -# from keri.core import coring -# when(coring).Salter(qb64='0AA0123456789abcdefghijk').thenReturn(mock_nsalter) - - # from signify.core.keeping import SaltyCreator - # mock_ncreator = mock(spec=SaltyCreator, strict=True) - - # from signify.core import keeping - # expect(keeping, times=1).SaltyCreator(salt='salter qb64', stem='signify:controller', tier=Tiers.low).thenReturn(mock_ncreator) - - # ctrl.rotate(nbran="0123456789abcdefghijk", aids=["aid_one"],) +def test_controller_rotate_randy(): + from signify.core.authing import Controller + from signify.core.keeping import RandyKeeper + ctrl = Controller(bran="abcdefghijklmnop01234", tier=Tiers.low) + + keeper = RandyKeeper(ctrl.salter, transferable=True) + pubs, digers = keeper.incept(transferable=True) + + aid = { + "name": "aid1", + "prefix": "ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK", + "randy": keeper.params(), + "state": { + "k": pubs, + }, + } + + old_prxs = list(aid["randy"]["prxs"]) + old_nxts = list(aid["randy"]["nxts"]) + + out = ctrl.rotate(nbran="0123456789abcdefghijk", aids=[aid]) + + rotated = out["keys"]["ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK"] + assert rotated["prxs"] != old_prxs + assert rotated["nxts"] != old_nxts + + from keri.core import coring, signing + signer = ctrl.salter.signer(transferable=False) + decrypter = signing.Decrypter(seed=signer.qb64) + + signers = [ + decrypter.decrypt(cipher=signing.Cipher(qb64=prx), transferable=True) + for prx in rotated["prxs"] + ] + assert [signer.verfer.qb64 for signer in signers] == pubs + + nsigners = [ + decrypter.decrypt(cipher=signing.Cipher(qb64=nxt), transferable=True) + for nxt in rotated["nxts"] + ] + assert [coring.Diger(ser=nsigner.verfer.qb64b).qb64 for nsigner in nsigners] == digers diff --git a/tests/core/test_keeping.py b/tests/core/test_keeping.py index 138c13c..5332971 100644 --- a/tests/core/test_keeping.py +++ b/tests/core/test_keeping.py @@ -677,6 +677,8 @@ def test_randy_keeper_rotate(): assert verfers == ['signer verfer qb64'] assert digers == ['diger qb64'] + assert rk.prxs == ['nxt qb64'] + assert rk.nxts == ['cipher qb64'] verifyNoUnwantedInteractions() unstub() @@ -709,7 +711,7 @@ def test_randy_keeper_sign(): from keri.core.signing import Signer mock_verfer = mock({'qb64': 'signer verfer qb64'}, spec=Verfer, strict=True) mock_signer = mock({'verfer': mock_verfer}, spec=Signer, strict=True) - expect(mock_decrypter, times=1).decrypt(ser=b'cipher qb64b', transferable=False).thenReturn(mock_signer) + expect(mock_decrypter, times=1).decrypt(cipher=mock_prx_cipher, transferable=False).thenReturn(mock_signer) # test from signify.core.keeping import RandyKeeper @@ -870,4 +872,4 @@ def test_base_keeper_sign_indexed_boom(indexed, indices, ondices, expected): mock_signer_one = mock(spec=Signer, strict=True) with pytest.raises(ValueError, match=expected): - BaseKeeper.__sign__(b'ser bytes', [mock_signer_one], indexed=indexed, indices=indices, ondices=ondices) \ No newline at end of file + BaseKeeper.__sign__(b'ser bytes', [mock_signer_one], indexed=indexed, indices=indices, ondices=ondices) diff --git a/tests/integration/test_provisioning_and_identifiers.py b/tests/integration/test_provisioning_and_identifiers.py index e5ec693..423dddb 100644 --- a/tests/integration/test_provisioning_and_identifiers.py +++ b/tests/integration/test_provisioning_and_identifiers.py @@ -5,6 +5,8 @@ from __future__ import annotations import pytest +from keri.app.keeping import Algos +from keri.core import coring, serdering from .constants import QVI_SCHEMA_SAID, TEST_WITNESS_AIDS from .helpers import ( additional_schema_oobis, @@ -100,6 +102,59 @@ def test_single_sig_identifier_lifecycle_smoke(client_factory): assert fetched["prefix"] == hab["prefix"] +def test_randy_identifier_lifecycle_smoke(client_factory): + """Prove Randy identifiers behave like the maintained SignifyTS lifecycle contract.""" + client = client_factory() + name = alias("randy") + + _, _, operation = client.identifiers().create(name, algo=Algos.randy, wits=[]) + result = wait_for_operation(client, operation) + icp = serdering.SerderKERI(sad=result["response"]) + + assert len(icp.verfers) == 1 + assert len(icp.ked["n"]) == 1 + assert icp.ked["kt"] == "1" + assert icp.ked["nt"] == "1" + + identifiers = client.identifiers().list() + aid = client.identifiers().get(name) + names = {entry["name"] for entry in identifiers["aids"]} + + assert name in names + assert aid["name"] == name + assert aid["prefix"] == icp.pre + assert aid["state"]["s"] == "0" + assert len(aid["randy"]["prxs"]) == 1 + assert len(aid["randy"]["nxts"]) == 1 + + _, _, interact_operation = client.identifiers().interact(name, data=[icp.pre]) + interact_result = wait_for_operation(client, interact_operation) + ixn = serdering.SerderKERI(sad=interact_result["response"]) + + assert ixn.ked["s"] == "1" + assert ixn.ked["a"] == [icp.pre] + + events = client.keyEvents().get(aid["prefix"]) + assert len(events) == 2 + + _, _, rotate_operation = client.identifiers().rotate(name) + rotate_result = wait_for_operation(client, rotate_operation) + rot = serdering.SerderKERI(sad=rotate_result["response"]) + + assert rot.ked["s"] == "2" + assert len(rot.verfers) == 1 + assert len(rot.ked["n"]) == 1 + assert rot.verfers[0].qb64 != icp.verfers[0].qb64 + assert rot.ked["n"][0] != icp.ked["n"][0] + assert coring.Diger(ser=rot.verfers[0].qb64b, code=coring.MtrDex.Blake3_256).qb64 == icp.ked["n"][0] + + rotated = client.identifiers().get(name) + assert rotated["state"]["s"] == "2" + + events = client.keyEvents().get(aid["prefix"]) + assert len(events) == 3 + + def test_identifier_rename_update_compatibility(client_factory): """Prove TS-style identifier rename works without dropping the Python wrappers.""" client = client_factory()