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
29 changes: 22 additions & 7 deletions src/signify/app/aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ def get(self, name):

def rename(self, name, newName):
"""Rename an identifier alias without changing its underlying AID."""
res = self.client.put(f"/identifiers/{name}", json={"name": newName})
return res.json()
return self.update(name, {"name": newName})

def create(self, name, transferable=True, isith="1", nsith="1", wits=None, toad="0", proxy=None, delpre=None,
dcode=MtrDex.Blake3_256, data=None, algo=Algos.salty, estOnly=False, DnD=False, **kwargs):
Expand Down Expand Up @@ -116,8 +115,20 @@ def create(self, name, transferable=True, isith="1", nsith="1", wits=None, toad=
res = self.client.post("/identifiers", json=body)
return serder, sigs, res.json()

def update(self, name, typ, **kwas):
"""Dispatch an identifier update to either ``interact`` or ``rotate``."""
def update(self, name, info=None, typ=None, **kwas):
"""Update identifier metadata or dispatch an interaction/rotation flow.

``update(name, {"name": "new-alias"})`` is the TS-compatible rename
path. The older dispatcher mode remains supported through either
``update(name, typ="interact", ...)`` or ``update(name, "interact", ...)``.
"""
if isinstance(info, dict) and typ is None:
res = self.client.put(f"/identifiers/{name}", json=info)
return res.json()

if typ is None:
typ = info

if typ == "interact":
return self.interact(name, **kwas)
elif typ == "rotate":
Expand All @@ -131,6 +142,12 @@ def delete(self, name):

def interact(self, name, data=None):
"""Create and submit a signed interaction event for an identifier."""
serder, sigs, body = self.createInteract(name, data=data)
res = self.client.post(f"/identifiers/{name}/events", json=body)
return serder, sigs, res.json()

def createInteract(self, name, data=None):
"""Create the local interaction event payload without submitting it."""
hab = self.get(name)
pre = hab["prefix"]

Expand All @@ -148,9 +165,7 @@ def interact(self, name, data=None):
ixn=serder.ked,
sigs=sigs)
body[keeper.algo] = keeper.params()

res = self.client.post(f"/identifiers/{name}/events", json=body)
return serder, sigs, res.json()
return serder, sigs, body

def rotate(self, name, *, transferable=True, nsith=None, toad=None, cuts=None, adds=None,
data=None, ncode=MtrDex.Ed25519_Seed, ncount=1, ncodes=None, states=None, rstates=None):
Expand Down
185 changes: 124 additions & 61 deletions src/signify/app/clienting.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
import sseclient
from keri import kering
from keri.core.coring import Tiers
from keri.end import ending
from keri.help import helping
from requests import HTTPError
from requests.auth import AuthBase
from requests.structures import CaseInsensitiveDict

from signify.core import keeping, authing, api
from signify.signifying import SignifyState
Expand All @@ -24,6 +26,8 @@
class SignifyClient:
"""Edge-signing client bound to one controller AID and delegated agent."""

ExternalRequestFields = ["@method", "@path", "Signify-Resource", "Signify-Timestamp"]

