Skip to content
Open
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
25 changes: 25 additions & 0 deletions src/keria/app/aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me wonder if there's actually value in storing any metadata at all with eprms, or if it's enough to just populate pres and keep it more lightweight.


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
Expand Down Expand Up @@ -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(
Expand Down
51 changes: 49 additions & 2 deletions src/keria/core/keeping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, shouldn't we also raise ValueError here as eprms should be set in incept?


return dict(extern=asdict(ep))
89 changes: 64 additions & 25 deletions tests/app/test_aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down