diff --git a/src/keria/app/aiding.py b/src/keria/app/aiding.py index 5eaede13..dab21c14 100644 --- a/src/keria/app/aiding.py +++ b/src/keria/app/aiding.py @@ -388,6 +388,19 @@ def on_put(self, req, rep, caid): cipher = core.Cipher(qb64=prx) agent.mgr.rb.nxts.put(keys=digers[idx].qb64b, val=cipher) + elif "extern_type" in val: + # extern AIDs keep minimal metadata in KERIA DB (parity with salty/randy). + # This enables alias-based lookup/rotation flows to treat extern AIDs as managed identifiers. + if (ep := agent.mgr.rb.eprms.get(pre)) is None: + raise ValueError(f"Attempt to update extern for nonexistent pre={pre}.") + + ep.extern_type = val.get("extern_type", ep.extern_type) + if "pidx" in val: + ep.pidx = val["pidx"] + + if not agent.mgr.rb.eprms.pin(pre, val=ep): + raise ValueError(f"Unable to update extern prms for pre={pre}.") + agent.mgr.delete_sxlt() rep.status = falcon.HTTP_204 @@ -1148,6 +1161,18 @@ def rotate(agent, name, body): return op + elif Algos.extern in body: + hab.rotate(serder=serder, sigers=sigers) + extern = body[Algos.extern] + keeper = agent.mgr.get(Algos.extern) + + try: + keeper.rotate(pre=serder.pre, **extern) + except ValueError as e: + agent.hby.deleteHab(name=name) + raise falcon.HTTPInternalServerError(description=f"{e.args[0]}") + + if hab.kever.delpre: agent.anchors.append(dict(alias=name, pre=hab.pre, sn=serder.sn)) op = agent.monitor.submit( diff --git a/src/keria/core/keeping.py b/src/keria/core/keeping.py index a99097ec..94cfeea5 100644 --- a/src/keria/core/keeping.py +++ b/src/keria/core/keeping.py @@ -42,7 +42,17 @@ class SaltyPrm: def __iter__(self): return iter(asdict(self)) +@dataclass() +class ExternPrm: + """ + Extern prefix's parameters for referencing external key management + """ + pidx: int = 0 + extern_type: str = "" + def __iter__(self): + return iter(asdict(self)) + class RemoteKeeper(dbing.LMDBer): """ RemoteKeeper stores data for Salty or Randy Encrypted edge key generation. @@ -95,6 +105,7 @@ def __init__(self, headDirPath=None, perm=None, reopen=False, **kwa): self.nxts = None self.prxs = None self.gbls = None + self.eprms = None if perm is None: perm = self.Perm # defaults to restricted permissions for non temp @@ -136,6 +147,11 @@ def reopen(self, **kwa): subkey="pubs.", schema=PubSet, ) # public key set at pre.ridx + self.eprms = koming.Komer( + db=self, + subkey="eprms.", + schema=ExternPrm, + ) # New Extern Parameter return self.opened @@ -444,5 +460,36 @@ class ExternKeeper: def __init__(self, rb: RemoteKeeper): self.rb = rb - def incept(self, **kwargs): - pass + def incept(self, pre, pidx=0, extern_type="", **kwargs): + # Ignore unused kwargs + pp = Prefix(pidx=pidx, algo=Algos.extern) + if not self.rb.pres.put(pre, val=pp): + raise ValueError("Already incepted pre={}.".format(pre)) + + ep = ExternPrm(pidx=pidx, extern_type=extern_type) + if not self.rb.eprms.put(pre, val=ep): + raise ValueError("Already incepted prm for pre={}.".format(pre)) + + def rotate(self, pre, pidx=None, extern_type=None, **kwargs): + if (pp := self.rb.pres.get(pre)) is None or pp.algo != Algos.extern: + raise ValueError("Attempt to rotate nonexistent or invalid pre={}.".format(pre)) + + if (ep := self.rb.eprms.get(pre)) is None: + ep = ExternPrm() + + if pidx is not None: + ep.pidx = pidx + if extern_type is not None: + ep.extern_type = extern_type + + if not self.rb.eprms.pin(pre, val=ep): + raise ValueError("Unable to rotate extern prms for pre={}.".format(pre)) + + def params(self, pre): + if (pp := self.rb.pres.get(pre)) is None or pp.algo != Algos.extern: + raise ValueError("Attempt to load nonexistent or invalid pre={}.".format(pre)) + # Default extern params + if (ep := self.rb.eprms.get(pre)) is None: + return dict(extern=dict(extern_type="", pidx=pp.pidx)) + + return dict(extern=asdict(ep)) \ No newline at end of file diff --git a/tests/app/test_aiding.py b/tests/app/test_aiding.py index c8299057..0600aa0e 100644 --- a/tests/app/test_aiding.py +++ b/tests/app/test_aiding.py @@ -1197,32 +1197,71 @@ def test_identifier_collection_end(helpers): "description": "unknown delegator EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHUNKNx", } - # Test extern keys for HSM integration, only initial tests, work still needed - with helpers.openKeria() as (agency, agent, app, client): - end = aiding.IdentifierCollectionEnd() - resend = aiding.IdentifierResourceEnd() - app.add_route("/identifiers", end) - app.add_route("/identifiers/{name}", resend) - - client = testing.TestClient(app) - - # Test with randy - serder, signers = helpers.inceptExtern(count=1) - sigers = [signer.sign(ser=serder.raw, index=0).qb64 for signer in signers] - - body = { - "name": "randy1", - "icp": serder.ked, - "sigs": sigers, - "extern": { - "stem": "test-fake-stem", - "transferable": True, - }, - } - res = client.simulate_post(path="/identifiers", body=json.dumps(body)) - assert res.status_code == 202 - + # Test extern keys for HSM integration, complete implementation test + with helpers.openKeria() as (agency, agent, app, client): + end = aiding.IdentifierCollectionEnd() + resend = aiding.IdentifierResourceEnd() + app.add_route("/identifiers", end) + app.add_route("/identifiers/{name}", resend) + app.add_route("/identifiers/{name}/events", resend) + + client = testing.TestClient(app) + + salt = b"0123456789abcdef" + + # Test Inception + serder, signers = helpers.incept(salt, "signify:aid", pidx=0) + sigers = [signer.sign(ser=serder.raw, index=0).qb64 for signer in signers] + + body = { + "name": "extern1", + "icp": serder.ked, + "sigs": sigers, + "extern": { + "extern_type": "aws_kms", + "pidx": 0 + }, + } + res = client.simulate_post(path="/identifiers", body=json.dumps(body)) + assert res.status_code == 202 + + # Verify params + res = client.simulate_get(path="/identifiers") + assert res.status_code == 200 + + res = client.simulate_get(path="/identifiers/extern1") + assert res.status_code == 200 + aid_info = res.json + assert aid_info["prefix"] == serder.pre + assert aid_info[Algos.extern]["extern_type"] == "aws_kms" + assert aid_info[Algos.extern]["pidx"] == 0 + + # Test rotation + bodyrot = helpers.createRotate( + aid_info, salt, signers, pidx=0, ridx=1, kidx=1, wits=[], toad=0 + ) + + # Remove temporary salty params and replace with extern params + if "salty" in bodyrot: + del bodyrot["salty"] + + bodyrot["extern"] = { + "extern_type": "ledger", + "pidx": 1 + } + res = client.simulate_post( + path="/identifiers/extern1/events", body=json.dumps(bodyrot) + ) + assert res.status_code == 200 + + # Verify updated params after rotation + res = client.simulate_get(path="/identifiers/extern1") + assert res.status_code == 200 + aid_info = res.json + assert aid_info[Algos.extern]["extern_type"] == "ledger" + assert aid_info[Algos.extern]["pidx"] == 1 + def test_challenge_ends(helpers): with helpers.openKeria() as (agency, agent, app, client): end = aiding.IdentifierCollectionEnd()