Skip to content
Closed
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
8 changes: 8 additions & 0 deletions src/keria/app/aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from marshmallow_dataclass import class_schema

from ..core import longrunning, httping
from .multisig import replay_multisig_embeds
from ..utils.openapi import namedtupleToEnum, dataclassFromFielddom
from keri.core.serdering import Protocols, Vrsn_1_0, Vrsn_2_0, SerderKERI

Expand Down Expand Up @@ -1659,6 +1660,13 @@ def on_post(req, rep, name, aid=None, role=None):
coring.Saider(qb64=hab.kever.serder.said),
rsigers,
)
replay_multisig_embeds(
agent,
hab,
route="/multisig/rpy",
embeds=dict(rpy=rserder.ked),
labels=("rpy",),
)
try:
agent.hby.rvy.processReply(rserder, tsgs=[tsg])
except kering.UnverifiedReplyError:
Expand Down
44 changes: 41 additions & 3 deletions src/keria/app/credentialing.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from ..utils.openapi import dataclassFromFielddom
from keri.core.serdering import Protocols, Vrsn_1_0, Vrsn_2_0, SerderKERI
from ..core import httping, longrunning
from .multisig import replay_multisig_embeds
from marshmallow import fields, Schema as MarshmallowSchema
from typing import List, Dict, Any, Optional, Tuple, Literal, Union
from .aiding import (
Expand Down Expand Up @@ -393,6 +394,17 @@ def on_post(self, req, rep, name):

registry = agent.rgy.makeSignifyRegistry(name=rname, prefix=hab.pre, regser=vcp)

# The `/multisig/vcp` EXN uses the embed label `anc` for the anchoring
# KEL event. In this endpoint that concrete event is the submitted `ixn`.
protocol_embeds = dict(vcp=vcp.ked, anc=ixn.ked)
replay_multisig_embeds(
agent,
hab,
route="/multisig/vcp",
embeds=protocol_embeds,
labels=("anc",),
)

if hab.kever.estOnly:
op = self.identifierResource.rotate(agent, name, body)
else:
Expand Down Expand Up @@ -951,6 +963,14 @@ def on_post(self, req, rep, name):
description=f"issue against invalid registry SAID {regk}"
)

replay_multisig_embeds(
agent,
hab,
route="/multisig/iss",
embeds=dict(acdc=creder.sad, iss=iserder.ked, anc=anc.ked),
labels=("anc", "acdc"),
)

if hab.kever.estOnly:
op = self.identifierResource.rotate(agent, name, body)
else:
Expand Down Expand Up @@ -1245,15 +1265,26 @@ def on_delete(self, req, rep, name, said):
description=f"credential for said {said} not found."
)

if hab.kever.estOnly:
anc = serdering.SerderKERI(sad=httping.getRequiredParam(body, "rot"))
else:
anc = serdering.SerderKERI(sad=httping.getRequiredParam(body, "ixn"))

replay_multisig_embeds(
agent,
hab,
route="/multisig/rev",
embeds=dict(rev=rserder.ked, anc=anc.ked),
labels=("anc",),
)

if hab.kever.estOnly:
op = self.identifierResource.rotate(agent, name, body)
anc = httping.getRequiredParam(body, "rot")
else:
op = self.identifierResource.interact(agent, name, body)
anc = httping.getRequiredParam(body, "ixn")

try:
agent.registrar.revoke(regk, rserder, anc)
agent.registrar.revoke(regk, rserder, anc.ked)
except Exception:
raise falcon.HTTPBadRequest(description="invalid revocation event.")

Expand Down Expand Up @@ -1392,6 +1423,10 @@ def incept(self, hab, registry, prefixer=None, seqner=None, saider=None):
)

