Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/signify/app/aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,10 @@ def rotate(self, name, *, transferable=True, nsith=None, toad=None, cuts=None, a
cuts=cuts,
adds=adds,
data=data)
sigs = keeper.sign(ser=serder.raw)
sign_kwargs = dict(ser=serder.raw)
if keeper.algo == Algos.group:
sign_kwargs["rotated"] = True
sigs = keeper.sign(**sign_kwargs)

body = dict(
rot=serder.ked,
Expand Down
54 changes: 50 additions & 4 deletions src/signify/core/keeping.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ def rotate(self, ncodes, transferable, **_):

return verfers, digers

def sign(self, ser, indexed=True, indices=None, ondices=None):
def sign(self, ser, indexed=True, indices=None, ondices=None, **_):
""" Sign provided data using the current signing keys for AID

Args:
Expand Down Expand Up @@ -362,6 +362,21 @@ def signers(self):


class GroupKeeper(BaseKeeper):
"""Map group signing indexes onto the local member AID's keeper.

A group keeper does not own group private keys. It receives group event key
lists from multisig ``states``/``rstates`` and delegates actual signing to
the local member habitat. ``rstates`` supply proposed next digests for the
event being built; they are not the signer authorization set.

For rotations, KERI dual-index signatures expose two positions: ``index``
in the event's current key list, and ``ondex`` in the prior establishment
event's next digest list. For inception, interaction, and other
non-rotation payloads, signatures satisfy only the current signing
threshold and do not expose ``ondex``. Direct low-level callers signing a
group rotation must pass ``rotated=True``; normal API callers get that from
``Identifiers.rotate``.
"""

def __init__(self, mgr: Manager, mhab=None, states=None, rstates=None,
keys=None, ndigs=None):
Expand All @@ -375,6 +390,11 @@ def __init__(self, mgr: Manager, mhab=None, states=None, rstates=None,

self.gkeys = keys
self.gdigs = ndigs
# Group prior next digests authorize the next rotation. On load,
# persisted group ndigs are expected to be the current establishment
# event's next digest list. On inception there is no separate prior
# event, so gpndigs starts as the same list as gdigs.
self.gpndigs = self.gdigs
self.mhab = mhab

def incept(self, **_):
Expand All @@ -386,16 +406,42 @@ def rotate(self, states, rstates, **_):

return self.gkeys, self.gdigs

def sign(self, ser, indexed=True, **_):
def sign(self, ser, indexed=True, rotated=False, **_):
key = self.mhab['state']['k'][0]
ndig = self.mhab['state']['n'][0]

csi = self.gkeys.index(key)
pni = self.gdigs.index(ndig)
if rotated:
# Rotation signatures must expose the signer's position in the
# prior establishment event's precommitted next digest list.
pni = self._priorNextIndexForKey(key)
else:
# Non-rotation signatures are current-only. The event's `n` field
# is a precommitment for a future rotation, not an authorization
# set for deriving `ondex`.
pni = None
mkeeper = self.mgr.get(self.mhab)

return mkeeper.sign(ser, indexed=indexed, indices=[csi], ondices=[pni])

def _priorNextIndexForKey(self, key):
"""Return the prior-next index for the supplied current signing key.

During rotation, KERI validators compare the signer key against the
prior establishment event's precommitted next digests. The digest code
comes from each prior next digest, so mirror that lookup locally before
producing the dual-index signature.
"""
for idx, pdig in enumerate(self.gpndigs or []):
prior = coring.Diger(qb64=pdig)
verfer = coring.Verfer(qb64=key)
exposed = coring.Diger(ser=verfer.qb64b, code=prior.code).qb64
if exposed == pdig:
return idx

raise ValueError(
"current signing key is not committed in the group prior next digests"
)

def signers(self):
"""Return the current signers for the member habitat backing this group."""
return self.mgr.get(self.mhab).signers()
Expand Down
59 changes: 59 additions & 0 deletions tests/app/test_aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,65 @@ def test_aiding_rotate():
unstub()


def test_aiding_rotate_group_passes_rotated_to_keeper():
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': 'group1',
'state': {'s': '0', 'd': 'hab digest', 'b': [], 'k': ['key1'], 'kt': '1'},
'group': {'mhab': {'name': 'member1'}, 'keys': ['key1'], 'ndigs': ['ndig1']}}
expect(ids, times=1).get('group1').thenReturn(mock_hab)

mock_keeper = mock(
{'algo': 'group', 'params': lambda: {'mhab': {'name': 'member1'}, 'keys': ['key1'], 'ndigs': ['ndig2']}},
spec=keeping.GroupKeeper,
strict=True,
)
expect(mock_manager, times=1).get(mock_hab).thenReturn(mock_keeper)

keys = ['key1']
ndigs = ['ndig2']
states = [{'i': 'member1', 'k': ['key1'], 'n': ['ndig1']}]
rstates = [{'i': 'member1', 'k': ['key1'], 'n': ['ndig2']}]
expect(mock_keeper, times=1).rotate(ncodes=['A'], transferable=True, states=states, rstates=rstates).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=['ndig2'], toad=None, wits=[],
cuts=[], adds=[], data=[]).thenReturn(mock_serder)

expect(mock_keeper, times=1).sign(ser=mock_serder.raw, rotated=True).thenReturn(['a signature'])

from requests import Response
mock_response = mock(spec=Response, strict=True)
expected_data = {'rot': {'a': 'key event dictionary'}, 'sigs': ['a signature'],
'group': {'mhab': {'name': 'member1'}, 'keys': ['key1'], 'ndigs': ['ndig2']},
'smids': ['member1'], 'rmids': ['member1']}
expect(mock_client, times=1).post('/identifiers/group1/events', json=expected_data).thenReturn(mock_response)
expect(mock_response, times=1).json().thenReturn({'success': 'yay'})

# Group rotation is the one app path that must ask the keeper to expose
# prior-next ondex; single-sig and external keepers remain untouched.
_, _, out = ids.rotate(name='group1', states=states, rstates=rstates)
assert out['success'] == 'yay'

verifyNoUnwantedInteractions()
unstub()


def test_aiding_rotate_randy():
from signify.app.clienting import SignifyClient
mock_client = mock(spec=SignifyClient, strict=True)
Expand Down
Loading
Loading