# -*- 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 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
@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)
@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]