else:
# assume SignifyGroupHab - multisig incept, so start the counselor to wait on multisig incept completion
self.counselor.start(
ghab=hab, prefixer=prefixer, seqner=seqner, saider=saider
)
logger.info(
"[%s | %s]: Waiting for TEL registry vcp event mulisig anchoring event",
hab.name,
Expand Down Expand Up @@ -1443,6 +1478,9 @@ def issue(self, regk, iserder, anc):
seqner = coring.Seqner(sn=sn)
saider = coring.Saider(qb64=said)

self.counselor.start(
prefixer=prefixer, seqner=seqner, saider=saider, ghab=hab
)
logger.info(
"[%s | %s]: Waiting for TEL iss event mulisig anchoring event %s",
hab.name,
Expand Down
60 changes: 60 additions & 0 deletions src/keria/app/multisig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# -*- encoding: utf-8 -*-
"""
KERIA
keria.app.multisig module

Shared helpers for multisig approval flows.
"""

from keri.app.habbing import SignifyGroupHab
from keri.core import coring


def replay_multisig_embeds(agent, hab, *, route, embeds, labels):
"""Replay stored non-local multisig embedded events before local approval.

Signify follower approval can happen after the matching multisig EXN was
already received and stored. In that local-after-remote ordering, KLI
explicitly replays the remote participant's signed embedded event streams
before it parses the follower's approval of the same proposal.

This helper restores that ordering for KERIA approval endpoints. It derives
the embedded-section SAID from the local approval payload, loads matching
non-local multisig EXNs from the mux, and replays the selected embedded
labels plus their pathed attachments through the parser.

The ``labels`` argument uses EXN embed-label names, not concrete KEL/TEL
event-type names. For example, the multisig protocol uses the embed label
``anc`` for the anchoring KEL event even when the concrete event is an
interaction event (``ixn``).
"""
if not isinstance(hab, SignifyGroupHab):
return 0

embed_ked = dict(embeds)
embed_ked["d"] = ""
_, embed_ked = coring.Saider.saidify(sad=embed_ked, label=coring.Saids.d)

replays = 0
for msg in agent.mux.get(esaid=embed_ked["d"]):
exn = msg["exn"]
if exn["r"] != route or exn["i"] == hab.mhab.pre:
continue

paths = msg["paths"]
for label in labels:
if label not in paths:
continue

sadder = coring.Sadder(ked=embeds[label])
attachment = paths[label]
if isinstance(attachment, str):
attachment = attachment.encode("utf-8")

ims = bytearray(sadder.raw)
ims.extend(attachment)
agent.hby.psr.parseOne(ims=ims)

replays += 1

return replays
55 changes: 55 additions & 0 deletions tests/app/test_aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,61 @@ def test_endrole_ends(helpers):
}


def test_endrole_replays_multisig_embeds_before_local_reply_processing(
helpers, monkeypatch
):
"""Follower B must replay A's stored `/multisig/rpy` before endorsing.

This is an endpoint-order test, not a full multisig integration test.

Identity mapping:

- participant A is represented by the mocked `replay_multisig_embeds(...)`
call, which stands in for the already-stored remote `/multisig/rpy`.
- participant B is the local `user1` habitat handled by this KERIA agent.
- `rpy` is B's local approval payload for the same end-role authorization.

The important assertion is ordering: KERIA must replay A's stored reply
stream before B processes B's own local reply.
"""
with helpers.openKeria() as (agency, agent, app, client):
end = aiding.IdentifierCollectionEnd()
app.add_route("/identifiers", end)
idResEnd = aiding.IdentifierResourceEnd()
app.add_route("/identifiers/{name}", idResEnd)
endRolesEnd = aiding.EndRoleCollectionEnd()
app.add_route("/identifiers/{name}/endroles", endRolesEnd)

# Participant B's local AID and the reply payload B later submits to
# KERIA when B approves the multisig end-role proposal.
salt = b"0123456789abcdef"
op = helpers.createAid(client, "user1", salt)
aid = op["response"]
rpy = helpers.endrole(aid["i"], agent.agentHab.pre)
sigs = helpers.sign(salt, 0, 0, rpy.raw)

calls = []

# This stands in for participant A's earlier `/multisig/rpy` arrival.
# We only care that the follower approval path invokes replay first.
def fake_replay(*args, **kwargs):
calls.append(("replay", kwargs["route"], kwargs["labels"]))

def fake_process_reply(*args, **kwargs):
calls.append("processReply")

monkeypatch.setattr(aiding, "replay_multisig_embeds", fake_replay)
monkeypatch.setattr(agent.hby.rvy, "processReply", fake_process_reply)

res = client.simulate_post(
path="/identifiers/user1/endroles",
body=json.dumps(dict(rpy=rpy.ked, sigs=sigs)).encode("utf-8"),
)

assert res.status_code == 202
assert calls[:2] == [("replay", "/multisig/rpy", ("rpy",)), "processReply"]


def test_locscheme_ends(helpers, mockHelpingNowUTC):
with helpers.openKeria() as (agency, agent, app, client):
locSchemesEnd = aiding.LocSchemeCollectionEnd()
Expand Down
Loading
Loading