Source code for keri.app.habbing

# -*- encoding: utf-8 -*-
"""
KERI
keri.app.habbing module

"""
import json
from contextlib import contextmanager
from math import ceil
from urllib.parse import urlsplit

from hio.base import doing
from hio.help import hicting

from keri.peer import exchanging
from . import keeping, configing
from .. import help
from .. import kering
from ..core import coring, eventing, parsing, routing, serdering
from ..db import dbing, basing
from ..kering import MissingSignatureError, Roles

logger = help.ogler.getLogger()

[docs] @contextmanager def openHby(*, name="test", base="", temp=True, salt=None, **kwa): """ Context manager wrapper for Habery instance. Context 'with' statements call .close on exit of 'with' block Parameters: name (str): name of habery and shared db and file path base (str): optional if "" path component of shared db and files. temp (bool): True means use temporary or limited resources testing. Store .ks, .db, and .cf in /tmp Use quick method to stretch salts for seeds such as bran salt to seed or key creation of Habs. Otherwise use more resources set by tier to stretch salt (str): qb64 salt for creating key pairs Parameters: Passed through via kwa ks (Keeper): keystore lmdb subclass instance db (Baser): database lmdb subclass instance cf (Configer): config file instance seed (str): qb64 private-signing key (seed) for the aeid from which the private decryption key may be derived. If aeid stored in database is not empty then seed may required to do any key management operations. The seed value is memory only and MUST NOT be persisted to the database for the manager with which it is used. It MUST only be loaded once when the process that runs the Manager is initialized. Its presence acts as an authentication, authorization, and decryption secret for the Manager and must be stored on another device from the device that runs the Manager. aeid (str): qb64 of non-transferable identifier prefix for authentication and encryption of secrets in keeper. If provided aeid (not None) and different from aeid stored in database then all secrets are re-encrypted using new aeid. In this case the provided prikey must not be empty. A change in aeid should require a second authentication mechanism besides the prikey. bran (str): Base64 22 char string that is used as base material for seed. bran allows alphanumeric passcodes generated by key managers like 1password to be key store for seed. pidx (int): Initial prefix index for vacuous ks algo (str): algorithm (randy or salty) for creating key pairs default is root algo which defaults to salty tier (str): security tier for generating keys from salt (Tierage) free (boo): free resources by closing on Doer exit if any """ habery = None salt = salt if not None else coring.Salter(raw=b'0123456789abcdef').qb64 try: habery = Habery(name=name, base=base, temp=temp, salt=salt, **kwa) yield habery finally: if habery: habery.close(clear=habery.temp)
[docs] @contextmanager def openHab(name="test", base="", salt=b'0123456789abcdef', temp=True, cf=None, **kwa): """ Context manager wrapper for Hab instance. Defaults to temporary resources Context 'with' statements call .close on exit of 'with' block Parameters: name(str): name of habitat to create base(str): the name used for shared resources i.e. Baser and Keeper The habitat specific config file will be in base/name salt(bytes): passed to habitat to use for inception raw salt not qb64 temp(bool): indicates if this uses temporary databases cf(Configer): optional configer for loading configuration data """ salt = coring.Salter(raw=salt).qb64 with openHby(name=name, base=base, salt=salt, temp=temp, cf=cf) as hby: if (hab := hby.habByName(name)) is None: hab = hby.makeHab(name=name, icount=1, isith='1', ncount=1, nsith='1', **kwa) yield hby, hab
[docs] class Habery: """Habery class provides shared database environments for all its Habitats Key controller and identifier controller shared configuration file, keystore and KEL databases. Attributes: name (str): name of associated databases base (str): optional directory path segment inserted before name that allows further hierarchical differentiation of databases. "" means optional. temp (bool): True for testing: temporary storage of databases and config file weak resources for stretch of salty key ks (keeping.Keeper): lmdb key store db (basing.Baser): lmdb data base for KEL etc cf (configing.Configer): config file instance mgr (keeping.Manager): creates and rotates keys in key store rtr (routing.Router): routes reply 'rpy' messages rvy (routing.Revery): factory that processes reply 'rpy' messages kvy (eventing.Kevery): factory for local processing of local event msgs psr (parsing.Parser): parses local messages for .kvy .rvy habs (dict): Hab instances keyed by prefix. To look up Hab by name get prefix from db.habs .prefix field using .habByName inited (bool): True means fully initialized wrt databases. False means not yet fully initialized Properties: kevers (dict): of eventing.Kever(s) keyed by qb64 prefix prefixes (OrderedSet): local prefixes for .db """
[docs] def __init__(self, *, name='test', base="", temp=False, ks=None, db=None, cf=None, clear=False, headDirPath=None, **kwa): """ Initialize instance. Parameters: name (str): alias name for shared environment config databases etc. base (str): optional directory path segment inserted before name that allows further differentiation with a hierarchy. "" means optional. temp (bool): True means use temporary or limited resources testing. Store .ks, .db, and .cf in /tmp Use quick method to stretch salts for seeds such as bran salt to seed or key creation of Habs. Otherwise use more resources set by tier to stretch ks (Keeper): keystore lmdb subclass instance db (Baser): database lmdb subclass instance cf (Configer): config file instance clear (bool): True means remove resource directory upon close when reopening False means do not remove directory upon close when reopening headDirPath (str): directory override Parameters: Passed through via kwa to setup for later init seed (str): qb64 private-signing key (seed) for the aeid from which the private decryption key may be derived. If aeid stored in database is not empty then seed may required to do any key management operations. The seed value is memory only and MUST NOT be persisted to the database for the manager with which it is used. It MUST only be loaded once when the process that runs the Manager is initialized. Its presence acts as an authentication, authorization, and decryption secret for the Manager and must be stored on another device from the device that runs the Manager. aeid (str): qb64 of non-transferable identifier prefix for authentication and encryption of secrets in keeper. If provided aeid (not None) and different from aeid stored in database then all secrets Haberyare re-encrypted using new aeid. In this case the provided prikey must not be empty. A change in aeid should require a second authentication mechanism besides the prikey. bran (str): Base64 22 char string that is used as base material for seed. bran allows alphanumeric passcodes generated by key managers like 1password to be key store for seed. pidx (int): Initial prefix index for vacuous ks algo (str): algorithm (randy or salty) for creating key pairs default is root algo which defaults to salty salt (str): qb64 salt for creating key pairs tier (str): security tier for generating keys from salt (Tierage) free (boo): free resources by closing on Doer exit if any temp (bool): See above """ self.name = name self.base = base self.temp = temp self.ks = ks if ks is not None else keeping.Keeper(name=self.name, base=self.base, temp=self.temp, reopen=True, clear=clear, headDirPath=headDirPath) self.db = db if db is not None else basing.Baser(name=self.name, base=self.base, temp=self.temp, reopen=True, clear=clear, headDirPath=headDirPath) self.cf = cf if cf is not None else configing.Configer(name=self.name, base=self.base, temp=self.temp, reopen=True, clear=clear) self.mgr = None # wait to setup until after ks is known to be opened self.rtr = routing.Router() self.rvy = routing.Revery(db=self.db, rtr=self.rtr) self.exc = exchanging.Exchanger(hby=self, handlers=[]) self.kvy = eventing.Kevery(db=self.db, lax=False, local=True, rvy=self.rvy) self.kvy.registerReplyRoutes(router=self.rtr) self.psr = parsing.Parser(framed=True, kvy=self.kvy, rvy=self.rvy, exc=self.exc) self.habs = {} # empty .habs self.namespaces = {} # empty .namespaces self._signator = None self.inited = False # save init kwy word arg parameters as ._inits in order to later finish # init setup elseqhere after databases are opened if not below self._inits = kwa self._inits['temp'] = temp # add temp for seed from bran tier override if self.db.opened and self.ks.opened: self.setup(**self._inits) # finish setup later
[docs] def setup(self, *, seed=None, aeid=None, bran=None, pidx=None, algo=None, salt=None, tier=None, free=False, temp=None, ): """ Setup Habery. Assumes that both .db and .ks have been opened. This allows dependency injection of .db and .ks into Habery instance prior to .db and .kx being opened to accomodate asynchronous process setup of these resources. Putting the .db and .ks associated initialization here enables asynchronous opening .db and .ks after Baser and Keeper instances are instantiated. First call to .setup will initialize databases (vacuous initialization). Parameters: seed (str): qb64 private-signing key (seed) for the aeid from which the private decryption key may be derived. If aeid stored in database is not empty then seed may required to do any key management operations. The seed value is memory only and MUST NOT be persisted to the database for the manager with which it is used. It MUST only be loaded once when the process that runs the Manager is initialized. Its presence acts as an authentication, authorization, and decryption secret for the Manager and must be stored on another device from the device that runs the Manager. aeid (str): qb64 of non-transferable identifier prefix for authentication and encryption of secrets in keeper. If provided aeid (not None) and different from aeid stored in database then all secrets are re-encrypted using new aeid. In this case the provided prikey must not be empty. A change in aeid should require a second authentication mechanism besides the prikey. bran (str): Base64 21 char string that is used as base material for seed. bran allows alphanumeric passcodes generated by key managers like 1password to be Okey store for seed. pidx (int): Initial prefix index for vacuous ks algo (str): algorithm (randy or salty) for creating key pairs default is root algo which defaults to salty salt (str): qb64 salt for creating key pairs tier (str): security tier for generating keys from salt (Tierage) free (boo): free resources by closing on Doer exit if any temp (bool): True means use shortcuts for testing. Use quick method to stretch salts for seeds such as bran salt to seed or key creation of Habs. Otherwise use more resources set by tier to stretch """ if not (self.ks.opened and self.db.opened): raise kering.ClosedError("Attempt to setup Habitat with closed " "database, .ks or .db.") self.free = True if free else False if bran and not seed: # create seed from stretch of bran as salt if len(bran) < 21: raise ValueError(f"Bran (passcode seed material) too short.") bran = coring.MtrDex.Salt_128 + 'A' + bran[:21] # qb64 salt for seed signer = coring.Salter(qb64=bran).signer(transferable=False, tier=tier, temp=temp) seed = signer.qb64 if not aeid: # aeid must not be empty event on initial creation aeid = signer.verfer.qb64 # lest it remove encryption if salt is None: # salt for signing keys not aeid seed salt = coring.Salter(raw=b'0123456789abcdef').qb64 else: salt = coring.Salter(qb64=salt).qb64 try: self.mgr = keeping.Manager(ks=self.ks, seed=seed, aeid=aeid, pidx=pidx, algo=algo, salt=salt, tier=tier) except kering.AuthError as ex: self.close() raise ex self._signator = Signator(db=self.db, mgr=self.mgr, temp=self.temp, ks=self.ks, cf=self.cf, rtr=self.rtr, kvy=self.kvy, psr=self.psr, rvy=self.rvy) self.loadHabs() self.inited = True
[docs] def loadHabs(self): """Load Habs instance from db .db.reopen calls .db.reload which loads .db.kevers from key state in .db.states and loads associated .db.prefixes. It also removes any bare .habs without key state Thus by now know that .habs are valid so can create Hab instances """ self.reconfigure() # pre hab load reconfiguration groups = [] for name, habord in self.db.habs.getItemIter(): name = ".".join(name) # detupleize the database key name pre = habord.hid # create Hab instance and inject dependencies if habord.mid and not habord.sid: hab = GroupHab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, pre=pre, temp=self.temp, smids=habord.smids) groups.append(habord) elif habord.sid and not habord.mid: hab = SignifyHab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, pre=habord.sid) elif habord.sid and habord.mid: hab = SignifyGroupHab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, pre=pre) groups.append(habord) else: hab = Hab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, pre=pre, temp=self.temp) # Rules for acceptance # if its delegated its accepted into its own local KEL even if the # delegator has not sealed it if not hab.accepted and not habord.mid: raise kering.ConfigurationError(f"Problem loading Hab pre=" f"{pre} name={name} from db.") # read in config file and process any oobis or endpoints for hab hab.inited = True self.habs[hab.pre] = hab for keys, habord in self.db.nmsp.getItemIter(): ns = keys[0] name = ".".join(keys[1:]) # detupleize the database key name pre = habord.hid # create Hab instance and inject dependencies if habord.mid and not habord.sid: hab = GroupHab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, ns=ns, pre=pre, temp=self.temp, smids=habord.smids) groups.append(habord) elif habord.sid and not habord.mid: hab = SignifyHab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, ns=ns, pre=habord.sid) elif habord.sid and habord.mid: hab = SignifyGroupHab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, pre=habord.sid) groups.append(habord) else: hab = Hab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, ns=ns, pre=pre, temp=self.temp) # Rules for acceptance # if its delegated its accepted into its own local KEL even if the # delegator has not sealed it if not hab.accepted and not habord.mid: raise kering.ConfigurationError(f"Problem loading Hab pre=" f"{pre} name={name} from db.") # read in config file and process any oobis or endpoints for hab hab.inited = True if ns not in self.namespaces: self.namespaces[ns] = dict() self.namespaces[ns][hab.pre] = hab # Populate the participant hab after loading all habs for habord in groups: self.habs[habord.hid].mhab = self.habs[habord.mid] self.reconfigure() # post hab load reconfiguration
[docs] def makeHab(self, name, ns=None, cf=None, **kwa): """Make new Hab with name, pre is generated from **kwa Parameters: (Passthrough to hab.make) secrecies (list): of list of secrets to preload key pairs if any iridx (int): initial rotation index after ingestion of secrecies code (str): prefix derivation code transferable (bool): True means pre is transferable (default) False means pre is nontransferable isith (Union[int, str, list]): incepting signing threshold as int, str hex, or list icount (int): incepting key count for number of keys nsith (Union[int, str, list]): next signing threshold as int, str hex or list ncount (int): next key count for number of next keys toad (Union[int,str]): int or str hex of witness threshold wits (list): of qb64 prefixes of witnesses delpre (str): qb64 of delegator identifier prefix estOnly (str): eventing.TraitCodex.EstOnly means only establishment events allowed in KEL for this Hab data (list | None): seal dicts """ if ns is not None and "." in ns: raise kering.ConfigurationError("Hab namespace names are not allowed to contain the '.' character") cf = cf if cf is not None else self.cf hab = Hab(ks=self.ks, db=self.db, cf=cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, ns=ns, temp=self.temp) hab.make(**kwa) if ns is None: self.habs[hab.pre] = hab else: if ns not in self.namespaces: self.namespaces[ns] = dict() self.namespaces[ns][hab.pre] = hab return hab
[docs] def makeGroupHab(self, group, mhab, smids, rmids=None, ns=None, **kwa): """Make new Group Hab using group has group hab name, with lhab as local participant. Parameters: (non-pass-through): group (str): human readable alias for group identifier mhab (Hab): group member (local) hab smids (list): group member signing ids (qb64) from which to extract inception event current signing keys rmids (list | None): group member rotation ids (qb64) from which to extract inception event next key digests if rmids is None then use assign smids to rmids if rmids is empty then no next key digests which means group identifier is no longer transferable. Parameters: (**kwa pass-through to hab.make) secrecies (list): of list of secrets to preload key pairs if any iridx (int): initial rotation index after ingestion of secrecies code (str): prefix derivation code transferable (bool): True means pre is transferable (default) False means pre is nontransferable isith (Union[int, str, list]): incepting signing threshold as int, str hex, or list icount (int): incepting key count for number of keys nsith (Union[int, str, list]): next signing threshold as int, str hex or list ncount (int): next key count for number of next keys toad (Union[int,str]): int or str hex of witness threshold wits (list): of qb64 prefixes of witnesses delpre (str): qb64 of delegator identifier prefix estOnly (str): eventing.TraitCodex.EstOnly means only establishment events allowed in KEL for this Hab DnD (bool): eventing.TraitCodex.DnD means do allow delegated identifiers from this identifier ToDo: NRR add midxs tuples for each group member or all in group multisig. """ if mhab.pre not in smids and mhab.pre not in rmids: raise kering.ConfigurationError(f"Local member identifier " f"{mhab.pre} must be member of " f"smids ={smids} and/or " f"rmids={rmids}.") for mid in smids: if mid not in self.kevers: raise kering.ConfigurationError(f"KEL missing for signing member " f"identifier {mid} from group's " f"current members ={smids}") if rmids is not None: for rmid in rmids: if rmid not in self.kevers: raise kering.ConfigurationError(f"KEL missing for next member " f"identifier {rmid} in group's" f" next members ={rmids}") # multisig group verfers of current signing keys and digers of next key digests merfers, migers = self.extractMerfersMigers(smids, rmids) # group verfers and digers kwa["merfers"] = merfers kwa["migers"] = migers # create group Hab in this Habery hab = GroupHab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=group, ns=ns, mhab=mhab, smids=smids, rmids=rmids, temp=self.temp) hab.make(**kwa) # finish making group hab with injected pass throughs if ns is None: self.habs[hab.pre] = hab else: if ns not in self.namespaces: self.namespaces[ns] = dict() self.namespaces[ns][hab.pre] = hab return hab
[docs] def joinGroupHab(self, pre, group, mhab, smids, rmids=None, ns=None): """Make new Group Hab using group has group hab name, with lhab as local participant. Parameters: (non-pass-through): pre (str): qb64 identifier prefix of group group (str): human readable alias for group identifier mhab (Hab): group member (local) hab smids (list): group member signing ids (qb64) from which to extract inception event current signing keys rmids (list | None): group member rotation ids (qb64) from which to extract inception event next key digests if rmids is None then use assign smids to rmids if rmids is empty then no next key digests which means group identifier is no longer transferable. """ if mhab.pre not in smids and mhab.pre not in rmids: raise kering.ConfigurationError(f"Local member identifier " f"{mhab.pre} must be member of " f"smids ={smids} and/or " f"rmids={rmids}.") for mid in smids: if mid not in self.kevers: raise kering.ConfigurationError(f"KEL missing for signing member " f"identifier {mid} from group's " f"current members ={smids}") if rmids is not None: for rmid in rmids: if rmid not in self.kevers: raise kering.ConfigurationError(f"KEL missing for next member " f"identifier {rmid} in group's" f" next members ={rmids}") # create group Hab in this Habery hab = GroupHab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=group, ns=ns, mhab=mhab, smids=smids, rmids=rmids, temp=self.temp) hab.pre = pre habord = basing.HabitatRecord(hid=hab.pre, mid=mhab.pre, smids=smids, rmids=rmids) hab.save(habord) hab.prefixes.add(pre) hab.inited = True if ns is None: self.habs[hab.pre] = hab else: if ns not in self.namespaces: self.namespaces[ns] = dict() self.namespaces[ns][hab.pre] = hab return hab
def makeSignifyHab(self, name, ns=None, **kwa): # create group Hab in this Habery hab = SignifyHab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, ns=ns, temp=self.temp) hab.make(**kwa) # finish making group hab with injected pass throughs if ns is None: self.habs[hab.pre] = hab else: if ns not in self.namespaces: self.namespaces[ns] = dict() self.namespaces[ns][hab.pre] = hab return hab def makeSignifyGroupHab(self, name, mhab, ns=None, **kwa): # create group Hab in this Habery hab = SignifyGroupHab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, mhab=mhab, ns=ns, temp=self.temp) hab.make(**kwa) # finish making group hab with injected pass throughs if ns is None: self.habs[hab.pre] = hab else: if ns not in self.namespaces: self.namespaces[ns] = dict() self.namespaces[ns][hab.pre] = hab return hab
[docs] def joinSignifyGroupHab(self, pre, name, mhab, smids, rmids=None, ns=None): """Make new Group Hab using group has group hab name, with lhab as local participant. Parameters: (non-pass-through): pre (str): qb64 identifier prefix of group name (str): human readable alias for group identifier mhab (Hab): group member (local) hab smids (list): group member signing ids (qb64) from which to extract inception event current signing keys rmids (list | None): group member rotation ids (qb64) from which to extract inception event next key digests if rmids is None then use assign smids to rmids if rmids is empty then no next key digests which means group identifier is no longer transferable. """ if mhab.pre not in smids and mhab.pre not in rmids: raise kering.ConfigurationError(f"Local member identifier " f"{mhab.pre} must be member of " f"smids ={smids} and/or " f"rmids={rmids}.") for mid in smids: if mid not in self.kevers: raise kering.ConfigurationError(f"KEL missing for signing member " f"identifier {mid} from group's " f"current members ={smids}") if rmids is not None: for rmid in rmids: if rmid not in self.kevers: raise kering.ConfigurationError(f"KEL missing for next member " f"identifier {rmid} in group's" f" next members ={rmids}") # create group Hab in this Habery hab = SignifyGroupHab(ks=self.ks, db=self.db, cf=self.cf, mgr=self.mgr, rtr=self.rtr, rvy=self.rvy, kvy=self.kvy, psr=self.psr, name=name, mhab=mhab, ns=ns, temp=self.temp) hab.pre = pre habord = basing.HabitatRecord(hid=hab.pre, sid=mhab.pre, smids=smids, rmids=rmids) hab.save(habord) hab.prefixes.add(pre) hab.inited = True if ns is None: self.habs[hab.pre] = hab else: if ns not in self.namespaces: self.namespaces[ns] = dict() self.namespaces[ns][hab.pre] = hab return hab
def deleteHab(self, name): hab = self.habByName(name) if not hab: return False if not self.db.habs.rem(keys=(name,)): return False del self.habs[hab.pre] self.db.prefixes.remove(hab.pre) return True
[docs] def extractMerfersMigers(self, smids, rmids=None): """ Extract the public key verfer and next digest diger from the current est event of all the members of the multisig group. Assumes that the KEL for each member is already in .kevers Parameters: smids (list): group signing member ids qb64 in group multisig rmids (list): group rotating member ids qb64 in group multisig """ if rmids is None: # default the same for both lists rmids = list(smids) merfers = [] # multisig group signing key verfers migers = [] # multisig group next key digest digers for mid in smids: kever = self.kevers[mid] verfers = kever.verfers merfers.append(verfers[0]) # assumes always verfers if len(verfers) > 1: raise kering.ConfigurationError("Identifier must have only one key, {} has {}" .format(mid, len(verfers))) for mid in rmids: kever = self.kevers[mid] digers = kever.ndigers if digers: # abandoned id may have empty next digers migers.append(digers[0]) if len(digers) > 1: raise kering.ConfigurationError("Identifier must have only one next key commitment, {} has {}" .format(mid, len(digers))) return merfers, migers
[docs] def close(self, clear=False): """Close resources. Parameters: clear is boolean, True means clear resource directories """ if self.ks: self.ks.close(clear=self.ks.temp or clear) if self.db: self.db.close(clear=self.db.temp or clear) if self.cf: self.cf.close(clear=self.cf.temp)
@property def kevers(self): """ Returns .db.kevers of all Kevers """ return self.db.kevers @property def prefixes(self): """ Returns .db.prefixes of local prefixes """ return self.db.prefixes
[docs] def habByPre(self, pre): """ Returns the Hab from and namespace including the default namespace. Args: pre (str): qb64 aid of hab to find Returns: Hab: Hab instance for the aid pre or None """ hab = None if pre in self.habs: hab = self.habs[pre] else: for nsp in self.namespaces.values(): if pre in nsp: hab = nsp[pre] return hab
[docs] def habByName(self, name, ns=None): """ Returns: hab (Hab): instance from .habs by name if any otherwise None Parameters: name (str): alias of Hab ns (str): optional namespace of hab """ if ns is not None: if (habord := self.db.nmsp.get(keys=(ns, name))) is not None: habs = self.namespaces[ns] return habs[habord.hid] if habord.hid in habs else None elif (habord := self.db.habs.get(name)) is not None: return self.habs[habord.hid] if habord.hid in self.habs else None return None
[docs] def reconfigure(self): """Apply configuration from config file managed by .cf. to this Habery Process any oobis or endpoints conf { dt: "isodatetime", curls: ["tcp://localhost:5620/"], iurls: ["tcp://localhost:5621/?name=eve"], } Config file is meant to be read only at init not changed by app at run time. Any dynamic app changes must go in database not config file that way we don't have to worry about multiple writers of cf. Use config file to preload database not as a database. Config file may have named sections for Habery or individual Habs as needed. """ conf = self.cf.get() if "dt" in conf: # datetime of config file dt = help.fromIso8601(conf["dt"]) # raises error if not convert if "iurls" in conf: # process OOBI URLs for oobi in conf["iurls"]: obr = basing.OobiRecord(date=help.toIso8601(dt)) self.db.oobis.put(keys=(oobi,), val=obr) if "durls" in conf: # process OOBI URLs for oobi in conf["durls"]: obr = basing.OobiRecord(date=help.toIso8601(dt)) self.db.oobis.put(keys=(oobi,), val=obr) if "wurls" in conf: # well known OOBI URLs for MFA for oobi in conf["wurls"]: obr = basing.OobiRecord(date=help.toIso8601(dt)) self.db.woobi.put(keys=(oobi,), val=obr)
@property def signator(self): """ signator for signing and verifying data at rest for this Habery environment Assumes db initialized. Returns: Signator: signer for data at rest """ return self._signator
SIGNER = "__signatory__"
[docs] class Signator: """ Signator will create one non-transferable identifier when it is first initialized and use that identifier to sign and verify any data it is passed. This class can be used to maintain BADA data ensuring that it is signed at rest. """
[docs] def __init__(self, db, name=SIGNER, **kwa): """ Create a Signator by checking for a signing AID in the Habery database and creating one if it does not exist. Args: db (Baser): Database environment for data signing """ self.db = db spre = self.db.hbys.get(name) if not spre: self._hab = Hab(name=name, db=db, **kwa) self._hab.make(transferable=False, hidden=True) self.pre = self._hab.pre self.db.hbys.pin(name, self.pre) else: self.pre = spre self._hab = Hab(name=name, db=db, pre=self.pre, **kwa)
[docs] def sign(self, ser): """ Sign the data in ser with the Signator's private key using the Manager Args: ser (bytes): Raw byte data to sign Returns: Cigar: signature object for non-transferable key """ return self._hab.sign(ser, indexed=False)[0]
[docs] def verify(self, ser, cigar): """ Args: ser(bytes): Raw byte data to verify against signature cigar (Cigar): Single non-transferable signature to verify Returns: bool: True means valid signature against data provided """ return self._hab.kever.verfers[0].verify(cigar.raw, ser)
[docs] class HaberyDoer(doing.Doer): """ Basic Habery Doer to initialize habery databases and config file. .cf, .ks, .db Inherited Attributes: .done is Boolean completion state: True means completed Otherwise incomplete. Incompletion maybe due to close or abort. Attributes: .habery is Habery subclass Inherited Properties: .tyme is float relative cycle time of associated Tymist .tyme obtained via injected .tymth function wrapper closure. .tymth is function wrapper closure returned by Tymist .tymeth() method. When .tymth is called it returns associated Tymist .tyme. .tymth provides injected dependency on Tymist tyme base. .tock is float, desired time in seconds between runs or until next run, non negative, zero means run asap Properties: Methods: .wind injects ._tymth dependency from associated Tymist to get its .tyme .__call__ makes instance callable Appears as generator function that returns generator .do is generator method that returns generator .enter is enter context action method .recur is recur context action method or generator method .exit is exit context method .close is close context method .abort is abort context method Hidden: ._tymth is injected function wrapper closure returned by .tymen() of associated Tymist instance that returns Tymist .tyme. when called. ._tock is hidden attribute for .tock property """
[docs] def __init__(self, habery, **kwa): """ Parameters: habery (Habery): instance """ super(HaberyDoer, self).__init__(**kwa) self.habery = habery
[docs] def enter(self): """ Enter context and set up Habery """ if not self.habery.inited: self.habery.setup(**self.habery._inits)
[docs] def exit(self): """Exit context and close Habery """ if self.habery.inited and self.habery.free: self.habery.close(clear=self.habery.temp)
[docs] class BaseHab: """ Hab class provides a given idetnifier controller's local resource environment i.e. hab or habitat. Includes dependency injection of database, keystore, configuration file as well as Kevery and key store Manager.. Attributes: (Injected) ks (keeping.Keeper): lmdb key store db (basing.Baser): lmdb data base for KEL etc cf (configing.Configer): config file instance mgr (keeping.Manager): creates and rotates keys in key store rtr (routing.Router): routes reply 'rpy' messages rvy (routing.Revery): factory that processes reply 'rpy' messages kvy (eventing.Kevery): factory for local processing of local event msgs psr (parsing.Parser): parses local messages for .kvy .rvy Attributes: name (str): alias of controller pre (str): qb64 prefix of own local controller or None if new temp (bool): True means testing: use weak level when salty algo for stretching in key creation for incept and rotate of keys for this hab.pre inited (bool): True means fully initialized wrt databases. False means not yet fully initialized delpre (str | None): delegator prefix if any else None Properties: kever (Kever): instance of key state of local controller kevers (dict): of eventing.Kever instances from KELs in local db keyed by qb64 prefix. Read through cache of of kevers of states for KELs in db.states iserder (coring.Serder): own inception event prefixes (OrderedSet): local prefixes for .db accepted (bool): True means accepted into local KEL. False otherwise """
[docs] def __init__(self, ks, db, cf, mgr, rtr, rvy, kvy, psr, *, name='test', ns=None, pre=None, temp=False): """ Initialize instance. Injected Parameters: (injected dependencies) ks (keeping.Keeper): lmdb key store db (basing.Baser): lmdb data base for KEL etc cf (configing.Configer): config file instance mgr (keeping.Manager): creates and rotates keys in key store rtr (routing.Router): routes reply 'rpy' messages rvy (routing.Revery): factory that processes reply 'rpy' messages kvy (eventing.Kevery): factory for local processing of local event msgs psr (parsing.Parser): parses local messages for .kvy .rvy Parameters: name (str): alias name for local controller of habitat pre (str | None): qb64 identifier prefix of own local controller else None temp (bool): True means testing: use weak level when salty algo for stretching in key creation for incept and rotate of keys for this hab.pre """ self.db = db # injected self.ks = ks # injected self.cf = cf # injected self.mgr = mgr # injected self.rtr = rtr # injected self.rvy = rvy # injected self.kvy = kvy # injected self.psr = psr # injected self.name = name self.ns = ns self.pre = pre # wait to setup until after db is known to be opened self.temp = True if temp else False self.inited = False self.delpre = None # assigned laster if delegated
[docs] def make(self, DnD, code, data, delpre, estOnly, isith, verfers, nsith, digers, toad, wits): """ Creates Serder of inception event for provided parameters. Assumes injected dependencies were already setup. Parameters: isith (int | str | list | None): incepting signing threshold as int, str hex, or list weighted if any, otherwise compute default from verfers code (str): prefix derivation code default Blake3 nsith (int, str, list | None ): next signing threshold as int, str hex or list weighted if any, otherwise compute default from digers verfers (list[Verfer]): Verfer instances for initial signing keys digers (list[Diger] | None) Diger instances for next key digests toad (int |str| None): int or str hex of witness threshold if specified else compute default based on number of wits (backers) wits (list | None): qb64 prefixes of witnesses if any delpre (str | None): qb64 of delegator identifier prefix if any estOnly (bool | None): True means add trait eventing.TraitCodex.EstOnly which means only establishment events allowed in KEL for this Hab False (default) means allows non-est events and no trait is added. DnD (bool): True means add trait of eventing.TraitCodex.DnD which means do not allow delegated identifiers from this identifier False (default) means do allow and no trait is added. data (list | None): seal dicts """ icount = len(verfers) ncount = len(digers) if digers is not None else 0 if isith is None: # compute default isith = f"{max(1, ceil(icount / 2)):x}" if nsith is None: # compute default nsith = f"{max(0, ceil(ncount / 2)):x}" cst = coring.Tholder(sith=isith).sith # current signing threshold nst = coring.Tholder(sith=nsith).sith # next signing threshold cnfg = [] if estOnly: cnfg.append(eventing.TraitCodex.EstOnly) if DnD: cnfg.append(eventing.TraitCodex.DoNotDelegate) self.delpre = delpre keys = [verfer.qb64 for verfer in verfers] if self.delpre: serder = eventing.delcept(keys=keys, delpre=self.delpre, isith=cst, nsith=nst, ndigs=[diger.qb64 for diger in digers], toad=toad, wits=wits, cnfg=cnfg, code=code) else: serder = eventing.incept(keys=keys, isith=cst, nsith=nst, ndigs=[diger.qb64 for diger in digers], toad=toad, wits=wits, cnfg=cnfg, code=code, data=data) return serder
def save(self, habord): if self.ns is None: self.db.habs.pin(keys=self.name, val=habord) else: self.db.nmsp.put(keys=(self.ns, self.name), val=habord)
[docs] def reconfigure(self): """Apply configuration from config file managed by .cf. to this Hab. Assumes that .pre and signing keys have been setup in order to create own endpoint auth when provided in .cf. conf { dt: "isodatetime", curls: ["tcp://localhost:5620/"], iurls: ["tcp://localhost:5621/?name=eve"] } Config file is meant to be read only at init not changed by app at run time. Any dynamic app changes must go in database not config file that way we don't have to worry about multiple writers of cf. Use config file to preload database not as a database. Config file may have named sections for Habery or individual Habs as needed. """ conf = self.cf.get() if self.name not in conf: return conf = conf[self.name] if "dt" in conf: # datetime of config file dt = help.fromIso8601(conf["dt"]) # raises error if not convert msgs = bytearray() msgs.extend(self.makeEndRole(eid=self.pre, role=kering.Roles.controller, stamp=help.toIso8601(dt=dt))) if "curls" in conf: curls = conf["curls"] for url in curls: splits = urlsplit(url) scheme = (splits.scheme if splits.scheme in kering.Schemes else kering.Schemes.http) msgs.extend(self.makeLocScheme(url=url, scheme=scheme, stamp=help.toIso8601(dt=dt))) self.psr.parse(ims=msgs)
@property def iserder(self): """ Return serder of inception event """ if (dig := self.db.getKeLast(eventing.snKey(pre=self.pre, sn=0))) is None: raise kering.ConfigurationError("Missing inception event in KEL for " "Habitat pre={}.".format(self.pre)) if (raw := self.db.getEvt(eventing.dgKey(pre=self.pre, dig=bytes(dig)))) is None: raise kering.ConfigurationError("Missing inception event for " "Habitat pre={}.".format(self.pre)) return serdering.SerderKERI(raw=bytes(raw)) @property def kevers(self): """ Returns .db.kevers """ return self.db.kevers @property def accepted(self): return self.pre in self.kevers @property def kever(self): """ Returns kever for its .pre """ return self.kevers[self.pre] if self.accepted else None @property def prefixes(self): """ Returns .db.prefixes """ return self.db.prefixes
[docs] def incept(self, **kwa): """Alias for .make """ self.make(**kwa)
[docs] def rotate(self, *, verfers=None, digers=None, isith=None, nsith=None, toad=None, cuts=None, adds=None, data=None): """ Perform rotation operation. Register rotation in database. Returns: bytearrayrotation message with attached signatures. Parameters: verfers (list | None): Verfer instances of public keys qb64 digers (list | None): Diger instances of public next key digests qb64 isith (int |str | None): current signing threshold as int or str hex or list of str weights default is prior next sith nsith (int |str | None): next, next signing threshold as int or str hex or list of str weights default is based on isith when None toad (int | str | None): hex of witness threshold after cuts and adds cuts (list | None): of qb64 pre of witnesses to be removed from witness list adds (list | None): of qb64 pre of witnesses to be added to witness list data (list | None): of dicts of committed data such as seals """ # recall that kever.pre == self.pre kever = self.kever # before rotation kever is prior next if isith is None: isith = kever.ntholder.sith # use prior next sith as default if nsith is None: nsith = isith # use new current as default if isith is None: # compute default from newly rotated verfers above isith = f"{max(1, ceil(len(verfers) / 2)):x}" if nsith is None: # compute default from newly rotated digers above nsith = f"{max(0, ceil((len(digers) if digers is not None else 0) / 2)):x}" cst = coring.Tholder(sith=isith).sith # current signing threshold nst = coring.Tholder(sith=nsith).sith # next signing threshold keys = [verfer.qb64 for verfer in verfers] indices = [] for idx, diger in enumerate(kever.ndigers): pdigs = [coring.Diger(ser=verfer.qb64b, code=diger.code).qb64 for verfer in verfers] if diger.qb64 in pdigs: indices.append(idx) if not kever.ntholder.satisfy(indices): raise kering.ValidationError("invalid rotation, new key set unable to satisfy prior next signing threshold") if kever.delegator is not None: # delegator only shows up in delcept serder = eventing.deltate(pre=kever.prefixer.qb64, keys=keys, dig=kever.serder.said, sn=kever.sner.num + 1, isith=cst, nsith=nst, ndigs=[diger.qb64 for diger in digers], toad=toad, wits=kever.wits, cuts=cuts, adds=adds, data=data) else: serder = eventing.rotate(pre=kever.prefixer.qb64, keys=keys, dig=kever.serder.said, sn=kever.sner.num + 1, isith=cst, nsith=nst, ndigs=[diger.qb64 for diger in digers], toad=toad, wits=kever.wits, cuts=cuts, adds=adds, data=data) # sign handles group hab with .mhab case sigers = self.sign(ser=serder.raw, verfers=verfers, rotated=True) # update own key event verifier state msg = eventing.messagize(serder, sigers=sigers) try: self.kvy.processEvent(serder=serder, sigers=sigers) except MissingSignatureError: pass except Exception as ex: raise kering.ValidationError("Improper Habitat rotation for " "pre={self.pre}.") from ex return msg
[docs] def interact(self, *, data=None): """ Perform interaction operation. Register interaction in database. Returns: bytearray interaction message with attached signatures. """ kever = self.kever serder = eventing.interact(pre=kever.prefixer.qb64, dig=kever.serder.said, sn=kever.sner.num + 1, data=data) sigers = self.sign(ser=serder.raw) msg = eventing.messagize(serder, sigers=sigers) try: # verify event, update kever state, and escrow if group self.kvy.processEvent(serder=serder, sigers=sigers) except MissingSignatureError: pass except Exception as ex: raise kering.ValidationError("Improper Habitat interaction for " "pre={}.".format(self.pre)) from ex return msg
[docs] def sign(self, ser, verfers=None, indexed=True, indices=None, ondices=None, **kwa): """Sign given serialization ser using appropriate keys. Use provided verfers or .kever.verfers to lookup keys to sign. Parameters: ser (bytes): serialization to sign verfers (list[Verfer] | None): Verfer instances to get pub verifier keys to lookup private siging keys. verfers None means use .kever.verfers. Assumes that when group and verfers is not None then provided verfers must be .kever.verfers indexed (bool): When not mhab then True means use use indexed signatures and return list of Siger instances. False means do not use indexed signatures and return list of Cigar instances indices (list[int] | None): indices (offsets) when indexed == True. See Manager.sign ondices (list[int | None] | None): other indices (offsets) when indexed is True. See Manager.sign """ if verfers is None: verfers = self.kever.verfers # when group these provide group signing keys return self.mgr.sign(ser=ser, verfers=verfers, indexed=indexed, indices=indices, ondices=ondices)
[docs] def decrypt(self, ser, verfers=None, **kwa): """Sign given serialization ser using appropriate keys. Use provided verfers or .kever.verfers to lookup keys to sign. Parameters: ser (bytes): serialization to sign verfers (list[Verfer] | None): Verfer instances to get pub verifier keys to lookup private siging keys. verfers None means use .kever.verfers. Assumes that when group and verfers is not None then provided verfers must be .kever.verfers """ if verfers is None: verfers = self.kever.verfers # when group these provide group signing keys return self.mgr.decrypt(ser=ser, verfers=verfers, )
[docs] def query(self, pre, src, query=None, **kwa): """ Create, sign and return a `qry` message against the attester for the prefix Parameters: pre (str): qb64 identifier prefix being queried for src (str): qb64 identifier prefix of attester being queried query (dict): addition query modifiers to include in `q` **kwa (dict): keyword arguments passed to eventing.query Returns: bytearray: signed query event """ query = query if query is not None else dict() query['i'] = pre query["src"] = src serder = eventing.query(query=query, **kwa) return self.endorse(serder, last=True)
[docs] def endorse(self, serder, last=False, pipelined=True): """ Returns msg with own endorsement of msg from serder with attached signature groups based on own pre transferable or non-transferable. Parameters: serder (Serder): instance of msg last (bool): True means use SealLast. False means use SealEvent query messages use SealLast pipelined (bool): True means use pipelining attachment code Useful for endorsing message when provided via serder such as state, reply, query or similar. """ if self.kever.prefixer.transferable: # create SealEvent or SealLast for endorser's est evt whose keys are # used to sign kever = self.kever if last: seal = eventing.SealLast(i=kever.prefixer.qb64) else: seal = eventing.SealEvent(i=kever.prefixer.qb64, s="{:x}".format(kever.lastEst.s), d=kever.lastEst.d) sigers = self.sign(ser=serder.raw, indexed=True) msg = eventing.messagize(serder=serder, sigers=sigers, seal=seal, pipelined=pipelined) else: cigars = self.sign(ser=serder.raw, indexed=False) msg = eventing.messagize(serder=serder, cigars=cigars, pipelined=pipelined) return msg
[docs] def exchange(self, route, payload, recipient, date=None, eid=None, dig=None, modifiers=None, embeds=None, save=False): """ Returns signed exn, message of serder with count code and receipt couples (pre+cig) Builds msg and then processes it into own db to validate """ # sign serder event serder, end = exchanging.exchange(route=route, payload=payload, sender=self.pre, recipient=recipient, date=date, dig=dig, modifiers=modifiers, embeds=embeds) if self.kever.prefixer.transferable: msg = self.endorse(serder=serder, pipelined=False) else: cigars = self.sign(ser=serder.raw, indexed=False) msg = eventing.messagize(serder, cigars=cigars) msg.extend(end) if save: self.psr.parseOne(ims=bytearray(msg)) # process local copy into db return msg
[docs] def receipt(self, serder): """ Returns own receipt, rct, message of serder with count code and receipt couples (pre+cig) Builds msg and then processes it into own db to validate """ ked = serder.ked reserder = eventing.receipt(pre=ked["i"], sn=int(ked["s"], 16), said=serder.said) # sign serder event if self.kever.prefixer.transferable: seal = eventing.SealEvent(i=self.pre, s="{:x}".format(self.kever.lastEst.s), d=self.kever.lastEst.d) sigers = self.sign(ser=serder.raw, indexed=True) msg = eventing.messagize(serder=reserder, sigers=sigers, seal=seal) else: cigars = self.sign(ser=serder.raw, indexed=False) msg = eventing.messagize(reserder, cigars=cigars) self.psr.parseOne(ims=bytearray(msg)) # process local copy into db return msg
[docs] def witness(self, serder): """ Returns own receipt, rct, message of serder with count code and witness indexed receipt signatures if key state of serder.pre shows that own pre is a current witness of event in serder Before calling this must check that serder being witnessed has been accepted as valid event into controller's KEL """ if self.kever.prefixer.transferable: # not non-transferable prefix raise ValueError("Attempt to create witness receipt with" " transferable pre={}.".format(self.pre)) ked = serder.ked if serder.pre not in self.kevers: raise ValueError("Attempt by {} to witness event with missing key " "state.".format(self.pre)) kever = self.kevers[serder.pre] if self.pre not in kever.wits: print("Attempt by {} to witness event of {} when not a " "witness in wits={}.".format(self.pre, serder.pre, kever.wits)) index = kever.wits.index(self.pre) reserder = eventing.receipt(pre=ked["i"], sn=int(ked["s"], 16), said=serder.said) # assumes witness id is nontrans so public key is same as pre wigers = self.mgr.sign(ser=serder.raw, pubs=[self.pre], indices=[index]) msg = eventing.messagize(reserder, wigers=wigers, pipelined=True) self.psr.parseOne(ims=bytearray(msg)) # process local copy into db return msg
[docs] def replay(self, pre=None, fn=0): """ Returns replay of FEL first seen event log for pre starting from fn Default pre is own .pre Parameters: pre is qb64 str or bytes of identifier prefix. default is own .pre fn is int first seen ordering number """ if not pre: pre = self.pre msgs = bytearray() kever = self.kevers[pre] for msg in self.db.cloneDelegation(kever=kever): msgs.extend(msg) for msg in self.db.clonePreIter(pre=pre, fn=fn): msgs.extend(msg) return msgs
[docs] def replayAll(self, key=b''): """ Returns replay of FEL first seen event log for all pre starting at key Parameters: key (bytes): fnKey(pre, fn) """ msgs = bytearray() for msg in self.db.cloneAllPreIter(key=key): msgs.extend(msg) return msgs
[docs] def makeOtherEvent(self, pre, sn): """ Returns: messagized bytearray message with attached signatures of own event at sequence number sn from retrieving event at sn and associated signatures from database. Parameters: sn is int sequence number of event """ if pre not in self.kevers: return None msg = bytearray() dig = self.db.getKeLast(dbing.snKey(pre, sn)) if dig is None: raise kering.MissingEntryError("Missing event for pre={} at sn={}." "".format(pre, sn)) dig = bytes(dig) key = dbing.dgKey(pre, dig) # digest key msg.extend(self.db.getEvt(key)) msg.extend(coring.Counter(code=coring.CtrDex.ControllerIdxSigs, count=self.db.cntSigs(key)).qb64b) # attach cnt for sig in self.db.getSigsIter(key): msg.extend(sig) # attach sig return msg
[docs] def fetchEnd(self, cid: str, role: str, eid: str): """ Returns: endpoint (basing.EndpointRecord): instance or None """ return self.db.ends.get(keys=(cid, role, eid))
[docs] def fetchLoc(self, eid: str, scheme: str = kering.Schemes.http): """ Returns: location (basing.LocationRecord): instance or None """ return self.db.locs.get(keys=(eid, scheme))
[docs] def fetchEndAllowed(self, cid: str, role: str, eid: str): """ Returns: allowed (bool): True if eid is allowed as endpoint provider for cid in role. False otherwise. Parameters: cid (str): identifier prefix qb64 of controller authZ endpoint provided eid in role role (str): endpoint role such as (controller, witness, watcher, etc) eid (str): identifier prefix qb64 of endpoint provider in role """ end = self.db.ends.get(keys=(cid, role, eid)) return end.allowed if end else None
[docs] def fetchEndEnabled(self, cid: str, role: str, eid: str): """ Returns: allowed (bool): True if eid is allowed as endpoint provider for cid in role. False otherwise. Parameters: cid (str): identifier prefix qb64 of controller authZ endpoint provided eid in role role (str): endpoint role such as (controller, witness, watcher, etc) eid (str): identifier prefix qb64 of endpoint provider in role """ end = self.db.ends.get(keys=(cid, role, eid)) return end.enabled if end else None
[docs] def fetchEndAuthzed(self, cid: str, role: str, eid: str): """ Returns: allowed (bool): True if eid is allowed as endpoint provider for cid in role. False otherwise. Parameters: cid (str): identifier prefix qb64 of controller authZ endpoint provided eid in role role (str): endpoint role such as (controller, witness, watcher, etc) eid (str): identifier prefix qb64 of endpoint provider in role """ end = self.db.ends.get(keys=(cid, role, eid)) return (end.enabled or end.allowed) if end else None
[docs] def fetchUrl(self, eid: str, scheme: str = kering.Schemes.http): """ Returns: url (str): for endpoint provider given by eid empty string when url is nullified None when no location record """ loc = self.db.locs.get(keys=(eid, scheme)) return loc.url if loc else loc
[docs] def fetchUrls(self, eid: str, scheme: str = ""): """ Returns: surls (hicting.Mict): urls keyed by scheme for given eid. Assumes that user independently verifies that the eid is allowed for a given cid and role. If url is empty then does not return Parameters: eid (str): identifier prefix qb64 of endpoint provider scheme (str): url scheme """ return hicting.Mict([(keys[1], loc.url) for keys, loc in self.db.locs.getItemIter(keys=(eid, scheme)) if loc.url])
[docs] def fetchRoleUrls(self, cid: str, *, role: str = "", scheme: str = "", eids=None, enabled: bool = True, allowed: bool = True): """ Returns: rurls (hicting.Mict): of nested dicts. The top level dict rurls is keyed by role for a given cid. Each value in rurls is eurls dict keyed by the eid of authorized endpoint provider and each value in eurls is a surls dict keyed by scheme Parameters: cid (str): identifier prefix qb64 of controller authZ endpoint provided eid in role role (str): endpoint role such as (controller, witness, watcher, etc) scheme (str): url scheme eids (list): when provided restrict returns to only eids in eids enabled (bool): True means fetch any allowed witnesses as well allowed (bool): True means fetech any enabled witnesses as well """ if eids is None: eids = [] rurls = hicting.Mict() if role == kering.Roles.witness: if kever := self.kevers[cid] if cid in self.kevers else None: # latest key state for cid for eid in kever.wits: if not eids or eid in eids: surls = self.fetchUrls(eid, scheme=scheme) if surls: rurls.add(kering.Roles.witness, hicting.Mict([(eid, surls)])) for (_, erole, eid), end in self.db.ends.getItemIter(keys=(cid, role)): if (enabled and end.enabled) or (allowed and end.allowed): if not eids or eid in eids: surls = self.fetchUrls(eid, scheme=scheme) if surls: rurls.add(erole, hicting.Mict([(eid, surls)])) return rurls
[docs] def fetchWitnessUrls(self, cid: str, scheme: str = "", eids=None, enabled: bool = True, allowed: bool = True): """ Fetch witness urls for witnesses of cid at latest key state or enabled or allowed witnesses if not a witness at latest key state. Returns: rurls (hicting.Mict): of nested dicts. The top level dict rurls is keyed by role for a given cid. Each value in rurls is eurls dict dict keyed by the eid of authorized endpoint provider and each value in eurls is a surls dict keyed by scheme Parameters: cid (str): identifier prefix qb64 of controller authZ endpoint provided eid is witness scheme (str): url scheme eids (list): when provided restrict returns to only eids in eids enabled (bool): True means fetch any allowed witnesses as well allowed (bool): True means fetech any enabled witnesses as well """ return (self.fetchRoleUrls(cid=cid, role=kering.Roles.witness, scheme=scheme, eids=eids, enabled=enabled, allowed=allowed))
[docs] def endsFor(self, pre): """ Load Authroized endpoints for provided AID Args: pre (str): qb64 aid for which to load ends Returns: dict: nest dict of Roles -> eid -> Schemes -> endpoints """ ends = dict() for (_, erole, eid), end in self.db.ends.getItemIter(keys=(pre,)): locs = dict() urls = self.fetchUrls(eid=eid, scheme="") for rscheme, url in urls.firsts(): locs[rscheme] = url if erole not in ends: ends[erole] = dict() ends[erole][eid] = locs witrolls = dict() if kever := self.kevers[pre] if pre in self.kevers else None: for eid in kever.wits: locs = dict() urls = self.fetchUrls(eid=eid, scheme="") for rscheme, url in urls.firsts(): locs[rscheme] = url witrolls[eid] = locs if len(witrolls) > 0: ends[Roles.witness] = witrolls return ends
[docs] def reply(self, **kwa): """ Returns: msg (bytearray): reply message Parameters: route is route path string that indicates data flow handler (behavior) to processs the reply data is list of dicts of comitted data such as seals dts is date-time-stamp of message at time or creation version is Version instance kind is serialization kind """ return self.endorse(eventing.reply(**kwa))
[docs] def makeEndRole(self, eid, role=kering.Roles.controller, allow=True, stamp=None): """ Returns: msg (bytearray): reply message allowing/disallowing endpoint provider eid in role Parameters: eid (str): qb64 of endpoint provider to be authorized role (str): authorized role for eid allow (bool): True means add eid at role as authorized False means cut eid at role as unauthorized stamp (str): date-time-stamp RFC-3339 profile of iso8601 datetime. None means use now. """ data = dict(cid=self.pre, role=role, eid=eid) route = "/end/role/add" if allow else "/end/role/cut" return self.reply(route=route, data=data, stamp=stamp)
def loadEndRole(self, cid, eid, role=kering.Roles.controller): msgs = bytearray() end = self.db.ends.get(keys=(cid, role, eid)) if end and (end.enabled or end.allowed): said = self.db.eans.get(keys=(cid, role, eid)) serder = self.db.rpys.get(keys=(said.qb64,)) cigars = self.db.scgs.get(keys=(said.qb64,)) tsgs = eventing.fetchTsgs(db=self.db.ssgs, saider=said) if len(cigars) == 1: (verfer, cigar) = cigars[0] cigar.verfer = verfer else: cigar = None if len(tsgs) > 0: (prefixer, seqner, diger, sigers) = tsgs[0] seal = eventing.SealEvent(i=prefixer.qb64, s=seqner.snh, d=diger.qb64) else: sigers = None seal = None msgs.extend(eventing.messagize(serder=serder, cigars=[cigar] if cigar else [], sigers=sigers, seal=seal, pipelined=True)) return msgs
[docs] def makeLocScheme(self, url, eid=None, scheme="http", stamp=None): """ Returns: msg (bytearray): reply message of own url service endpoint at scheme Parameters: url (str): url of endpoint, may have scheme missing or not If url is empty then nullifies location eid (str): qb64 of endpoint provider to be authorized scheme (str): url scheme must matche scheme in url if any stamp (str): date-time-stamp RFC-3339 profile of iso8601 datetime. None means use now. """ eid = eid if eid is not None else self.pre data = dict(eid=eid, scheme=scheme, url=url) return self.reply(route="/loc/scheme", data=data, stamp=stamp)
[docs] def replyLocScheme(self, eid, scheme=""): """ Returns a reply message stream composed of entries authed by the given eid from the appropriate reply database including associated attachments in order to disseminate (percolate) BADA reply data authentication proofs. Currently uses promiscuous model for permitting endpoint discovery. Future is to use identity constraint graph to constrain discovery of whom by whom. eid and not scheme then: loc url for all schemes at eid eid and scheme then: loc url for scheme at eid Parameters: eid (str): endpoint provider id scheme (str): url scheme """ msgs = bytearray() urls = self.fetchUrls(eid=eid, scheme=scheme) for rscheme, url in urls.firsts(): msgs.extend(self.makeLocScheme(eid=eid, url=url, scheme=rscheme)) return msgs
def loadLocScheme(self, eid, scheme=None): msgs = bytearray() keys = (eid, scheme) if scheme else (eid,) for (pre, _), said in self.db.lans.getItemIter(keys=keys): serder = self.db.rpys.get(keys=(said.qb64,)) cigars = self.db.scgs.get(keys=(said.qb64,)) tsgs = eventing.fetchTsgs(db=self.db.ssgs, saider=said) if len(cigars) == 1: (verfer, cigar) = cigars[0] cigar.verfer = verfer else: cigar = None if len(tsgs) > 0: (prefixer, seqner, diger, sigers) = tsgs[0] seal = eventing.SealEvent(i=prefixer.qb64, s=seqner.snh, d=diger.qb64) else: sigers = None seal = None msgs.extend(eventing.messagize(serder=serder, cigars=[cigar] if cigar else [], sigers=sigers, seal=seal, pipelined=True)) return msgs
[docs] def replyEndRole(self, cid, role=None, eids=None, scheme=""): """ Returns a reply message stream composed of entries authed by the given cid from the appropriate reply database including associated attachments in order to disseminate (percolate) BADA reply data authentication proofs. Currently uses promiscuous model for permitting endpoint discovery. Future is to use identity constraint graph to constrain discovery of whom by whom. cid and not role and not scheme then: end authz for all eids in all roles and loc url for all schemes at each eid if eids then only eids in eids else all eids cid and not role and scheme then: end authz for all eid in all roles and loc url for scheme at each eid if eids then only eids in eids else all eids cid and role and not scheme then: end authz for all eid in role and loc url for all schemes at each eid if eids then only eids in eids else all eids cid and role and scheme then: end authz for all eid in role and loc url for scheme at each eid if eids then only eids in eids else all eids Parameters: cid (str): identifier prefix qb64 of controller authZ endpoint provided eid is witness role (str): authorized role for eid eids (list): when provided restrict returns to only eids in eids scheme (str): url scheme """ msgs = bytearray() if eids is None: eids = [] if cid not in self.kevers: return msgs msgs.extend(self.replay(cid)) kever = self.kevers[cid] witness = self.pre in kever.wits # see if we are cid's witness if role == kering.Roles.witness: # latest key state for cid for eid in kever.wits: if not eids or eid in eids: if eid == self.pre: msgs.extend(self.replyLocScheme(eid=eid, scheme=scheme)) else: msgs.extend(self.loadLocScheme(eid=eid, scheme=scheme)) if not witness: # we are not witness, send auth records msgs.extend(self.makeEndRole(eid=eid, role=role)) for (_, erole, eid), end in self.db.ends.getItemIter(keys=(cid,)): if (end.enabled or end.allowed) and (not role or role == erole) and (not eids or eid in eids): msgs.extend(self.loadLocScheme(eid=eid, scheme=scheme)) msgs.extend(self.loadEndRole(cid=cid, eid=eid, role=erole)) return msgs
[docs] def replyToOobi(self, aid, role, eids=None): """ Returns a reply message stream composed of entries authed by the given aid from the appropriate reply database including associated attachments in order to disseminate (percolate) BADA reply data authentication proofs. Currently uses promiscuous model for permitting oobi initiated endpoint discovery. Future is to use identity constraint graph to constrain discovery of whom by whom. This method is entry point for initiating replies generated by .replyEndRole and/or .replyLocScheme Parameters: aid (str): qb64 of identifier in oobi, may be cid or eid role (str): authorized role for eid eids (list): when provided restrict returns to only eids in eids """ # default logic is that if self.pre is witness of aid and has a loc url # for self then reply with loc scheme for all witnesses even if self # not permiteed in .habs.oobis return self.replyEndRole(cid=aid, role=role, eids=eids)
[docs] def getOwnEvent(self, sn, allowPartiallySigned=False): """ Returns: message Serder and controller signatures of own event at sequence number sn from retrieving event at sn and associated signatures from database. Parameters: sn (int): is int sequence number of event allowPartiallySigned(bool): True means attempt to load from partial signed escrow """ key = dbing.snKey(self.pre, sn) dig = self.db.getKeLast(key) if dig is None and allowPartiallySigned: dig = self.db.getPseLast(key) if dig is None: raise kering.MissingEntryError("Missing event for pre={} at sn={}." "".format(self.pre, sn)) dig = bytes(dig) key = dbing.dgKey(self.pre, dig) # digest key msg = self.db.getEvt(key) serder = serdering.SerderKERI(raw=bytes(msg)) sigs = [] for sig in self.db.getSigsIter(key): sigs.append(coring.Siger(qb64b=bytes(sig))) couple = self.db.getAes(key) return serder, sigs, couple
[docs] def makeOwnEvent(self, sn, allowPartiallySigned=False): """ Returns: messagized bytearray message with attached signatures of own event at sequence number sn from retrieving event at sn and associated signatures from database. Parameters: sn(int): is int sequence number of event allowPartiallySigned(bool): True means attempt to load from partial signed escrow """ msg = bytearray() serder, sigs, couple = self.getOwnEvent(sn=sn, allowPartiallySigned=allowPartiallySigned) msg.extend(serder.raw) msg.extend(coring.Counter(code=coring.CtrDex.ControllerIdxSigs, count=len(sigs)).qb64b) # attach cnt for sig in sigs: msg.extend(sig.qb64b) # attach sig if couple is not None: msg.extend(coring.Counter(code=coring.CtrDex.SealSourceCouples, count=1).qb64b) msg.extend(couple) return msg
[docs] def makeOwnInception(self, allowPartiallySigned=False): """ Returns: messagized bytearray message with attached signatures of own inception event by retrieving event and signatures from database. """ return self.makeOwnEvent(sn=0, allowPartiallySigned=allowPartiallySigned)
[docs] def processCues(self, cues): """ Returns bytearray of messages as a result of processing all cues Parameters: cues is deque of cues """ msgs = bytearray() # outgoing messages for msg in self.processCuesIter(cues): msgs.extend(msg) return msgs
[docs] def processCuesIter(self, cues): """ Iterate through cues and yields one or more msgs for each cue. Parameters: cues is deque of cues """ while cues: # iteratively process each cue in cues msgs = bytearray() cue = cues.pull() #cues.popleft() cueKin = cue["kin"] # type or kind of cue if cueKin in ("receipt",): # cue to receipt a received event from other pre cuedSerder = cue["serder"] # Serder of received event for other pre cuedKed = cuedSerder.ked cuedPrefixer = coring.Prefixer(qb64=cuedKed["i"]) logger.info("%s got cue: kin=%s\n%s\n\n", self.pre, cueKin, json.dumps(cuedKed, indent=1)) if cuedKed["t"] == coring.Ilks.icp: dgkey = dbing.dgKey(self.pre, self.iserder.said) found = False if cuedPrefixer.transferable: # find if have rct from other pre for own icp for quadruple in self.db.getVrcsIter(dgkey): if bytes(quadruple).decode("utf-8").startswith(cuedKed["i"]): found = True # yes so don't send own inception else: # find if already rcts of own icp for couple in self.db.getRctsIter(dgkey): if bytes(couple).decode("utf-8").startswith(cuedKed["i"]): found = True # yes so don't send own inception if not found: # no receipt from remote so send own inception # no vrcs or rct of own icp from remote so send own inception msgs.extend(self.makeOwnInception()) msgs.extend(self.receipt(cuedSerder)) yield msgs elif cueKin in ("replay",): msgs = cue["msgs"] yield msgs elif cueKin in ("reply",): data = cue["data"] route = cue["route"] msg = self.reply(data=data, route=route) yield msg
# ToDo XXXX cue for kin = "query" various types of queries # (query witness, query delegation etc) # ToDo XXXX cue for kin = "notice" new event # ToDo XXXX cue for kin = "witness" to create witness receipt own is witness # ToDo XXXX cue for kin = "noticeBadCloneFN" # ToDo XXXX cue for kin = "approveDelegation" own is delegator # ToDo XXXX cue for kin = "keyStateSaved" # ToDo XXXX cue for kin = "psUnescrow" # ToDo XXXX cue for kin = "stream" # ToDo XXXX cue for kin = "invalid" def witnesser(self): return True
[docs] class Hab(BaseHab): """ Hab class provides a given idetnifier controller's local resource environment i.e. hab or habitat. Includes dependency injection of database, keystore, configuration file as well as Kevery and key store Manager.. Attributes: (Injected) ks (keeping.Keeper): lmdb key store db (basing.Baser): lmdb data base for KEL etc cf (configing.Configer): config file instance mgr (keeping.Manager): creates and rotates keys in key store rtr (routing.Router): routes reply 'rpy' messages rvy (routing.Revery): factory that processes reply 'rpy' messages kvy (eventing.Kevery): factory for local processing of local event msgs psr (parsing.Parser): parses local messages for .kvy .rvy Attributes: name (str): alias of controller pre (str): qb64 prefix of own local controller or None if new mhab (Hab | None): group member (local) hab when this Hab is multisig group else None smids (list | None): group signing member ids qb64 when this Hab is group else None rmids (list | None): group rotating member ids qb64 when this Hab is group else None temp (bool): True means testing: use weak level when salty algo for stretching in key creation for incept and rotate of keys for this hab.pre inited (bool): True means fully initialized wrt databases. False means not yet fully initialized delpre (str | None): delegator prefix if any else None Properties: kever (Kever): instance of key state of local controller kevers (dict): of eventing.Kever instances from KELs in local db keyed by qb64 prefix. Read through cache of of kevers of states for KELs in db.states iserder (serdering.SerderKERI): own inception event prefixes (OrderedSet): local prefixes for .db accepted (bool): True means accepted into local KEL. False otherwise """ def __init__(self, **kwa): super(Hab, self).__init__(**kwa)
[docs] def make(self, *, secrecies=None, iridx=0, code=coring.MtrDex.Blake3_256, dcode=coring.MtrDex.Blake3_256, icode=coring.MtrDex.Ed25519_Seed, transferable=True, isith=None, icount=1, nsith=None, ncount=None, toad=None, wits=None, delpre=None, estOnly=False, DnD=False, hidden=False, data=None, algo=None, salt=None, tier=None): """ Finish setting up or making Hab from parameters includes inception. Assumes injected dependencies were already setup. Parameters: secrecies (list | None): list of secrets to preload key pairs if any iridx (int): initial rotation index after ingestion of secrecies code (str): prefix derivation code default Blake3 icode (str): signing key code default Ed25519 dcode (str): next key derivation code default Blake3 transferable (bool): True means pre is transferable (default) False means pre is nontransferable isith (int | str | list | None): incepting signing threshold as int, str hex, or list weighted if any, otherwise compute default from verfers icount (int): incepting key count for number of keys. default 1 nsith (int, str, list | None ): next signing threshold as int, str hex or list weighted if any, otherwise compute default from digers ncount (int | None): next key count for number of next keys toad (int |str| None): int or str hex of witness threshold if specified else compute default based on number of wits (backers) wits (list | None): qb64 prefixes of witnesses if any delpre (str | None): qb64 of delegator identifier prefix if any estOnly (bool | None): True means add trait eventing.TraitCodex.EstOnly which means only establishment events allowed in KEL for this Hab False (default) means allows non-est events and no trait is added. DnD (bool): True means add trait of eventing.TraitCodex.DnD which means do not allow delegated identifiers from this identifier False (default) means do allow and no trait is added. hidden (bool): A hidden Hab is not included in the list of Habs. data (list | None): seal dicts algo is str key creation algorithm code salt(str): qb64 salt for randomization when salty algorithm used tier(str): is str security criticality tier code when using salty algorithm """ if not (self.ks.opened and self.db.opened and self.cf.opened): raise kering.ClosedError("Attempt to make Hab with unopened " "resources.") if nsith is None: nsith = isith if ncount is None: ncount = icount if not transferable: ncount = 0 # next count nsith = '0' code = coring.MtrDex.Ed25519N stem = self.name if self.ns is None else f"{self.ns}{self.name}" if secrecies: # replay ipre, _ = self.mgr.ingest(secrecies, iridx=iridx, ncount=ncount, stem=stem, transferable=transferable, temp=self.temp) verfers, digers = self.mgr.replay(pre=ipre, advance=False) else: # use defaults verfers, digers = self.mgr.incept(icount=icount, icode=icode, ncount=ncount, stem=stem, transferable=transferable, dcode=dcode, algo=algo, salt=salt, tier=tier, temp=self.temp) serder = super(Hab, self).make(isith=isith, verfers=verfers, nsith=nsith, digers=digers, code=code, toad=toad, wits=wits, estOnly=estOnly, DnD=DnD, delpre=delpre, data=data) self.pre = serder.ked["i"] # new pre opre = verfers[0].qb64 # default zeroth original pre from key store self.mgr.move(old=opre, new=self.pre) # move to incept event pre # may want db method that updates .habs. and .prefixes together habord = basing.HabitatRecord(hid=self.pre) if not hidden: self.save(habord) self.prefixes.add(self.pre) # sign handles group hab with .mhab case sigers = self.sign(ser=serder.raw, verfers=verfers) # during delegation initialization of a habitat we ignore the MissingDelegationError and # MissingSignatureError try: self.kvy.processEvent(serder=serder, sigers=sigers) except MissingSignatureError: pass except Exception as ex: raise kering.ConfigurationError("Improper Habitat inception for " "pre={} {}".format(self.pre, ex)) # read in self.cf config file and process any oobis or endpoints self.reconfigure() # should we do this for new Habs not loaded from db self.inited = True
@property def algo(self): pp = self.ks.prms.get(self.pre) return pp.algo
[docs] def rotate(self, *, isith=None, nsith=None, ncount=None, toad=None, cuts=None, adds=None, data=None, **kwargs): """ Perform rotation operation. Register rotation in database. Returns: bytearrayrotation message with attached signatures. Parameters: isith (int |str | None): current signing threshold as int or str hex or list of str weights default is prior next sith nsith (int |str | None): next, next signing threshold as int or str hex or list of str weights default is based on isith when None ncount (int | None): next number of signing keys default is len of prior next digs toad (int | str | None): hex of witness threshold after cuts and adds cuts (list | None): of qb64 pre of witnesses to be removed from witness list adds (list | None): of qb64 pre of witnesses to be added to witness list data (list | None): of dicts of committed data such as seals """ # recall that kever.pre == self.pre kever = self.kever # before rotation kever is prior next if ncount is None: ncount = len(kever.ndigers) # use len of prior next digers as default try: verfers, digers = self.mgr.replay(pre=self.pre) except IndexError: # old next is new current verfers, digers = self.mgr.rotate(pre=self.pre, ncount=ncount, temp=self.temp) return super(Hab, self).rotate(verfers=verfers, digers=digers, isith=isith, nsith=nsith, toad=toad, cuts=cuts, adds=adds, data=data)
[docs] class SignifyHab(BaseHab): """ Hab class provides a given idetnifier controller's local resource environment i.e. hab or habitat. Includes dependency injection of database, keystore, configuration file as well as Kevery and key store Manager.. """ def __init__(self, **kwa): super(SignifyHab, self).__init__(**kwa)
[docs] def make(self, *, serder, sigers, **kwargs): self.pre = serder.ked["i"] # new pre self.prefixes.add(self.pre) self.processEvent(serder, sigers) habord = basing.HabitatRecord(hid=self.pre, sid=self.pre) self.save(habord) self.inited = True
[docs] def sign(self, ser, verfers=None, indexed=True, indices=None, ondices=None, **kwa): """Sign given serialization ser using appropriate keys. Use provided verfers or .kever.verfers to lookup keys to sign. Parameters: ser (bytes): serialization to sign verfers (list[Verfer] | None): Verfer instances to get pub verifier keys to lookup private siging keys. verfers None means use .kever.verfers. Assumes that when group and verfers is not None then provided verfers must be .kever.verfers indexed (bool): When not mhab then True means use use indexed signatures and return list of Siger instances. False means do not use indexed signatures and return list of Cigar instances indices (list[int] | None): indices (offsets) when indexed == True. See Manager.sign ondices (list[int | None] | None): other indices (offsets) when indexed is True. See Manager.sign """ raise kering.KeriError("Signify hab does not support local signing")
[docs] def rotate(self, *, serder=None, sigers=None, **kwargs): """ Perform rotation operation. Register rotation in database. Returns: bytearrayrotation message with attached signatures. Parameters: serder (Serder): pre-created rotation event sigers (list[Siger]): Siger instances on next rotation event """ msg = eventing.messagize(serder, sigers=sigers) self.processEvent(serder, sigers) return msg
[docs] def interact(self, *, serder=None, sigers=None, **kwargs): """ Perform interaction operation. Register interaction in database. Returns: bytearray interaction message with attached signatures. """ msg = eventing.messagize(serder, sigers=sigers) self.processEvent(serder, sigers) return msg
[docs] def exchange(self, serder, seal=None, sigers=None, save=False): """ Returns signed exn, message of serder with count code and receipt couples (pre+cig) Builds msg and then processes it into own db to validate """ # sign serder event msg = eventing.messagize(serder=serder, sigers=sigers, seal=seal) if save: self.psr.parseOne(ims=bytearray(msg)) # process local copy into db return msg
[docs] def processEvent(self, serder, sigers): """ Process event with signatures raising any exception that occurs Performs event processing using local Kevery allowing raising all exceptions Args: serder (Serder): event serder to process sigers (list): list of Siger or Cigar instances representing signatures over serder.raw """ try: # verify event, update kever state, and escrow if group self.kvy.processEvent(serder=serder, sigers=sigers) except Exception: raise kering.ConfigurationError(f"Improper Habitat event type={serder.ked['t']} for " f"pre={self.pre}.")
[docs] def replyEndRole(self, cid, role=None, eids=None, scheme=""): """ Returns a reply message stream composed of entries authed by the given cid from the appropriate reply database including associated attachments in order to disseminate (percolate) BADA reply data authentication proofs. Currently uses promiscuous model for permitting endpoint discovery. Future is to use identity constraint graph to constrain discovery of whom by whom. cid and not role and not scheme then: end authz for all eids in all roles and loc url for all schemes at each eid if eids then only eids in eids else all eids cid and not role and scheme then: end authz for all eid in all roles and loc url for scheme at each eid if eids then only eids in eids else all eids cid and role and not scheme then: end authz for all eid in role and loc url for all schemes at each eid if eids then only eids in eids else all eids cid and role and scheme then: end authz for all eid in role and loc url for scheme at each eid if eids then only eids in eids else all eids Parameters: cid (str): identifier prefix qb64 of controller authZ endpoint provided eid is witness role (str): authorized role for eid eids (list): when provided restrict returns to only eids in eids scheme (str): url scheme """ msgs = bytearray() if eids is None: eids = [] # introduce yourself, please msgs.extend(self.replay(cid)) if role == kering.Roles.witness: if kever := self.kevers[cid] if cid in self.kevers else None: witness = self.pre in kever.wits # see if we are cid's witness # latest key state for cid for eid in kever.wits: if not eids or eid in eids: msgs.extend(self.loadLocScheme(eid=eid, scheme=scheme)) if not witness: # we are not witness, send auth records msgs.extend(self.makeEndRole(eid=eid, role=role)) if witness: # we are witness, set KEL as authz msgs.extend(self.replay(cid)) for (_, erole, eid), end in self.db.ends.getItemIter(keys=(cid,)): if (end.enabled or end.allowed) and (not role or role == erole) and (not eids or eid in eids): msgs.extend(self.replay(eid)) msgs.extend(self.loadLocScheme(eid=eid, scheme=scheme)) msgs.extend(self.loadEndRole(cid=cid, eid=eid, role=erole)) return msgs
[docs] class SignifyGroupHab(SignifyHab): def __init__(self, mhab=None, **kwa): self.mhab = mhab super(SignifyGroupHab, self).__init__(**kwa)
[docs] def make(self, *, serder, sigers, **kwargs): self.pre = serder.ked["i"] # new pre self.prefixes.add(self.pre) self.processEvent(serder, sigers) habord = basing.HabitatRecord(hid=self.pre, mid=self.mhab.pre, sid=self.pre) self.save(habord) self.inited = True
[docs] def processEvent(self, serder, sigers): """ Process event with signatures ignoring missing signature exceptions Performs event processing using local Kevery allowing missing signature exceptions to be ignored so multisig events can be created with a single local signature Args: serder (Serder): event serder to process sigers (list): list of Siger or Cigar instances representing signatures over serder.raw """ try: # verify event, update kever state, and escrow if group self.kvy.processEvent(serder=serder, sigers=sigers) except MissingSignatureError: pass except Exception: raise kering.ValidationError(f"Improper Habitat event type={serder.ked['t']} for " f"pre={self.pre}.")
[docs] class GroupHab(BaseHab): """ Hab class provides a given idetnifier controller's local resource environment i.e. hab or habitat. Includes dependency injection of database, keystore, configuration file as well as Kevery and key store Manager. Attributes: (Injected) ks (keeping.Keeper): lmdb key store db (basing.Baser): lmdb data base for KEL etc cf (configing.Configer): config file instance mgr (keeping.Manager): creates and rotates keys in key store rtr (routing.Router): routes reply 'rpy' messages rvy (routing.Revery): factory that processes reply 'rpy' messages kvy (eventing.Kevery): factory for local processing of local event msgs psr (parsing.Parser): parses local messages for .kvy .rvy Attributes: name (str): alias of controller pre (str): qb64 prefix of own local controller or None if new mhab (Hab | None): group member (local) hab when this Hab is multisig group else None smids (list | None): group signing member ids qb64 when this Hab is group else None rmids (list | None): group rotating member ids qb64 when this Hab is group else None temp (bool): True means testing: use weak level when salty algo for stretching in key creation for incept and rotate of keys for this hab.pre inited (bool): True means fully initialized wrt databases. False means not yet fully initialized delpre (str | None): delegator prefix if any else None Properties: kever (Kever): instance of key state of local controller kevers (dict): of eventing.Kever instances from KELs in local db keyed by qb64 prefix. Read through cache of of kevers of states for KELs in db.states iserder (serdering.SerderKERI): own inception event prefixes (OrderedSet): local prefixes for .db accepted (bool): True means accepted into local KEL. False otherwise """
[docs] def __init__(self, smids, mhab=None, rmids=None, **kwa): """ Initialize instance. Injected Parameters: (injected dependencies) ks (keeping.Keeper): lmdb key store db (basing.Baser): lmdb data base for KEL etc cf (configing.Configer): config file instance mgr (keeping.Manager): creates and rotates keys in key store rtr (routing.Router): routes reply 'rpy' messages rvy (routing.Revery): factory that processes reply 'rpy' messages kvy (eventing.Kevery): factory for local processing of local event msgs psr (parsing.Parser): parses local messages for .kvy .rvy Parameters: name (str): alias name for local controller of habitat pre (str | None): qb64 identifier prefix of own local controller else None mhab (Hab | None): group member hab (local) when this Hab is multisig group else None. The mhab.pre aid could be a member of .smids, or .rmids, or both. smids (list | None): group signing member ids (prefixes) when this Hab is multisig group else None. Set holds current signing authority for group multi-sig identifier. rmids (list | None): group rotation member ids (prefixes) when this Hab is multisig group else None. Set holds next rotating authority for group multi-sig identifier. When None defaults to copy of smids. temp (bool): True means testing: use weak level when salty algo for stretching in key creation for incept and rotate of keys for this hab.pre """ self.mhab = mhab # local participant Hab of this group hab self.smids = smids # group signing member aids in this group hab self.rmids = rmids # group rotating member aids in this group hab super(GroupHab, self).__init__(**kwa)
[docs] def make(self, *, code=coring.MtrDex.Blake3_256, transferable=True, isith=None, nsith=None, toad=None, wits=None, delpre=None, estOnly=False, DnD=False, merfers, migers=None, data=None): """ Finish setting up or making GroupHab from parameters includes inception. Assumes injected dependencies were already setup. Parameters: code (str): prefix derivation code default Blake3 transferable (bool): True means pre is transferable (default) False means pre is nontransferable isith (int | str | list | None): incepting signing threshold as int, str hex, or list weighted if any, otherwise compute default from verfers nsith (int, str, list | None ): next signing threshold as int, str hex or list weighted if any, otherwise compute default from digers toad (int |str| None): int or str hex of witness threshold if specified else compute default based on number of wits (backers) wits (list | None): qb64 prefixes of witnesses if any delpre (str | None): qb64 of delegator identifier prefix if any estOnly (bool | None): True means add trait eventing.TraitCodex.EstOnly which means only establishment events allowed in KEL for this Hab False (default) means allows non-est events and no trait is added. DnD (bool): True means add trait of eventing.TraitCodex.DnD which means do not allow delegated identifiers from this identifier False (default) means do allow and no trait is added. merfers (list[Verfer] | None): group member Verfer instances of public keys qb64 one collected from each multisig group member migers (list[Diger] | None): group member Diger instances of public next key digests qb64 one collected from each multisig group member data (list | None): seal dicts """ if not (self.ks.opened and self.db.opened and self.cf.opened): raise kering.ClosedError("Attempt to make Hab with unopened " "resources.") if nsith is None: nsith = isith if not transferable: nsith = '0' code = coring.MtrDex.Ed25519N verfers = merfers digers = migers serder = super(GroupHab, self).make(isith=isith, verfers=verfers, nsith=nsith, digers=digers, code=code, toad=toad, wits=wits, estOnly=estOnly, DnD=DnD, delpre=delpre, data=data) self.pre = serder.ked["i"] # new pre # sign handles group hab with .mhab case sigers = self.sign(ser=serder.raw, verfers=verfers) # during delegation initialization of a habitat we ignore the MissingDelegationError and # MissingSignatureError try: self.kvy.processEvent(serder=serder, sigers=sigers) except MissingSignatureError: pass except Exception as ex: raise kering.ConfigurationError("Improper Habitat inception for " "pre={} {}".format(self.pre, ex)) habord = basing.HabitatRecord(hid=self.pre, mid=self.mhab.pre, smids=self.smids, rmids=self.rmids) self.save(habord) self.prefixes.add(self.pre) self.inited = True
[docs] def rotate(self, serder=None, **kwargs): if serder is None: return super(GroupHab, self).rotate(**kwargs) # sign handles group hab with .mhab case sigers = self.sign(ser=serder.raw, verfers=serder.verfers, rotated=True) # update own key event verifier state msg = eventing.messagize(serder, sigers=sigers) try: self.kvy.processEvent(serder=serder, sigers=sigers) except MissingSignatureError: pass except Exception as ex: raise kering.ValidationError("Improper Habitat rotation for " "pre={self.pre}.") from ex return msg
[docs] def sign(self, ser, verfers=None, indexed=True, rotated=False, indices=None, ondices=None): """ Sign given serialization ser using appropriate keys. Walk .mhab's kel to find latest contribution of signing key material to group in order to sign properly. Contributions to group key material always use the zeroth element of signing key and/or rotating key digest lists. Find index into provided group verfers from current group key state. Parameters: ser (bytes): serialization to sign verfers (list[Verfer] | None): Verfer instances to get pub verifier keys to lookup private siging keys. verfers None means use .kever.verfers. Assumes that when group and verfers is not None then provided verfers must be .kever.verfers indexed (bool): When not mhab then True means use use indexed signatures and return list of Siger instances. False means do not use indexed signatures and return list of Cigar instances rotated (bool): When indexed and .mhab then True means use use dual indexed signatures, i.e. current indices and prior next ondices False means do not use dual indexed signatures, i.e. current siging indices only Otherwise ignore Assumes .kever.digers represent prior next indices (list[int] | None): indices (offsets) when indexed == True. See Manager.sign ondices (list[int | None] | None): other indices (offsets) when indexed is True. See Manager.sign """ if verfers is None: verfers = self.kever.verfers # when group these provide group signing keys # contributed member verfer from .mhab KEL. # Convention is to walk KEL to find correct contributed key if any. # Contributed keys MUSt always be zeroth element of member key list # and or member next key digests list. # first dig of mhab's prior nexter.digs. # walk member kel to find event if event where member contributed to # group est event from which verfers is taken if (result := self.mhab.kever.fetchLatestContribTo(verfers=verfers)) is None: raise ValueError(f"Member hab={self.mhab.pre} not a participant in " f"event for this group hab={self.pre}.") sn, csi, merfer = result # unpack result # the rotated flag may now be obsolete since fixing the Kever validation # logic to correctly chack both of the dual indices if rotated: # rotation so uses the other index from dual indices # Either the verfer key or both the verfer key and prior dig # might be participants in signature on group hab's rotation event. # Each prior dig must also be exposed as a participant # from current (after rotation) key list. # If mhab.kever.verfer[0] key is in group's new verfers (after rot) # then mhab participates in group as new key at index csi. # If in addition mhab prior dig at nexter.digs[0] is in group's # kever.digers (which will be prior next for group after rotation) # then mhab participates as group prior next at index pni. # else pni is None which means mhab only participates as new key. # get nexter of .mhab's prior Next est event migers = self.mhab.kever.fetchPriorDigers(sn=sn - 1) if migers: # not None or not empty mig = migers[0].qb64 # always use first prior dig of mhab digs = [diger.qb64 for diger in self.kever.ndigers] # group habs prior digs try: pni = digs.index(mig) # find mhab dig index in group hab digs except ValueError: # not found pni = None # default not participant else: pni = None # default not participant else: # not a rotation so ignores other index of dual index # pni = csi # backwards compatible is both same # in the future may want to fix Kever validation logic so that pni = None # should also work return (self.mhab.sign(ser=ser, verfers=[merfer], indexed=indexed, indices=[csi], ondices=[pni]))
[docs] def witness(self, serder): """ Group Habs are not valid witnesses thus can't provide witness receipts """ raise ValueError("Attempt to witness by group hab ={self.pre}.")
[docs] def query(self, pre, src, query=None, **kwa): """ Create, sign and return a `qry` message against the attester for the prefix Parameters: pre (str): qb64 identifier prefix being queried for src (str): qb64 identifier prefix of attester being queried query (dict): addition query modifiers to include in `q` **kwa (dict): keyword arguments passed to eventing.query Returns: bytearray: signed query event """ query = query if query is not None else dict() query['i'] = pre query["src"] = src serder = eventing.query(query=query, **kwa) return self.mhab.endorse(serder, last=True)
[docs] def witnesser(self): """This method name does not match logic??? """ kever = self.kever keys = [verfer.qb64 for verfer in kever.verfers] sigs = self.db.getSigs(dbing.dgKey(self.pre, kever.serder.saidb)) if not sigs: # otherwise its a list of sigs return False sigers = [coring.Siger(qb64b=bytes(sig)) for sig in sigs] windex = min([siger.index for siger in sigers]) # True if Elected to perform delegation and witnessing return self.mhab.kever.verfers[0].qb64 == keys[windex]