def __init__(self, passcode, url=None, boot_url=None, tier=Tiers.low, extern_modules=None):
"""
Create a new SignifyClient. Connects to the KERIA instance and delegates from the local
Expand Down Expand Up @@ -172,110 +176,164 @@ def states(self):

return state

def state(self):
"""Compatibility wrapper returning the current controller/agent state bundle."""
return self.states()

def _save_old_salt(self, salt):
"""Persist the previous controller salt during passcode rotation flows."""
caid = self.ctrl.pre
body = dict(salt=salt)
res = self.put(f"/salt/{caid}", json=body)
return res.status_code == 204

def saveOldPasscode(self, passcode):
"""Persist a prior controller passcode during passcode rotation."""
return self._save_old_salt(passcode)

def _delete_old_salt(self):
"""Delete the previously persisted controller salt after rotation."""
caid = self.ctrl.pre
res = self.delete(f"/salt/{caid}")
return res.status_code == 204

def get(self, path, params=None, headers=None, body=None):
"""Issue an authenticated ``GET`` request relative to the client base URL."""
url = urljoin(self.base, path)
def deletePasscode(self):
"""Delete any previously persisted controller passcode backup."""
return self._delete_old_salt()

def _request_kwargs(self, *, params=None, headers=None, json=None):
"""Build ``requests`` kwargs for an authenticated relative request."""
kwargs = dict()
if params is not None:
kwargs["params"] = params

if headers is not None:
kwargs["headers"] = headers

if body is not None:
kwargs["json"] = body
if json is not None:
kwargs["json"] = json

return kwargs

def _request(self, method, path, *, params=None, headers=None, json=None):
"""Issue an authenticated HTTP request relative to the client base URL."""
url = urljoin(self.base, path)
kwargs = self._request_kwargs(params=params, headers=headers, json=json)

requester = getattr(self.session, method.lower(), None)
if requester is None:
res = self.session.request(method=method, url=url, **kwargs)
else:
res = requester(url, **kwargs)

res = self.session.get(url, **kwargs)
if not res.ok:
self.raiseForStatus(res)

return res

def stream(self, path, params=None, headers=None, body=None):
"""Open a server-sent-event stream against an authenticated endpoint."""
url = urljoin(self.base, path)
@staticmethod
def _signature_path(url):
"""Return the path component used when signing an external request URL."""
path = urlsplit(url).path
return path if path else "/"

kwargs = dict()
if params is not None:
kwargs["params"] = params
@staticmethod
def _signature_headers_for_signer(headers, method, path, signer):
"""Attach Signify signature headers using one explicit signer."""
signed_headers = CaseInsensitiveDict(headers)
header, qsig = ending.siginput(
"signify",
method,
path,
signed_headers,
fields=SignifyClient.ExternalRequestFields,
signers=[signer],
alg="ed25519",
keyid=signer.verfer.qb64,
)
for key, val in header.items():
signed_headers[key] = val

signage = ending.Signage(
markers=dict(signify=qsig),
indexed=False,
signer=None,
ordinal=None,
digest=None,
kind=None,
)
for key, val in ending.signature([signage]).items():
signed_headers[key] = val

if headers is not None:
kwargs["headers"] = headers
return signed_headers

if body is not None:
kwargs["json"] = body
def get(self, path, params=None, headers=None, body=None):
"""Issue an authenticated ``GET`` request relative to the client base URL."""
return self._request("GET", path, params=params, headers=headers, json=body)

def fetch(self, path, method, data, headers=None):
"""Compatibility wrapper for a unified signed request entrypoint."""
method = method.upper()
payload = None if method == "GET" else data
return self._request(method, path, headers=headers, json=payload)

def createSignedRequest(self, name, url, req=None):
"""Build a ``PreparedRequest`` signed by the named managed identifier."""
if self.manager is None:
raise kering.ConfigurationError("client must be connected before signing external requests")

req = {} if req is None else dict(req)
method = req.get("method", "GET").upper()
headers = CaseInsensitiveDict(req.get("headers") or {})

hab = self.identifiers().get(name)
keeper = self.manager.get(aid=hab)
signer = keeper.signers()[0]

headers["Signify-Resource"] = hab["prefix"]
headers["Signify-Timestamp"] = helping.nowIso8601()
headers = self._signature_headers_for_signer(
headers=headers,
method=method,
path=self._signature_path(url),
signer=signer,
)

body = req.get("data", req.get("body"))
json_body = req.get("json")
if body is not None and json_body is not None:
raise ValueError("req cannot contain both 'body'/'data' and 'json'")

request = requests.Request(
method=method,
url=url,
headers=dict(headers),
params=req.get("params"),
data=body,
json=json_body,
)
return request.prepare()

def stream(self, path, params=None, headers=None, body=None):
"""Open a server-sent-event stream against an authenticated endpoint."""
url = urljoin(self.base, path)
kwargs = self._request_kwargs(params=params, headers=headers, json=body)

client = sseclient.SSEClient(url, session=self.session, **kwargs)
for event in client:
yield event

def delete(self, path, params=None, headers=None, body=None):
"""Issue an authenticated ``DELETE`` request relative to the client base URL."""
url = urljoin(self.base, path)

kwargs = dict()
if params is not None:
kwargs["params"] = params

if headers is not None:
kwargs["headers"] = headers

if body is not None:
kwargs["json"] = body

res = self.session.delete(url, **kwargs)
if not res.ok:
self.raiseForStatus(res)

return res
return self._request("DELETE", path, params=params, headers=headers, json=body)

def post(self, path, json, params=None, headers=None):
"""Issue an authenticated ``POST`` request relative to the client base URL."""
url = urljoin(self.base, path)

kwargs = dict(json=json)
if params is not None:
kwargs["params"] = params

if headers is not None:
kwargs["headers"] = headers

res = self.session.post(url, **kwargs)
if not res.ok:
self.raiseForStatus(res)

return res
return self._request("POST", path, params=params, headers=headers, json=json)

def put(self, path, json, params=None, headers=None):
"""Issue an authenticated ``PUT`` request relative to the client base URL."""
url = urljoin(self.base, path)

kwargs = dict(json=json)
if params is not None:
kwargs["params"] = params

if headers is not None:
kwargs["headers"] = headers

res = self.session.put(url, **kwargs)
if not res.ok:
self.raiseForStatus(res)

return res
return self._request("PUT", path, params=params, headers=headers, json=json)

def identifiers(self):
"""Return the identifier lifecycle resource wrapper."""
Expand Down Expand Up @@ -342,6 +400,11 @@ def schemas(self):
from signify.app.schemas import Schemas
return Schemas(client=self)

def config(self):
"""Return the agent-configuration read resource wrapper."""
from signify.app.coring import Config
return Config(client=self)

def exchanges(self):
"""Return the exchange transport resource wrapper."""
from signify.app.exchanging import Exchanges
Expand Down
13 changes: 13 additions & 0 deletions src/signify/app/coring.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,16 @@ def get(self, pre):
"""Fetch KERI events for one AID prefix."""
res = self.client.get(f"/events?pre={pre}")
return res.json()


class Config:
"""Resource wrapper for reading agent configuration exposed by KERIA."""

def __init__(self, client: SignifyClient):
"""Create an agent-configuration resource bound to one Signify client."""
self.client = client

def get(self):
"""Fetch the public agent configuration subset exposed by KERIA."""
res = self.client.get("/config")
return res.json()
10 changes: 9 additions & 1 deletion src/signify/app/grouping.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ def get_request(self, said):
res = self.client.get(f"/multisig/request/{said}")
return res.json()

def getRequest(self, said):
"""Compatibility alias for :meth:`get_request`."""
return self.get_request(said)

def send_request(self, name, exn, sigs, atc):
""" Send multisig exn peer-to-peer message to other members of the multisig group

Expand All @@ -48,12 +52,16 @@ def send_request(self, name, exn, sigs, atc):
body = dict(
exn=exn,
sigs=sigs,
atc=atc.decode("utf-8")
atc=atc.decode("utf-8") if isinstance(atc, bytes) else atc
)

res = self.client.post(f"/identifiers/{name}/multisig/request", json=body)
return res.json()

def sendRequest(self, name, exn, sigs, atc):
"""Compatibility alias for :meth:`send_request`."""
return self.send_request(name, exn, sigs, atc)

def join(self, name, rot, sigs, gid, smids, rmids):
"""Submit a multisig join approval using a received proposal event.

Expand Down
Loading
Loading