# -*- encoding: utf-8 -*-
"""
KERI
keri.app.habbing module
"""
from contextlib import contextmanager
from math import ceil
from urllib.parse import urlsplit
from hio.base import doing
from hio.help import hicting, ogler
from .configing import Configer
from .keeping import Keeper, Manager
from ..peer import Exchanger, specialExchange
from ..db import Baser, dgKey, fetchTsgs
from ..help import fromIso8601, toIso8601
from ..kering import (Version, Vrsn_1_0, Vrsn_2_0, Ilks, Kinds, Roles, Schemes,
ClosedError, AuthError, ConfigurationError, KeriError,
ValidationError, MissingEntryError, MissingSignatureError)
from ..core import (Tholder, Diger, Prefixer, Kevery, Parser, Revery,
Router, Counter, Salter, SealEvent, SealSource, SealLast,
Codens, MtrDex, TraitDex, messagize, exchange,)
from ..core import eventing
from ..recording import HabitatRecord, OobiRecord
logger = ogler.getLogger()
[docs]
@contextmanager
def openHby(*, name="test", base="", temp=True, salt=None, **kwa):
"""Context manager that creates and yields a ``Habery`` instance, closing
and optionally clearing it on exit.
Parameters::
name (str): Name used for the shared databases and config file path.
base (str): Optional path component inserted before ``name`` for
further hierarchical differentiation of databases. Empty string
means no additional component.
temp (bool): When ``True``, stores ``.ks``, ``.db``, and ``.cf`` in
``/tmp`` and uses a fast (low-cost) salt-stretch method suitable
for testing. When ``False``, uses the resource tier specified by
``tier``.
salt (str): qb64-encoded salt used for key-pair creation. A fresh
random salt is generated when ``None``.
**kwa: Additional keyword arguments forwarded to ``Habery.__init__``.
See ``Habery`` for the full list (``seed``, ``aeid``, ``bran``,
``pidx``, ``algo``, ``tier``, ``free``, ``version``).
Yields::
Habery: Fully initialised ``Habery`` instance.
"""
habery = None
salt = salt if salt is not None else Salter().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=None, temp=True, cf=None, **kwa):
"""Context manager that creates and yields a ``(Habery, Hab)`` pair,
closing and optionally clearing resources on exit.
If a ``Hab`` with ``name`` already exists in the ``Habery`` it is reused;
otherwise a new single-key ``Hab`` (``icount=1, isith='1', ncount=1,
nsith='1'``) is created via ``Habery.makeHab``.
Parameters::
name (str): Name of the ``Hab`` (and the underlying shared databases).
base (str): Optional path component for shared resources. See
``openHby``.
salt (bytes | None): Raw (not qb64) salt bytes passed to ``Habery``.
Converted to qb64 internally. A fresh salt is generated when
``None``.
temp (bool): ``True`` means use temporary databases. See ``openHby``.
cf (Configer | None): Optional ``Configer`` instance for loading
configuration data.
**kwa: Additional keyword arguments forwarded to ``Habery.makeHab``.
When ``version`` is provided, it is also used for the shared
``Habery`` parser.
Yields::
tuple[Habery, Hab]: The shared ``Habery`` environment and the named
``Hab`` instance.
"""
salt = Salter(raw=salt).qb64
with openHby(name=name, base=base, salt=salt, temp=temp, cf=cf,
version=kwa.get("version", Version)) as hby:
if (hab := hby.habByName(name)) is None:
hab = hby.makeHab(name=name, icount=1, isith='1', ncount=1, nsith='1', cf=cf, **kwa)
yield hby, hab
[docs]
class Habery:
"""Shared environment for a collection of ``Hab`` (Habitat) instances.
Provides a single keystore (``Keeper``), event database (``Baser``), and
config file (``Configer``) that are shared among all ``Hab`` instances
created within this environment. Also owns the ``Manager``, ``Router``,
``Revery``, ``Kevery``, and ``Parser`` used for key management and event
processing.
Attributes:
name (str): Name used for the associated databases and config file.
base (str): Optional directory path segment inserted before ``name``
for hierarchical differentiation of databases. Empty string means
no additional component.
temp (bool): ``True`` means temporary storage and fast (test-suitable)
salt stretching. ``False`` means persistent storage and
tier-appropriate key stretching.
ks (Keeper): LMDB keystore instance.
db (Baser): LMDB event database instance for KELs etc.
cf (Configer): Config file instance.
mgr (Manager or None): Key manager for creating and rotating keys.
``None`` until ``setup`` completes successfully.
rtr (Router): Routes ``rpy`` (reply) messages to registered handlers.
rvy (Revery): Processes ``rpy`` messages.
exc (Exchanger): Processes ``exn`` (exchange) messages.
kvy (Kevery): Processes local key-event messages.
psr (Parser): Parses framed local messages, dispatching to ``kvy``,
``rvy``, and ``exc``.
habs (dict): ``Hab`` instances keyed by their qb64 prefix.
Use ``habByName`` to look up by name and ``habByPre`` to look up
by prefix.
inited (bool): ``True`` once ``setup`` has completed successfully.
"""
[docs]
def __init__(self, *, name='test', base="", temp=False,
ks=None, db=None, cf=None, clear=False, headDirPath=None,
version=Version, **kwa):
"""Initialise a ``Habery`` instance.
Opens (or reuses) the keystore, event database, and config file, then
calls ``setup`` if both ``db`` and ``ks`` are already open. When
dependency-injected stores are not yet open (e.g. in an async context),
``setup`` must be called explicitly once they have been opened.
Args:
name (str): Alias name for the shared environment, databases, and
config file.
base (str): Optional directory path segment inserted before
``name``. Empty string means no additional component.
temp (bool): ``True`` means use temporary storage in ``/tmp`` and
fast salt-stretch methods suitable for testing.
ks (Keeper | None): Existing open keystore to reuse. A new
``Keeper`` is created when ``None``.
db (Baser | None): Existing open event database to reuse. A new
``Baser`` is created when ``None``.
cf (Configer | None): Existing config file instance to reuse. A
new ``Configer`` is created when ``None``.
clear (bool): When ``True``, removes the resource directory on
``close``.
headDirPath (str | None): Override for the top-level directory path
used when creating ``ks`` and ``db``.
version (Versionage): Parser attachment code table version.
**kwa: Keyword arguments forwarded to ``setup`` and stored in
``_inits`` for deferred initialisation. See ``setup`` for the
full parameter list (``seed``, ``aeid``, ``bran``, ``pidx``,
``algo``, ``salt``, ``tier``, ``free``).
"""
self.name = name
self.base = base
self.temp = temp
self.ks = ks if ks is not None else 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 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 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 = Router()
self.rvy = Revery(db=self.db, rtr=self.rtr)
self.exc = Exchanger(hby=self, handlers=[])
self.kvy = Kevery(db=self.db, lax=False, local=True, rvy=self.rvy)
self.kvy.registerReplyRoutes(router=self.rtr)
self.psr = Parser(framed=True, kvy=self.kvy, rvy=self.rvy,
exc=self.exc, local=True, version=version)
self.habs = {} # empty .habs
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, ):
"""Finish initialisation of the ``Habery`` after ``db`` and ``ks`` are open.
Intended to be called once both ``.db`` and ``.ks`` have been opened.
This separation allows dependency injection of database instances that
may be opened asynchronously after ``__init__``. The first successful
call performs vacuous (initial) database setup.
Args:
seed (str | None): qb64 private signing key (seed) for the
``aeid``. Used to derive the private decryption key and to
authenticate the ``Manager``. This value is **memory-only**
and must never be persisted to the database. It must be loaded
exactly once when the process starts and stored on a separate
device from the one running the ``Manager``.
aeid (str | None): qb64 non-transferable identifier prefix used for
authentication and encryption of secrets in the keystore. When
provided and different from the ``aeid`` already in the
database, all secrets are re-encrypted under the new ``aeid``;
in this case ``seed`` must also be provided. An ``aeid``
change should require a second authentication factor in
addition to ``seed``.
bran (str | None): Base64 string of at least 21 characters used as
base material to derive ``seed`` via salt stretching. Allows
alphanumeric passcodes (e.g. from a password manager) to serve
as the key-store secret. Ignored when ``seed`` is provided.
pidx (int | None): Initial prefix index for a vacuous (empty)
keystore.
algo (str | None): Key-pair generation algorithm (``'randy'`` or
``'salty'``). Defaults to the root algorithm (salty) when
``None``.
salt (str | None): qb64 salt used for signing-key creation. A
fresh random salt is generated when ``None``.
tier (str | None): Security tier (``Tierage``) controlling the
cost of salt-to-seed stretching.
free (bool): When ``True``, releases (closes) resources when the
associated ``Doer`` exits.
temp (bool | None): When ``True``, overrides the instance
``temp`` flag to use fast salt-stretch methods during setup.
Raises:
ClosedError: If ``.ks`` or ``.db`` is not open when called.
ValueError: If ``bran`` is shorter than 21 characters.
AuthError: If the provided ``seed`` does not authenticate the
``aeid`` stored in the keystore.
"""
if not (self.ks.opened and self.db.opened):
raise 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 = MtrDex.Salt_128 + 'A' + bran[:21] # qb64 salt for seed
signer = 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 = Salter().qb64
else:
salt = Salter(qb64=salt).qb64
try:
self.mgr = Manager(ks=self.ks, seed=seed, aeid=aeid, pidx=pidx,
algo=algo, salt=salt, tier=tier)
except 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 all ``Hab`` instances persisted in the database into ``.habs``.
Called by ``setup`` after the keystore and event database are open and
``db.reload`` has already populated ``db.kevers`` and ``db.prefixes``
from stored key state, removing any orphaned hab records without valid
key state.
Each record in ``db.habs`` is inspected to determine the correct ``Hab``
subclass:
* ``GroupHab`` — ``mid`` set, ``sid`` not set (local multisig group).
* ``SignifyHab`` — ``sid`` set, ``mid`` not set (Signify-managed
single identifier).
* ``SignifyGroupHab`` — both ``sid`` and ``mid`` set (Signify-managed
multisig group).
* ``Hab`` — neither ``sid`` nor ``mid`` set (standard local
identifier).
After all habs are loaded, group habs have their ``.mhab`` (local
member hab) populated from ``.habs``. ``reconfigure`` is called both
before and after loading.
Raises:
ConfigurationError: If a non-group ``Hab`` loaded from the database
has not been accepted into its own local KEL.
"""
self.reconfigure() # pre hab load reconfiguration
groups = []
for prefix, habord in self.db.habs.getTopItemIter():
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=habord.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=habord.name, pre=habord.sid)
elif habord.sid and habord.mid:
hab = SignifyGroupHab(smids=habord.smids, 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=habord.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=habord.name, pre=pre, temp=self.temp)
# Rules for acceptance:
# It is accepted into its own local KEL even if it has not been fully
# witnessed and if delegated, its delegator has not yet sealed it
if not hab.accepted and not habord.mid:
raise ConfigurationError(f"Problem loading Hab pre="
f"{pre} name={habord.name} from db.")
# read in config file and process any oobis or endpoints for hab
hab.inited = True
self.habs[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):
"""Create, persist, and return a new local ``Hab``.
The new ``Hab`` is registered in ``.habs`` keyed by its generated
prefix.
Args:
name (str): Human-readable alias for the new identifier.
ns (str | None): Optional namespace for the identifier. Must not
contain a ``'.'`` character.
cf (Configer | None): Config file instance to use for this hab.
Defaults to ``self.cf`` when ``None``.
**kwa: Keyword arguments forwarded to ``Hab.make``:
* ``secrecies`` (list): Pre-loaded key-pair secrets.
* ``iridx`` (int): Initial rotation index after secret ingestion.
* ``code`` (str): Prefix derivation code.
* ``transferable`` (bool): ``True`` (default) for a
transferable prefix; ``False`` for non-transferable.
* ``isith`` (int | str | list): Inception signing threshold.
* ``icount`` (int): Number of inception signing keys.
* ``nsith`` (int | str | list): Next signing threshold.
* ``ncount`` (int): Number of next (pre-rotated) keys.
* ``toad`` (int | str): Witness threshold.
* ``wits`` (list[str]): qb64 witness prefixes.
* ``delpre`` (str): qb64 delegator prefix.
* ``estOnly`` (str): ``TraitDex.EstOnly`` to restrict the KEL
to establishment events only.
* ``data`` (list | None): Seal dicts for the inception event.
Returns:
Hab: The newly created and persisted ``Hab`` instance.
Raises:
ConfigurationError: If ``ns`` contains a ``'.'`` character.
"""
if ns is not None and "." in ns:
raise 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.incept(**kwa)
self.habs[hab.pre] = hab
return hab
[docs]
def makeGroupHab(self, group, mhab, smids, rmids=None, ns=None, **kwa):
"""Create, persist, and return a new multisig ``GroupHab``.
The KEL for each signing and rotation member must already be present in
``.kevers`` before calling this method. Current signing keys are
extracted from each member's latest establishment event to form the
group's inception keys (``merfers``), and next key digests form the
group's next key commitments (``migers``).
Args:
group (str): Human-readable alias for the group identifier.
mhab (Hab): The local participant ``Hab`` that is a member of this
group.
smids (list[str]): qb64 prefixes of the signing members. Each
must have a KEL in ``.kevers`` and exactly one current signing
key.
rmids (list[str] | None): qb64 prefixes of the rotation members
from which next key digests are extracted. When ``None``,
``smids`` is used for both signing and rotation. An empty list
produces a group identifier with no next key commitments
(non-transferable after inception).
ns (str | None): Optional namespace for the group identifier.
**kwa: Keyword arguments forwarded to ``GroupHab.make``. See
``makeHab`` for the full list; additionally:
* ``DnD`` (bool): ``TraitDex.DnD`` to disallow delegated
identifiers from this identifier.
Returns:
GroupHab: The newly created and persisted ``GroupHab`` instance.
Raises:
ConfigurationError: If ``mhab.pre`` is not present in either
``smids`` or ``rmids``, if a signing member's KEL is missing
from ``.kevers``, if a rotation member's KEL is missing from
``.kevers``, or if any member has more than one current signing
key or more than one next key digest.
"""
if mhab.pre not in smids and mhab.pre not in rmids:
raise 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 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 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.incept(**kwa) # finish making group hab with injected pass throughs
self.habs[hab.pre] = hab
return hab
[docs]
def joinGroupHab(self, pre, group, mhab, smids, rmids=None, ns=None):
"""Join an existing multisig group as a participant without creating a
new inception event.
Used when the group's inception event was initiated by another
participant and this node is joining after the fact. The group
``Hab`` is constructed, its prefix set to ``pre``, and the record
persisted directly rather than through ``Hab.make``.
Args:
pre (str): qb64 prefix of the already-established group identifier.
group (str): Human-readable alias for the group identifier.
mhab (Hab): The local participant ``Hab`` that is a member of this
group.
smids (list[str]): qb64 prefixes of the signing members.
rmids (list[str] | None): qb64 prefixes of the rotation members.
When ``None``, ``smids`` is used for both. An empty list means
the group is non-transferable after inception.
ns (str | None): Optional namespace for the group identifier.
Returns:
GroupHab: The newly created and persisted ``GroupHab`` instance
with ``.pre`` set to ``pre``.
Raises:
ConfigurationError: If ``mhab.pre`` is not in ``smids`` or
``rmids``, or if a member's KEL is missing from ``.kevers``.
"""
if mhab.pre not in smids and mhab.pre not in rmids:
raise 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 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 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 = HabitatRecord(hid=hab.pre,
name=hab.name,
domain=ns,
mid=mhab.pre,
smids=smids,
rmids=rmids)
hab.save(habord)
hab.prefixes.add(pre)
hab.inited = True
self.habs[hab.pre] = hab
return hab
[docs]
def makeSignifyHab(self, name, ns=None, **kwa):
"""Create, persist, and return a new ``SignifyHab`` (Signify-managed
single identifier).
Args:
name (str): Human-readable alias for the identifier.
ns (str | None): Optional namespace for the identifier.
**kwa: Keyword arguments forwarded to ``SignifyHab.make``.
Returns:
SignifyHab: The newly created and persisted ``SignifyHab`` instance.
"""
# 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.incept(**kwa) # finish making group hab with injected pass throughs
self.habs[hab.pre] = hab
return hab
[docs]
def makeSignifyGroupHab(self, name, mhab, smids, rmids=None, ns=None, **kwa):
"""Create, persist, and return a new ``SignifyGroupHab`` (Signify-managed
multisig group identifier).
Args:
name (str): Human-readable alias for the group identifier.
mhab (Hab): The local participant ``Hab`` that is a member of this
group.
smids (list[str]): qb64 prefixes of the signing members.
rmids (list[str] | None): qb64 prefixes of the rotation members.
When ``None``, ``smids`` is used for both.
ns (str | None): Optional namespace for the group identifier.
**kwa: Keyword arguments forwarded to ``SignifyGroupHab.make``.
Returns:
SignifyGroupHab: The newly created and persisted
``SignifyGroupHab`` instance.
"""
# 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, smids=smids, rmids=rmids, ns=ns, temp=self.temp)
hab.incept(**kwa) # finish making group hab with injected pass throughs
self.habs[hab.pre] = hab
return hab
[docs]
def joinSignifyGroupHab(self, pre, name, mhab, smids, rmids=None, ns=None):
"""Join an existing Signify-managed multisig group as a participant
without creating a new inception event.
Analogous to ``joinGroupHab`` but for ``SignifyGroupHab`` instances.
The group ``Hab`` is constructed with the given ``pre``, and the
record is persisted directly.
Args:
pre (str): qb64 prefix of the already-established group identifier.
name (str): Human-readable alias for the group identifier.
mhab (Hab): The local participant ``Hab`` that is a member of this
group.
smids (list[str]): qb64 prefixes of the signing members.
rmids (list[str] | None): qb64 prefixes of the rotation members.
When ``None``, ``smids`` is used for both. An empty list means
the group is non-transferable after inception.
ns (str | None): Optional namespace for the group identifier.
Returns:
SignifyGroupHab: The newly created and persisted
``SignifyGroupHab`` instance with ``.pre`` set to ``pre``.
Raises:
ConfigurationError: If ``mhab.pre`` is not in ``smids`` or
``rmids``, or if a member's KEL is missing from ``.kevers``.
"""
if mhab.pre not in smids and mhab.pre not in rmids:
raise 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 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 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, smids=smids, rmids=rmids, ns=ns, temp=self.temp)
hab.pre = pre
habord = HabitatRecord(hid=hab.pre,
sid=mhab.pre,
name=name,
domain=ns,
smids=smids,
rmids=rmids)
hab.save(habord)
hab.prefixes.add(pre)
hab.inited = True
self.habs[hab.pre] = hab
return hab
[docs]
def deleteHab(self, name, ns=None):
"""Remove a ``Hab`` from the database and from ``.habs``.
Also removes the name-to-prefix mapping, the prefix from
``db.prefixes``, and (if present) the entry from ``db.groups``.
Args:
name (str): Human-readable alias of the ``Hab`` to delete.
ns (str | None): Namespace of the ``Hab``. Defaults to ``""``
when ``None``.
Returns:
bool: ``True`` if the ``Hab`` was found and successfully removed;
``False`` if it was not found or if either database removal
failed.
"""
hab = self.habByName(name, ns=ns)
if not hab:
return False
if not self.db.habs.rem(keys=(hab.pre,)):
return False
ns = "" if ns is None else ns
if not self.db.names.rem(keys=(ns, name)):
return False
del self.habs[hab.pre]
self.db.prefixes.remove(hab.pre)
if hab.pre in self.db.groups:
self.db.groups.remove(hab.pre)
return True
[docs]
def close(self, clear=False):
"""Close all managed resources (keystore, database, config file).
Args:
clear (bool): When ``True``, remove the resource directories in
addition to closing them. Temporary resources (``temp=True``)
are always cleared regardless of this flag.
"""
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):
"""dict: All ``Kever`` instances from ``db.kevers``, keyed by qb64 prefix."""
return self.db.kevers
@property
def prefixes(self):
"""OrderedSet: Local prefixes registered in ``db.prefixes``."""
return self.db.prefixes
[docs]
def habByPre(self, pre):
"""Return the ``Hab`` instance for a given prefix, or ``None``.
Args:
pre (str): qb64 AID prefix to look up.
Returns:
Hab | None: The ``Hab`` registered under ``pre``, or ``None`` if
not found.
"""
if pre in self.habs:
return self.habs[pre]
return None
[docs]
def habByName(self, name, ns=None):
"""Return the ``Hab`` instance for a given name and optional namespace,
or ``None``.
Resolves the name to a prefix via ``db.names`` and then looks up the
``Hab`` in ``.habs``.
Args:
name (str): Human-readable alias of the ``Hab``.
ns (str | None): Namespace of the ``Hab``. Defaults to ``""``
when ``None``.
Returns:
Hab | None: The matching ``Hab`` instance, or ``None`` if not
found.
"""
ns = "" if ns is None else ns
if (pre := self.db.names.get(keys=(ns, name))) is not None:
if pre in self.habs:
return self.habs[pre]
return None
@property
def signator(self):
"""Signator: Signer and verifier for data-at-rest in this ``Habery``
environment. ``None`` until ``setup`` completes successfully.
"""
return self._signator
SIGNER = "__signatory__"
[docs]
class Signator:
"""Manages a non-transferable identifier used to sign and verify data at rest.
Creates a single non-transferable AID on first initialization and persists it
in the Habery database. Subsequent instantiations with the same name rehydrate
the existing AID. Intended for signing BADA data to ensure integrity at rest.
Attributes:
db (Baser): Database environment used for key state and prefix storage.
pre (str): Qualified Base64 AID prefix for the signing identifier.
"""
[docs]
def __init__(self, db, name=SIGNER, **kwa):
"""Initializes the Signator, creating a new signing AID if none exists for name.
Looks up name in the Habery prefix index. If absent, creates a new
non-transferable, hidden Hab and pins its prefix. If present, rehydrates
the existing Hab from the stored prefix.
Args:
db (Baser): Database environment for key state and AID storage.
name (str): Label used to look up or register the signing AID.
Defaults to SIGNER.
**kwa: Additional keyword arguments forwarded to Hab.
"""
self.db = db
spre = self.db.hbys.get(name)
if not spre:
self._hab = Hab(name=name, db=db, **kwa)
self._hab.incept(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):
"""Signs raw bytes using the Signator's non-transferable private key.
Delegates to the underlying Hab's sign method with indexed=False,
returning the first (and only) Cigar signature object.
Args:
ser (bytes): Raw byte data to sign.
Returns:
Cigar: Non-indexed signature over ser using the current verfer's
private key.
"""
return self._hab.sign(ser, indexed=False)[0]
[docs]
def verify(self, ser, cigar):
"""Verifies a Cigar signature against raw bytes using the current verfer.
Checks the raw signature in cigar against ser using the first verfer
on the Signator's current key event state (kever).
Args:
ser (bytes): Raw byte data to verify against the signature.
cigar (Cigar): Non-transferable signature to verify.
Returns:
bool: True if the signature is cryptographically valid for ser,
False otherwise.
"""
return self._hab.kever.verfers[0].verify(cigar.raw, ser)
[docs]
class HaberyDoer(doing.Doer):
"""Doer subclass that manages Habery lifecycle within a coroutine context.
Initializes the Habery on enter if not already inited, and closes it on
exit if inited and free. Intended to drive Habery setup and teardown as
part of a Doist-managed task graph.
Attributes:
habery (Habery): Habery instance whose lifecycle this doer manages.
Inherited Attributes:
done (bool): Completion state. ``True`` means finished normally;
``False`` indicates incomplete due to close or abort.
tyme (float): Relative cycle time obtained from the injected
``tymth`` closure.
tymth (callable): Injected closure returned by ``Tymist.tymeth()``.
Call it to get the current ``Tymist.tyme``. Injected via
``wind()``.
tock (float): Desired seconds between ``recur`` calls. Zero means run
ASAP. Non-negative.
Note:
Implements the Doer coroutine protocol: ``enter``, ``recur``,
``exit``, ``close``, and ``abort``. See ``doing.Doer`` for the full
interface.
"""
[docs]
def __init__(self, habery, **kwa):
"""Initializes HaberyDoer with the Habery instance to manage.
Args:
habery (Habery): Habery instance to initialize and close during
the doer lifecycle.
**kwa: Additional keyword arguments forwarded to Doer.__init__.
"""
super(HaberyDoer, self).__init__(**kwa)
self.habery = habery
[docs]
def enter(self, *, temp=None):
"""Enters the doer context and initializes Habery if not already inited.
Calls habery.setup() with its stored _inits parameters only when
habery.inited is False. No-ops if Habery is already initialized.
Args:
temp (bool | None): Unused in this implementation. Present for
interface compatibility with the base Doer enter signature.
"""
if not self.habery.inited:
self.habery.setup(**self.habery._inits)
[docs]
def exit(self):
"""Exits the doer context and closes Habery if inited and free.
Calls habery.close() with clear set to habery.temp, which causes
database files to be removed when operating in temporary mode.
No-ops if Habery is not inited or not free.
"""
if self.habery.inited and self.habery.free:
self.habery.close(clear=self.habery.temp)
[docs]
class BaseHab:
"""Hab class provides a given identifier 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:
ks (Keeper): Injected. lmdb key store.
db (basing.Baser): Injected. lmdb data base for KEL etc.
cf (Configer): Injected. Config file instance.
mgr (Manager): Injected. Creates and rotates keys in key store.
rtr (Router): Injected. Routes reply ``rpy`` messages.
rvy (Revery): Injected. Factory that processes reply ``rpy`` messages.
kvy (Kevery): Injected. Factory for local processing of local event msgs.
psr (Parser): Injected. Parses local messages for ``.kvy`` and ``.rvy``.
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 or None): Delegator prefix if any, else None.
"""
[docs]
def __init__(self, ks, db, cf, mgr, rtr, rvy, kvy, psr, *,
name='test', ns=None, pre=None, temp=False):
"""Initialize instance.
Args:
ks (Keeper): lmdb key store.
db (basing.Baser): lmdb data base for KEL etc.
cf (Configer): config file instance.
mgr (Manager): creates and rotates keys in key store.
rtr (Router): routes reply ``rpy`` messages.
rvy (Revery): factory that processes reply ``rpy`` messages.
kvy (Kevery): factory for local processing of local event msgs.
psr (Parser): parses local messages for ``.kvy`` and ``.rvy``.
name (str): alias name for local controller of habitat.
pre (str or 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 # what is this?
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 incept(self, DnD, code, data, delpre, estOnly, isith, verfers, nsith,
digers, toad, wits, kind=Kinds.json, version=Version):
"""Creates Serder of inception event for provided parameters.
Assumes injected dependencies were already setup.
Args:
DnD (bool): True means add trait ``TraitDex.DnD`` which means do
not allow delegated identifiers from this identifier. False
(default) means do allow, and no trait is added.
code (str): prefix derivation code, default Blake3.
data (list or None): seal dicts.
delpre (str or None): qb64 of delegator identifier prefix if any.
estOnly (bool or None): True means add trait ``TraitDex.EstOnly``
which means only establishment events are allowed in the KEL
for this Hab. False (default) means allow non-est events and
no trait is added.
isith (int, str, list, or None): incepting signing threshold as
int, str hex, or list weighted if any, otherwise compute
default from verfers.
verfers (list[Verfer]): Verfer instances for initial signing keys.
nsith (int, str, list, or None): next signing threshold as int,
str hex, or list weighted if any, otherwise compute default
from digers.
digers (list[Diger] or None): Diger instances for next key digests.
toad (int, str, or None): int or str hex of witness threshold if
specified, else compute default based on number of wits
(backers).
wits (list or None): qb64 prefixes of witnesses if any.
kind (str): serialization for key event message
one of Kinds ("json","cbor","mgpk","cesr")
version (Versionage): version for key event message
Returns:
Serder: inception event serder.
"""
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 = Tholder(sith=isith).sith # current signing threshold
nst = Tholder(sith=nsith).sith # next signing threshold
cnfg = []
if estOnly:
cnfg.append(TraitDex.EstOnly)
if DnD:
cnfg.append(TraitDex.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,
kind=kind,
version=version)
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,
kind=kind,
version=version )
return serder
[docs]
def make(self, **kwa):
"""Alias for ``.incept``.
Args:
**kwa: keyword arguments forwarded to :meth:`incept`.
"""
self.incept(**kwa)
def save(self, habord):
self.db.habs.pin(keys=self.pre,
val=habord)
ns = "" if self.ns is None else self.ns
if self.db.names.get(keys=(ns, self.name)) is not None:
raise ValueError("AID already exists with that name")
self.db.names.pin(keys=(ns, self.name),
val=self.pre)
@property
def iserder(self):
"""Return serder of inception event.
Returns:
SerderKERI: own inception event serder.
Raises:
ConfigurationError: if inception event is missing from the KEL or
the event store.
"""
if (dig := self.db.kels.getLast(keys=self.pre, on=0)) is None:
raise ConfigurationError("Missing inception event in KEL for "
"Habitat pre={}.".format(self.pre))
dig = dig.encode("utf-8")
if (serder := self.db.evts.get(keys=(self.pre, bytes(dig)))) is None:
raise ConfigurationError("Missing inception event for "
"Habitat pre={}.".format(self.pre))
return serder
@property
def kevers(self):
"""Returns ``.db.kevers``.
Returns:
dict: mapping of qb64 prefix to Kever instances.
"""
return self.db.kevers
@property
def accepted(self):
"""True if own prefix has been accepted into the local KEL.
Returns:
bool: True if ``.pre`` is in ``.kevers``, False otherwise.
"""
return self.pre in self.kevers
@property
def kever(self):
"""Returns kever for own ``.pre``.
Returns:
Kever or None: Kever instance if accepted, else None.
"""
return self.kevers[self.pre] if self.accepted else None
@property
def prefixes(self):
"""Returns ``.db.prefixes``.
Returns:
OrderedSet: local prefixes for ``.db``.
"""
return self.db.prefixes
[docs]
def rotate(self, *, verfers=None, digers=None, isith=None, nsith=None,
toad=None, cuts=None, adds=None, data=None,
kind=Kinds.json, version=Version, framed=False,
nested=False, gvrsn=Version, genusify=False):
"""Perform rotation operation. Register rotation in database.
Parameters::
verfers (list or None): Verfer instances of public keys qb64.
digers (list or None): Diger instances of public next key digests
qb64.
isith (int, str, or None): current signing threshold as int, str
hex, or list of str weights. Default is prior next sith.
nsith (int, str, or None): next signing threshold as int, str hex,
or list of str weights. Default is based on isith when None.
toad (int or str or None): hex of witness threshold after cuts and
adds.
cuts (list or None): qb64 prefixes of witnesses to be removed from
the witness list.
adds (list or None): qb64 prefixes of witnesses to be added to the
witness list.
data (list or None): dicts of committed data such as seals.
kind (str): serialization for key event message
one of Kinds ("json","cbor","mgpk","cesr")
version (Versionage): version for key event message
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
Returns::
bytearray: rotation message with attached signatures.
Raises::
ValidationError: if the new key set cannot satisfy the prior next
signing threshold, or if the rotation event is otherwise
improper.
"""
# 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 toad is None and not cuts and not adds:
toad = kever.toader.num # preserve prior toad when no witness changes
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 = Tholder(sith=isith).sith # current signing threshold
nst = Tholder(sith=nsith).sith # next signing threshold
keys = [verfer.qb64 for verfer in verfers]
indices = []
for idx, diger in enumerate(kever.ndigers):
pdigs = [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 ValidationError("invalid rotation, new key set unable to satisfy prior next signing threshold")
if kever.delpre 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,
kind=kind,
version=version)
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,
kind=kind,
version=version)
# 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, framed=framed, nested=nested,
gvrsn=gvrsn, genusify=genusify)
try:
self.kvy.processEvent(serder=serder, sigers=sigers)
except MissingSignatureError:
pass
except Exception as ex:
raise ValidationError("Improper Habitat rotation for "
"pre={self.pre}.") from ex
return msg
[docs]
def interact(self, *, data=None, kind=Kinds.json, version=Version,
framed=False, nested=False, gvrsn=Version, genusify=False):
"""Perform interaction operation. Register interaction in database.
Parameters::
data (list or None): dicts of committed data such as seals.
kind (str): serialization for key event message
one of Kinds ("json","cbor","mgpk","cesr")
version (Versionage): version for key event message
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
Returns::
bytearray: interaction message with attached signatures.
Raises::
ValidationError: if the interaction event is improper.
"""
kever = self.kever
serder = eventing.interact(pre=kever.prefixer.qb64,
dig=kever.serder.said,
sn=kever.sner.num + 1,
data=data,
kind=kind,
version=version)
sigers = self.sign(ser=serder.raw)
msg = eventing.messagize(serder, sigers=sigers, framed=framed,
nested=nested, gvrsn=gvrsn, genusify=genusify)
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 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.
Uses provided verfers or ``.kever.verfers`` to look up keys to sign.
Args:
ser (bytes): serialization to sign.
verfers (list[Verfer] or None): Verfer instances to get public
verifier keys to look up private signing keys. None means use
``.kever.verfers``. When group and verfers is not None, the
provided verfers must be ``.kever.verfers``.
indexed (bool): when not mhab, True means use indexed signatures
and return a list of Siger instances. False means do not use
indexed signatures and return a list of Cigar instances.
indices (list[int] or None): indices (offsets) when
``indexed`` is True. See ``Manager.sign``.
ondices (list[int or None] or None): other indices (offsets) when
``indexed`` is True. See ``Manager.sign``.
Returns:
list[Siger] or list[Cigar]: signed instances depending on
``indexed``.
"""
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):
"""Decrypt given serialization ``ser`` using appropriate keys.
Uses provided verfers or ``.kever.verfers`` to look up keys to decrypt.
Args:
ser (str, bytes, bytearray, or memoryview): serialization to
decrypt.
verfers (list[Verfer] or None): Verfer instances to get public
verifier keys to look up and convert to private decryption
keys. None means use ``.kever.verfers``. When group and
verfers is not None, the provided verfers must be
``.kever.verfers``.
Returns:
bytes: decrypted serialization.
"""
if verfers is None:
verfers = self.kever.verfers # when group these provide group signing keys
# should not use mgr.decrypt since it assumes qb64. Just lucky its not
# yet a problem
return self.mgr.decrypt(qb64=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.
Args:
pre (str): qb64 identifier prefix being queried for.
src (str): qb64 identifier prefix of attester being queried.
query (dict or None): additional query modifiers to include in
``q``.
**kwa: 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(pre=self.pre, query=query, **kwa)
return self.endorse(serder, last=True, framed=False) # was framed=False
[docs]
def exchange(self, *,
receiver="",
prior="",
xid="",
route="",
modifiers=None,
attributes=None,
stamp=None,
version=Version,
pvrsn=None,
gvrsn=None,
kind=Kinds.json,
framed=False,
nested=False,
genusify=False,
embeds=None,
eid=None,
save=False):
"""Build and return a signed ``exn`` message, optionally saving it to
own db.
Parameters::
sender (str): qb64 of sender identifier (AID)
receiver (str): qb64 of receiver identifier (AID)
xid (str): qb64 of exchange ID which is SAID of exchange inception 'xip'
if any
prior (str): qb64 of prior exchange event including 'xip" if any
route (str): '/' delimited path identifier of data flow handler
(behavior) to processs the reply if any (equivalent of
url path to resource)
modifiers (dict): modifiers field map (equvalent of http query string)
attributes (dict): attributes field map (payload body)
stamp (str): date-time-stamp RFC-3339 profile of ISO-8601 datetime of
creation of message or data, default is now.
version (Versionage): KERI protocol default version if psvrsn is None
pvrsn (Versionage): KERI protocol version
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
kind (str): serialization for key event message
one of Kinds ("json","cbor","mgpk","cesr")
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
embeds (dict or None): embedded message serders if any.
eid (str or None): qb64 of endpoint provider identifier if any.
save (bool): True means process local copy into db after building.
Returns::
bytearray: signed exchange message with count code and receipt
couples (pre+cig).
"""
pvrsn = pvrsn if pvrsn is not None else version
# generate exchange with pathed embed attachments in end
serder, end = specialExchange(sender=self.pre,
receiver=receiver,
xid=xid,
prior=prior,
route=route,
modifiers=modifiers,
attributes=attributes,
embeds=embeds,
stamp=stamp,
version=version,
pvrsn=pvrsn,
gvrsn=gvrsn,
kind=kind,)
if self.kever.prefixer.transferable:
msg = self.endorse(serder=serder, framed=framed, nested=nested,
gvrsn=gvrsn, genusify=genusify)
else:
cigars = self.sign(ser=serder.raw,
indexed=False)
gvrsn = gvrsn if gvrsn is not None else version
msg = eventing.messagize(serder, cigars=cigars, framed=framed,
nested=nested, gvrsn=gvrsn, genusify=genusify)
msg.extend(end)
if save:
self.psr.parseOne(ims=bytearray(msg)) # process local copy into db
return msg
[docs]
def endorse(self, serder, last=False, framed=False, nested=False,
gvrsn=Version, genusify=False):
"""Return 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): Affects which signature group code messagize will use
the seal type provided here,
True means provide SealLast so messagize uses TransLastIdxSigGroups
Query messages should always use SealLast.
False means provide SealEvent so messagize uses TransIdxSigGroups
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
Returns::
bytearray: endorsed message with attached signatures from messagize.
"""
if self.kever.prefixer.transferable:
# create SealEvent or SealLast for endorser's est evt whose keys are
# used to sign to indicate to messagize which type sig group to use
kever = self.kever
if last:
source = SealLast(i=kever.prefixer.qb64)
else:
source = 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, source=source,
framed=framed,nested=nested, gvrsn=gvrsn,
genusify=genusify)
else:
cigars = self.sign(ser=serder.raw, indexed=False)
msg = eventing.messagize(serder=serder, cigars=cigars, framed=framed,
nested=nested, gvrsn=gvrsn, genusify=genusify)
return msg
[docs]
def receipt(self, serder, kind=Kinds.json, version=Version, framed=False,
nested=False, gvrsn=Version, genusify=False):
"""Build own receipt ``rct`` message of serder with count code and
receipt couples (pre+cig). Processes local copy into db to validate.
Parameters::
serder (Serder): event serder to receipt.
kind (str): serialization for receipt message
one of Kinds ("json","cbor","mgpk","cesr")
version (Versionage): KERI protocol default version for receipt
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
Returns::
bytearray: receipt message with attached signatures.
"""
ked = serder.ked
reserder = eventing.receipt(pre=ked["i"],
sn=int(ked["s"], 16),
said=serder.said,
kind=kind,
version=version)
# sign serder event
if self.kever.prefixer.transferable:
source = 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, source=source,
framed=framed, nested=nested, gvrsn=gvrsn,
genusify=genusify)
else:
cigars = self.sign(ser=serder.raw,
indexed=False)
msg = eventing.messagize(reserder, cigars=cigars, framed=framed,
nested=nested, gvrsn=gvrsn, genusify=genusify)
self.psr.parseOne(ims=bytearray(msg)) # process local copy into db
return msg
[docs]
def witness(self, serder, kind=Kinds.json, version=Version, framed=False,
nested=False, gvrsn=Version, genusify=False):
"""Build own witness receipt ``rct`` message of serder with count code
and witness indexed receipt signatures, if the key state of
``serder.pre`` shows that own pre is a current witness of the event in
serder.
Note::
The caller must ensure that the serder being witnessed has been
accepted as a valid event into this hab controller's KEL before
calling this method.
Parameters::
serder (Serder): event serder to witness.
kind (str): serialization for receipt message
one of Kinds ("json","cbor","mgpk","cesr")
version (Versionage): KERI protocol default version for receipt
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
Returns::
bytearray: witness receipt message with attached signatures.
Raises::
ValueError: if own prefix is transferable, if the key state for
``serder.pre`` is missing, or if own prefix is not a witness
of the event.
"""
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,
kind=kind,
version=version)
# 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, framed=framed,
nested=nested,gvrsn=gvrsn, genusify=genusify)
self.psr.parseOne(ims=bytearray(msg)) # process local copy into db
return msg
[docs]
def replay(self, pre=None, fn=0):
"""Return replay of FEL (first-seen event log) for ``pre`` starting
from ``fn``. Default pre is own ``.pre``.
Args:
pre (str or None): qb64 str or bytes of identifier prefix.
Default is own ``.pre``.
fn (int): first-seen ordering number to start from.
Returns:
bytearray: serialized event log messages.
"""
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):
"""Return replay of FEL (first-seen event log) for all prefixes.
Returns:
bytearray: serialized event log messages for all prefixes.
"""
msgs = bytearray()
for msg in self.db.cloneAllPreIter():
msgs.extend(msg)
return msgs
[docs]
def fetchEnd(self, cid: str, role: str, eid: str):
"""Return the endpoint record for the given controller, role, and
endpoint provider.
Args:
cid (str): qb64 identifier prefix of controller.
role (str): endpoint role.
eid (str): qb64 identifier prefix of endpoint provider.
Returns:
EndpointRecord or None: endpoint record instance, or None if not
found.
"""
return self.db.ends.get(keys=(cid, role, eid))
[docs]
def fetchLoc(self, eid: str, scheme: str = Schemes.http):
"""Return the location record for the given endpoint provider and
scheme.
Args:
eid (str): qb64 identifier prefix of endpoint provider.
scheme (str): url scheme. Default is ``Schemes.http``.
Returns:
LocationRecord or None: location record instance, or None if not
found.
"""
return self.db.locs.get(keys=(eid, scheme))
[docs]
def fetchEndAllowed(self, cid: str, role: str, eid: str):
"""Return whether ``eid`` is allowed as endpoint provider for ``cid``
in ``role``.
Args:
cid (str): qb64 identifier prefix of controller authorizing
endpoint provider ``eid`` in role.
role (str): endpoint role such as controller, witness, watcher,
etc.
eid (str): qb64 identifier prefix of endpoint provider in role.
Returns:
bool or None: True if ``eid`` is allowed, False if not, None if
no endpoint record exists.
"""
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):
"""Return whether ``eid`` is enabled as endpoint provider for ``cid``
in ``role``.
Args:
cid (str): qb64 identifier prefix of controller authorizing
endpoint provider ``eid`` in role.
role (str): endpoint role such as controller, witness, watcher,
etc.
eid (str): qb64 identifier prefix of endpoint provider in role.
Returns:
bool or None: True if ``eid`` is enabled, False if not, None if
no endpoint record exists.
"""
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):
"""Return whether ``eid`` is authorized (enabled or allowed) as
endpoint provider for ``cid`` in ``role``.
Args:
cid (str): qb64 identifier prefix of controller authorizing
endpoint provider ``eid`` in role.
role (str): endpoint role such as controller, witness, watcher,
etc.
eid (str): qb64 identifier prefix of endpoint provider in role.
Returns:
bool or None: True if ``eid`` is enabled or allowed, False if
neither, None if no endpoint record exists.
"""
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 = Schemes.http):
"""Return the url for the endpoint provider given by ``eid``.
Args:
eid (str): qb64 identifier prefix of endpoint provider.
scheme (str): url scheme. Default is ``Schemes.http``.
Returns:
str or None: url string for the endpoint provider (empty string
when url is nullified), or None when no location record exists.
"""
loc = self.db.locs.get(keys=(eid, scheme))
return loc.url if loc else loc
[docs]
def fetchUrls(self, eid: str, scheme: str = ""):
"""Return urls keyed by scheme for the given ``eid``.
Note:
The caller is responsible for independently verifying that ``eid``
is allowed for a given ``cid`` and role. Entries with empty urls
are excluded from the result.
Args:
eid (str): qb64 identifier prefix of endpoint provider.
scheme (str): url scheme filter. Empty string means all schemes.
Returns:
hicting.Mict: urls keyed by scheme for the given ``eid``.
"""
return hicting.Mict([(keys[1], loc.url) for keys, loc in
self.db.locs.getTopItemIter(keys=(eid, scheme)) if loc.url])
[docs]
def fetchRoleUrls(self, cid: str, *, role: str = "", scheme: str = "",
eids=None, enabled: bool = True, allowed: bool = True):
"""Return nested dicts of role -> eid -> scheme -> url for the given ``cid``.
Args:
cid (str): qb64 identifier prefix of the controller authorizing
endpoint provider ``eid`` in role.
role (str): endpoint role filter (e.g. ``controller``, ``witness``,
``watcher``). Empty string means all roles.
scheme (str): url scheme filter. Empty string means all schemes.
eids (list or None): when provided, restrict results to only eids
in this list.
enabled (bool): True means include enabled endpoint providers.
allowed (bool): True means include allowed endpoint providers.
Returns:
hicting.Mict: nested Mict keyed as ``rurls[role][eid][scheme]``,
where each leaf value is a url string.
"""
if eids is None:
eids = []
rurls = hicting.Mict()
if role == 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(Roles.witness,
hicting.Mict([(eid, surls)]))
for (_, erole, eid), end in self.db.ends.getTopItemIter(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/allowed witnesses if not a witness at latest key state.
Args:
cid (str): qb64 identifier prefix of controller whose witnesses
are being fetched.
scheme (str): url scheme filter. Empty string means all schemes.
eids (list or None): when provided, restrict results to only eids
in this list.
enabled (bool): True means include enabled witnesses.
allowed (bool): True means include allowed witnesses.
Returns:
hicting.Mict: nested Mict keyed as ``rurls[role][eid][scheme]``,
where each leaf value is a url string. Role is always
``witness`` for results from this method.
"""
return (self.fetchRoleUrls(cid=cid,
role=Roles.witness,
scheme=scheme,
eids=eids,
enabled=enabled,
allowed=allowed))
[docs]
def endsFor(self, pre):
"""Load authorized endpoints for the provided AID.
Args:
pre (str): qb64 aid for which to load ends.
Returns:
dict: nested dict of role -> eid -> scheme -> endpoint.
"""
ends = dict()
for (_, erole, eid), end in self.db.ends.getTopItemIter(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, framed=False, nested=False, gvrsn=Version, genusify=False, **kwa):
"""Return own endorsed reply message.
Parameters::
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
**kwa: keyword arguments forwarded to ``eventing.reply``, including:
route (str): route path string indicating the data flow handler.
data (list): dicts of committed data such as seals.
dts (str): date-time-stamp of message at time of creation.
version (Version): version instance.
kind (str): serialization kind.
Returns::
bytearray: reply message.
"""
kwa["pre"] = self.pre
return self.endorse(eventing.reply(**kwa), framed=framed, nested=nested,
gvrsn=gvrsn, genusify=genusify)
[docs]
def makeEndRole(self, eid, role=Roles.controller, allow=True, stamp=None,
**kwa):
"""Return a reply message allowing or disallowing endpoint provider
``eid`` in ``role``.
Args:
eid (str): qb64 of endpoint provider to be authorized.
role (str): authorized role for ``eid``. Default is
``Roles.controller``.
allow (bool): True means add ``eid`` at ``role`` as authorized.
False means cut ``eid`` at ``role`` as unauthorized.
stamp (str or None): date-time-stamp RFC-3339 profile of iso8601
datetime. None means use now.
**kwa: keyword arguments forwarded to ``eventing.reply``, including:
version (Version): version instance.
kind (str): serialization kind.
Returns:
bytearray: reply message.
"""
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, **kwa)
[docs]
def loadEndRole(self, cid, eid, role=Roles.controller, framed=False,
nested=False, gvrsn=Version, genusify=False):
"""Load and return the messagized end role authorization record for
the given ``cid``, ``eid``, and ``role`` from the database, including
associated attachments.
Parameters::
cid (str): qb64 identifier prefix of controller.
eid (str): qb64 identifier prefix of endpoint provider.
role (str): endpoint role. Default is ``Roles.controller``.
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
Returns::
bytearray: messagized end role record with attachments, or empty
bytearray if not found or not enabled/allowed.
"""
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 = fetchTsgs(db=self.db.ssgs, diger=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 = 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,
source=seal,
framed=framed,
nested=nested,
gvrsn=gvrsn,
genusify=genusify))
return msgs
[docs]
def makeLocScheme(self, url, eid=None, scheme="http", stamp=None, **kwa):
"""Return a reply message of own url service endpoint at ``scheme``.
Parameters::
url (str): url of endpoint. May have scheme missing or not. An
empty url nullifies the location.
eid (str or None): qb64 of endpoint provider to be authorized.
None means use own ``.pre``.
scheme (str): url scheme; must match scheme in url if present.
Default is ``"http"``.
stamp (str or None): date-time-stamp RFC-3339 profile of iso8601
datetime. None means use now.
**kwa: keyword arguments forwarded to ``eventing.reply``, including:
route (str): route path string indicating the data flow handler.
data (list): dicts of committed data such as seals.
dts (str): date-time-stamp of message at time of creation.
version (Version): version instance.
kind (str): serialization kind.
Returns::
bytearray: reply message.
"""
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, **kwa)
[docs]
def replyLocScheme(self, eid, scheme="", **kwa):
"""Return a reply message stream of location scheme entries authed by
the given ``eid`` from the reply database, including associated
attachments, for dissemination of BADA reply data authentication
proofs.
Note:
Currently uses a promiscuous model for permitting endpoint
discovery. Future versions will use an identity constraint graph
to constrain discovery.
Parameters::
eid (str): endpoint provider id.
scheme (str): url scheme filter. Empty string means all schemes.
**kwa: keyword arguments forwarded to ``eventing.reply``, including:
route (str): route path string indicating the data flow handler.
data (list): dicts of committed data such as seals.
dts (str): date-time-stamp of message at time of creation.
version (Version): version instance.
kind (str): serialization kind.
Returns::
bytearray: reply message stream for location scheme entries.
"""
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, **kwa))
return msgs
[docs]
def loadLocScheme(self, eid, scheme=None, framed=False, nested=False,
gvrsn=Version, genusify=False):
"""Load and return messagized location scheme records for the given
``eid`` and optional ``scheme`` from the database, including associated
attachments.
Parameters::
eid (str): qb64 identifier prefix of endpoint provider.
scheme (str or None): url scheme filter. None means all schemes.
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
Returns::
bytearray: messagized location scheme records with attachments.
"""
msgs = bytearray()
keys = (eid, scheme) if scheme else (eid,)
for (pre, _), said in self.db.lans.getTopItemIter(keys=keys):
serder = self.db.rpys.get(keys=(said.qb64,))
cigars = self.db.scgs.get(keys=(said.qb64,))
tsgs = fetchTsgs(db=self.db.ssgs, diger=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 = 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,
source=seal,
framed=framed,
nested=nested,
gvrsn=gvrsn,
genusify=genusify))
return msgs
[docs]
def replyEndRole(self, cid, role=None, eids=None, scheme="", **kwa):
"""Return a reply message stream of end role authorization entries
authed by the given ``cid`` from the reply database, including
associated attachments, for dissemination of BADA reply data
authentication proofs.
Note:
Currently uses a promiscuous model for permitting endpoint
discovery. Future versions will use an identity constraint graph
to constrain discovery.
Behavior by argument combination:
- ``cid`` only: end authz for all eids in all roles and loc url for
all schemes at each eid (filtered by ``eids`` if provided).
- ``cid`` + ``scheme``: end authz for all eids in all roles and loc
url for ``scheme`` at each eid.
- ``cid`` + ``role``: end authz for all eids in ``role`` and loc url
for all schemes at each eid.
- ``cid`` + ``role`` + ``scheme``: end authz for all eids in ``role``
and loc url for ``scheme`` at each eid.
Parameters::
cid (str): qb64 identifier prefix of controller authorizing
endpoint provider ``eid``.
role (str or None): endpoint role filter. None means all roles.
eids (list or None): when provided, restrict results to only eids
in this list.
scheme (str): url scheme filter. Empty string means all schemes.
**kwa: keyword arguments forwarded to ``eventing.reply``, including:
route (str): route path string indicating the data flow handler.
data (list): dicts of committed data such as seals.
dts (str): date-time-stamp of message at time of creation.
version (Version): version instance.
kind (str): serialization kind.
Returns::
bytearray: reply message stream for end role entries.
"""
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 == 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, **kwa))
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, **kwa))
for (_, erole, eid), end in self.db.ends.getTopItemIter(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, **kwa):
"""Return a reply message stream of entries authed by the given ``aid``
for OOBI-initiated endpoint discovery, including associated
attachments, for dissemination of BADA reply data authentication
proofs.
Note:
Currently uses a promiscuous model for permitting OOBI-initiated
endpoint discovery. Future versions will use an identity constraint
graph to constrain discovery.
This method is the entry point for initiating replies generated by
:meth:`replyEndRole` and/or :meth:`replyLocScheme`.
Parameters::
aid (str): qb64 of identifier in oobi; may be cid or eid.
role (str): authorized role for eid.
eids (list or None): when provided, restrict results to only eids
in this list.
**kwa: keyword arguments forwarded to ``eventing.reply``, including:
route (str): route path string indicating the data flow handler.
data (list): dicts of committed data such as seals.
dts (str): date-time-stamp of message at time of creation.
version (Version): version instance.
kind (str): serialization kind.
Returns::
bytearray: reply message stream for OOBI endpoint entries.
"""
# 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, **kwa)
[docs]
def getOwnEvent(self, sn, allowPartiallySigned=False):
"""Return the event serder, controller signatures, and seal source
duple for own event at sequence number ``sn``.
Args:
sn (int): sequence number of event.
allowPartiallySigned (bool): True means attempt to load from
partial signed escrow if not found in KEL.
Returns:
tuple: ``(serder, sigers, duple)`` where ``serder`` is the event
Serder, ``sigers`` is a list of Siger instances, and ``duple`` is
the seal source couple or None.
Raises:
MissingEntryError: if no event is found for own prefix at ``sn``.
"""
dig = self.db.kels.getLast(keys=self.pre, on=sn)
dig = dig.encode("utf-8") if dig else None
if dig is None and allowPartiallySigned:
vals = self.db.pses.getLast(keys=self.pre, on=sn)
dig = vals.encode() if vals else None
if dig is None:
raise MissingEntryError("Missing event for pre={} at sn={}."
"".format(self.pre, sn))
serder = self.db.evts.get(keys=(self.pre, dig))
sigers = self.db.sigs.get(keys=(self.pre, dig))
duple = self.db.aess.get(keys=(self.pre, dig))
return serder, sigers, duple
[docs]
def msgOwnEvent(self, sn, allowPartiallySigned=False, framed=False, nested=False,
gvrsn=Version, genusify=False):
"""Messagize own event at sn with attachments if any.
Parameters::
sn (int): sequence number of event.
allowPartiallySigned (bool): True means attempt to load from
partial signed escrow if not found in KEL.
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
Returns::
msg (bytearray): qb64b serialization of own event at ``sn`` with
optionally attached signatures and seal source couple.
"""
serder, sigers, duple = self.getOwnEvent(sn=sn,
allowPartiallySigned=allowPartiallySigned)
seal = None
if duple is not None:
number, diger = duple
seal = SealSource(s=number.snh, d=diger.qb64)
return eventing.messagize(serder, sigers=sigers, bonds=seal, framed=framed,
nested=nested, gvrsn=gvrsn, genusify=genusify)
[docs]
def msgOwnInception(self, allowPartiallySigned=False, framed=False, nested=False,
gvrsn=Version, genusify=False):
"""Return messagized own inception event with attached signatures,
retrieved from the database.
Parameters::
allowPartiallySigned (bool): True means attempt to load from
partial signed escrow if not found in KEL.
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
Returns::
msg (bytearray): messagized inception event with attached signatures.
"""
return self.msgOwnEvent(sn=0, allowPartiallySigned=allowPartiallySigned,
framed=framed, nested=nested, gvrsn=gvrsn,
genusify=genusify )
[docs]
def msgOtherEvent(self, pre, sn, framed=False, nested=False, gvrsn=Version,
genusify=False):
"""Return messagized bytearray message with attached signatures of
the event at sequence number ``sn`` for ``pre``, retrieved from the
database.
Parameters::
pre (str): qb64 identifier prefix.
sn (int): sequence number of event.
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
Returns::
msg (bytearray |None): messagized event with attached signatures,
or None if ``pre`` is not in kevers.
Raises::
MissingEntryError: if no event is found for ``pre`` at ``sn``.
"""
if pre not in self.kevers:
return None
dig = self.db.kels.getLast(keys=pre, on=sn)
if dig is None:
raise MissingEntryError("Missing event for pre={} at sn={}."
"".format(pre, sn))
serder = self.db.evts.get(keys=(pre, dig))
sigers = [siger for siger in self.db.sigs.getIter(keys=(pre, dig))]
return eventing.messagize(serder, sigers=sigers, framed=framed,
nested=nested, gvrsn=gvrsn, genusify=genusify)
[docs]
def processCues(self, cues, gvrsn=Version, version=Version,
kind=Kinds.json):
"""Return bytearray of messages resulting from processing all cues.
Args:
cues (deque): cue dicts to process.
gvrsn (Versionage): CESR genus version for attachment group codes.
version (Versionage): KERI protocol version for generated receipt
events.
kind (str): Serialization kind for generated receipt events.
Returns:
bytearray: concatenated outgoing messages.
"""
msgs = bytearray() # outgoing messages
for msg in self.processCuesIter(cues, gvrsn=gvrsn,
version=version, kind=kind):
msgs.extend(msg)
return msgs
[docs]
def processCuesIter(self, cues, gvrsn=Version, version=Version,
kind=Kinds.json):
"""Iterate through cues and yield one or more msgs for each cue.
Args:
cues (deque): cue dicts to process.
gvrsn (Versionage): CESR genus version for attachment group codes.
version (Versionage): KERI protocol version for generated receipt
events.
kind (str): Serialization kind for generated receipt events.
Yields:
bytearray: message(s) produced for each cue.
"""
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 = Prefixer(qb64=cuedKed["i"])
logger.info("%s got cue: kin=%s%s", self.pre, cueKin,
cuedSerder.said)
logger.debug(f"event=\n{cuedSerder.pretty()}\n")
if cuedKed["t"] == Ilks.icp:
dgkey = dgKey(self.pre, self.iserder.said)
found = False
if cuedPrefixer.transferable: # find if have rct from other pre for own icp
for sprefixer, snumber, sdiger, siger in self.db.vrcs.getIter(dgkey):
if sprefixer.qb64 == cuedKed["i"]:
found = True
else: # find if already rcts of own icp
for prefixer, cigar in self.db.rcts.getIter(dgkey):
if prefixer.qb64.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.msgOwnInception(framed=True,
gvrsn=gvrsn))
msgs.extend(self.receipt(cuedSerder, framed=True,
gvrsn=gvrsn, version=version,
kind=kind))
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,
gvrsn=gvrsn, version=version, kind=kind)
yield msg
elif cueKin in ("witness",): # cue to witness a received event, own pre must be a witness
cuedSerder = cue["serder"]
logger.info("%s got cue: kin=%s %s", self.pre, cueKin,
cuedSerder.said)
logger.debug(f"event=\n{cuedSerder.pretty()}\n")
msgs.extend(self.witness(cuedSerder, framed=True,
gvrsn=gvrsn, version=version,
kind=kind))
yield msgs
elif cueKin in ("query",): # cue to send a query message
pre = cue["pre"]
src = cue["src"]
route = cue.get("route")
query = cue.get("query")
kwa = dict()
if route is not None:
kwa["route"] = route
msg = self.query(pre=pre, src=src, query=query,
gvrsn=gvrsn, version=version, kind=kind,
**kwa)
yield msg
elif cueKin in ("notice",): # cue to notify of new own event accepted into KEL
cuedSerder = cue["serder"]
logger.info("%s got cue: kin=%s %s", self.pre, cueKin,
cuedSerder.said)
logger.debug(f"event=\n{cuedSerder.pretty()}\n")
elif cueKin in ("noticeBadCloneFN",): # cue to notify of bad cloned first seen ordinal
cuedSerder = cue["serder"]
fn = cue["fn"]
firner = cue["firner"]
dater = cue["dater"]
logger.error("%s got cue: kin=%s %s mismatch fn=%s expected=%s at %s",
self.pre, cueKin, cuedSerder.said, fn, firner.sn,
dater.dts)
logger.debug(f"event=\n{cuedSerder.pretty()}\n")
# ToDo XXXX cue for kin = "approveDelegation" own is delegator
# ToDo XXXX cue for kin = "psUnescrow"
# ToDo XXXX cue for kin=""remoteMemberedSig""
elif cueKin in ("keyStateSaved",): # cue to notify that key state has been saved
ksn = cue["ksn"]
logger.info("%s got cue: kin=%s for aid=%s at sn=%s",
self.pre, cueKin, ksn.get("i"), ksn.get("s"))
elif cueKin in ("stream",): # cue to notify of a query stream request
cuedSerder = cue["serder"]
pre = cue["pre"]
src = cue["src"]
topics = cue["topics"]
logger.info("%s got cue: kin=%s for pre=%s src=%s topics=%s",
self.pre, cueKin, pre, src, topics)
logger.debug(f"event=\n{cuedSerder.pretty()}\n")
elif cueKin in ("invalid",): # cue to notify of an invalid query message
cuedSerder = cue["serder"]
logger.error("%s got cue: kin=%s %s",
self.pre, cueKin, cuedSerder.said)
logger.debug(f"event=\n{cuedSerder.pretty()}\n")
def witnesser(self):
return True
[docs]
class Hab(BaseHab):
"""Local habitat for a given identifier controller.
Provides the local resource environment (hab or habitat) for a controller,
including dependency injection of the database, keystore, configuration
file, Kevery, and key store Manager.
Attributes:
ks (keeping.Keeper): LMDB key store. (Injected)
db (basing.Baser): LMDB database for KEL etc. (Injected)
cf (configing.Configer): Configuration file instance. (Injected)
mgr (keeping.Manager): Creates and rotates keys in the key store. (Injected)
rtr (routing.Router): Routes reply ``rpy`` messages. (Injected)
rvy (routing.Revery): Factory that processes reply ``rpy`` messages. (Injected)
kvy (eventing.Kevery): Factory for local processing of local event messages. (Injected)
psr (parsing.Parser): Parses local messages for ``.kvy`` and ``.rvy``. (Injected)
name (str): Alias of the controller.
pre (str): qb64 prefix of the own local controller, or ``None`` if new.
temp (bool): ``True`` means testing — uses 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 with respect to
databases; ``False`` means not yet fully initialized.
delpre (str or None): Delegator prefix if any, else ``None``.
kevers (dict): Kever instances from KELs in the local db, keyed by
qb64 prefix. Read-through cache of states for KELs in
``db.states``. (Read-only property)
iserder (serdering.SerderKERI): Own inception event. (Read-only property)
prefixes (oset.OrderedSet): Local prefixes for ``.db``. (Read-only property)
accepted (bool): ``True`` means accepted into the local KEL,
``False`` otherwise. (Read-only property)
"""
def __init__(self, **kwa):
super(Hab, self).__init__(**kwa)
[docs]
def incept(self, *, secrecies=None, iridx=0, code=MtrDex.Blake3_256,
dcode=MtrDex.Blake3_256, icode=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,
kind=Kinds.json, version=Version):
"""Finish setting up or making this Hab from parameters, including inception.
Assumes injected dependencies have already been set up. When
``secrecies`` are provided the key manager replays pre-loaded key
pairs; otherwise it generates new keys via ``mgr.incept``.
After key material is prepared the inception event is built,
persisted to the habitat record store, and processed by the local
Kevery. ``MissingSignatureError`` is silently swallowed during
delegated-identifier initialisation.
Args:
secrecies (list or None): List of secret seeds to pre-load key
pairs. When provided, key pairs are replayed rather than
generated. Defaults to ``None``.
iridx (int): Initial rotation index used after ingestion of
``secrecies``. Defaults to ``0``.
code (str): Prefix derivation code. Defaults to
``MtrDex.Blake3_256``.
dcode (str): Next-key derivation code. Defaults to
``MtrDex.Blake3_256``.
icode (str): Signing key derivation code. Defaults to
``MtrDex.Ed25519_Seed``.
transferable (bool): ``True`` means the prefix is transferable
(default). ``False`` means non-transferable; forces
``ncount=0``, ``nsith='0'``, and ``code=MtrDex.Ed25519N``.
isith (int, str, list, or None): Incepting signing threshold as an
int, hex str, or weighted list. Computed from ``verfers`` when
``None``.
icount (int): Number of incepting signing keys. Defaults to ``1``.
nsith (int, str, list, or None): Next signing threshold as an int,
hex str, or weighted list. Defaults to ``isith`` when ``None``.
ncount (int or None): Number of next (pre-rotated) keys. Defaults
to ``icount`` when ``None``.
toad (int, str, or None): Witness threshold as an int or hex str.
Computed from the number of witnesses when ``None``.
wits (list or None): qb64 prefixes of witnesses, if any.
delpre (str or None): qb64 delegator identifier prefix, if any.
estOnly (bool): ``True`` adds ``TraitDex.EstOnly``, restricting
the KEL to establishment events only. Defaults to ``False``.
DnD (bool): ``True`` adds ``TraitDex.DnD``, disallowing delegated
identifiers from this identifier. Defaults to ``False``.
hidden (bool): When ``True`` the Hab is not saved to the habitat
record store and its prefix is not added to ``self.prefixes``.
Defaults to ``False``.
data (list or None): Seal dicts to embed in the inception event.
algo (str or None): Key-creation algorithm code used by the key
manager when generating new keys.
salt (str or None): qb64 salt for randomisation when the salty
algorithm is used.
tier (str or None): Security-criticality tier code used with the
salty algorithm.
kind (str): serialization value of Kinds ("json","cbor","mgpk","cesr")
version (Versionage): version for incept key event message
Raises:
ClosedError: If the key store, database, or config file is not
open.
ConfigurationError: If inception event processing fails for any
reason other than a missing signature.
"""
if not (self.ks.opened and self.db.opened and self.cf.opened):
raise 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 = 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).incept(isith=isith,
verfers=verfers,
nsith=nsith,
digers=digers,
code=code,
toad=toad,
wits=wits,
estOnly=estOnly,
DnD=DnD,
delpre=delpre,
data=data,
kind=kind,
version=version)
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 = HabitatRecord(hid=self.pre, name=self.name, domain=self.ns)
# must add self.pre to self.prefixes before calling processEvent so that
# Kever.locallyOwned or Kever.locallyDelegated or Kever.locallyWitnessed
# evaluates correctly when processing own inception event.
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 ConfigurationError("Improper Habitat inception for "
"pre={} {}".format(self.pre, ex))
# read in self.cf config file and process any oobis or endpoints
self.reconfigure(version=version, kind=kind) # 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, **kwa):
"""Perform a rotation operation and register it in the database.
Advances the key state by replaying the pre-committed next keys
(``mgr.replay``) or, when no pre-committed keys exist, generating a
fresh set (``mgr.rotate``). Key store state is rolled back
automatically if the rotation event fails validation, keeping the key
store and KEL in sync (see issue #819). Stale private keys from the
previous signing set are erased only after successful validation.
Args:
isith (int, str, list, or None): Current signing threshold as an
int, hex str, or weighted list. Defaults to the prior next
threshold when ``None``.
nsith (int, str, list, or None): Next signing threshold as an int,
hex str, or weighted list. Defaults to ``isith`` when ``None``.
ncount (int or None): Number of next (pre-rotated) keys. Defaults
to the length of the prior next digers when ``None``.
toad (int, str, or None): Witness threshold after cuts and adds, as
an int or hex str.
cuts (list or None): qb64 prefixes of witnesses to remove.
adds (list or None): qb64 prefixes of witnesses to add.
data (list or None): Seal dicts to embed in the rotation event.
Returns:
bytearray: Rotation message with attached signatures.
Raises:
Exception: Re-raises any exception raised by
``BaseHab.rotate`` after rolling back key store state.
"""
# 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
# Save pre-rotation key state so we can rollback if event validation
# fails. Both mgr.replay() and mgr.rotate() advance and persist key
# state before the rotation event is validated by BaseHab.rotate().
# Without rollback, a failed rotation leaves the key store out of
# sync with the KEL (issue #819).
ps_before = self.mgr.ks.sits.get(self.pre)
try:
verfers, digers = self.mgr.replay(pre=self.pre, erase=False)
except IndexError: # old next is new current
verfers, digers = self.mgr.rotate(pre=self.pre,
ncount=ncount,
temp=self.temp,
erase=False)
try:
msg = super(Hab, self).rotate(verfers=verfers, digers=digers,
isith=isith,
nsith=nsith,
toad=toad,
cuts=cuts,
adds=adds,
data=data, **kwa)
except Exception:
# Rotation event validation failed. Rollback key state to
# pre-rotation snapshot so KEL and key store stay in sync.
self.mgr.ks.sits.pin(self.pre, val=ps_before)
raise
# Event validated successfully. Now safe to erase old stale
# private keys that were preserved during the key advancement.
if ps_before.old.pubs:
for pub in ps_before.old.pubs:
self.mgr.ks.pris.rem(pub)
return msg
[docs]
class SignifyHab(BaseHab):
"""Remote-signer habitat for a given identifier controller.
Provides the local resource environment (hab or habitat) for a controller
whose private keys are held by a remote signer (Signify agent), rather
than locally. Inception and rotation events are supplied to ``make`` and
``rotate`` as pre-built, pre-signed serders; this class never generates or
holds private key material, and its ``sign`` method always raises
``KeriError``.
Attributes:
ks (keeping.Keeper): LMDB key store. (Injected)
db (basing.Baser): LMDB database for KEL etc. (Injected)
cf (configing.Configer): Configuration file instance. (Injected)
mgr (keeping.Manager): Creates and rotates keys in the key store. (Injected)
rtr (routing.Router): Routes reply ``rpy`` messages. (Injected)
rvy (routing.Revery): Factory that processes reply ``rpy`` messages. (Injected)
kvy (eventing.Kevery): Factory for local processing of local event messages. (Injected)
psr (parsing.Parser): Parses local messages for ``.kvy`` and ``.rvy``. (Injected)
name (str): Alias of the controller.
pre (str): qb64 prefix of the own local controller, or ``None`` if new.
temp (bool): ``True`` means testing — uses 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 with respect to
databases; ``False`` means not yet fully initialized.
delpre (str or None): Delegator prefix if any, else ``None``.
"""
def __init__(self, **kwa):
super(SignifyHab, self).__init__(**kwa)
[docs]
def sign(self, ser, verfers=None, indexed=True, indices=None, ondices=None, **kwa):
"""Signing is not supported for SignifyHab. So raises error.
Private keys are held by the remote Signify agent, so local signing is
intentionally disabled.
Parameters::
ser (bytes): Serialization to sign.
verfers (list or None): Ignored.
indexed (bool): Ignored.
indices (list or None): Ignored.
ondices (list or None): Ignored.
**kwa: Ignored.
Raises::
KeriError: Always because local signing is not permitted for this hab type.
"""
raise KeriError("Signify hab does not support local signing")
[docs]
def incept(self, *, serder=None, sigers=None, source=None, bonds=None,
wigers=None, cigars=None, framed=False, nested=False,
gvrsn=Version, genusify=False, **kwa):
"""Finish setting up this SignifyHab from a pre-built inception event.
Registers the prefix, processes the inception event through the local
Kevery, persists the habitat record, and marks the hab as initialised.
Args:
serder (SerderKERI): Pre-built inception event serder. The prefix
``serder.ked["i"]`` is assigned to ``self.pre``.
sigers (list[Siger]|None): Siger instances carrying the remote
agent's signatures over ``serder.raw``.
source (SealEvent|SealLast|None): optiona modifier to sigers when provided
If SealEvent use attachment group code TransIdxSigGroups plus attach
triple pre+snu+dig made from (i,s,d) of seal plus ControllerIdxSigs
plus attached indexed sigs in sigers
Elif SealLast use attachment group code TransLastIdxSigGroups plus
attach uniple pre made from (i,) of seal plus ControllerIdxSigs
plus attached indexed sigs in sigers
Else None use ControllerIdxSigs plus attached indexed sigs in sigers
bonds (list[]|SealEvent|SealSource|SealLast|BlindState|BoundState|TypeMedia|None):
Non signature based authenticator typically an event reference or may
Only v2 supports BlindState|BoundState|TypeMedia
if bonds is not list convert to list.
wigers (list): optional list of Siger instances of witness index signatures
cigars (list): optional list of Cigars instances of non-transferable non indexed
signatures from which to form receipt couples.
Each cigar.vefer.qb64 is pre of receiptor and cigar.qb64 is signature
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
**kwas: Absorbed for API compatibility; not used.
"""
if not serder:
raise KeriError("Missing serder from remote .incept")
self.pre = serder.ked["i"] # new pre
self.prefixes.add(self.pre)
self.processEvent(serder, sigers)
habord = HabitatRecord(hid=self.pre, sid=self.pre, name=self.name, domain=self.ns)
self.save(habord)
self.inited = True
msg = eventing.messagize(serder, sigers=sigers, source=source, bonds=bonds,
wigers=wigers, cigars=cigars, framed=framed,
nested=nested, gvrsn=gvrsn, genusify=genusify)
return msg
[docs]
def rotate(self, *, serder=None, sigers=None, source=None, bonds=None,
wigers=None, cigars=None, framed=False, nested=False,
gvrsn=Version, genusify=False, **kwa):
"""Messagize a rotation operation from a pre-built, pre-signed event.
Packages the provided serder and sigers into a message and processes
it through the local Kevery to update key state.
Parameters::
serder (SerderKERI): Pre-built rotation event serder.
sigers (list[Siger]|None): Siger instances carrying the remote
agent's signatures over ``serder.raw``.
source (SealEvent|SealLast|None): optiona modifier to sigers when provided
If SealEvent use attachment group code TransIdxSigGroups plus attach
triple pre+snu+dig made from (i,s,d) of seal plus ControllerIdxSigs
plus attached indexed sigs in sigers
Elif SealLast use attachment group code TransLastIdxSigGroups plus
attach uniple pre made from (i,) of seal plus ControllerIdxSigs
plus attached indexed sigs in sigers
Else None use ControllerIdxSigs plus attached indexed sigs in sigers
bonds (list[]|SealEvent|SealSource|SealLast|BlindState|BoundState|TypeMedia|None):
Non signature based authenticator typically an event reference or may
Only v2 supports BlindState|BoundState|TypeMedia
if bonds is not list convert to list.
wigers (list): optional list of Siger instances of witness index signatures
cigars (list): optional list of Cigars instances of non-transferable non indexed
signatures from which to form receipt couples.
Each cigar.vefer.qb64 is pre of receiptor and cigar.qb64 is signature
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
**kwa: Absorbed for API compatibility; not used.
Returns::
bytearray: Rotation message with attached signatures.
"""
if not serder:
raise KeriError("Missing serder from remote .rotate")
msg = eventing.messagize(serder, sigers=sigers, source=source, bonds=bonds,
wigers=wigers, cigars=cigars, framed=framed,
nested=nested, gvrsn=gvrsn, genusify=genusify)
self.processEvent(serder, sigers) # maybe should parse msg here
return msg
[docs]
def interact(self, *, serder=None, sigers=None, source=None, bonds=None,
wigers=None, cigars=None, framed=False, nested=False,
gvrsn=Version, genusify=False, **kwa):
"""Perform an interaction operation from a pre-built, pre-signed event.
Packages the provided serder and sigers into a message and processes
it through the local Kevery to update key state.
Parameters::
serder (SerderKERI): Pre-built interaction event serder.
sigers (list[Siger]): Siger instances carrying the remote
agent's signatures over ``serder.raw``.
source (SealEvent|SealLast|None): optiona modifier to sigers when provided
If SealEvent use attachment group code TransIdxSigGroups plus attach
triple pre+snu+dig made from (i,s,d) of seal plus ControllerIdxSigs
plus attached indexed sigs in sigers
Elif SealLast use attachment group code TransLastIdxSigGroups plus
attach uniple pre made from (i,) of seal plus ControllerIdxSigs
plus attached indexed sigs in sigers
Else None use ControllerIdxSigs plus attached indexed sigs in sigers
bonds (list[]|SealEvent|SealSource|SealLast|BlindState|BoundState|TypeMedia|None):
Non signature based authenticator typically an event reference or may
Only v2 supports BlindState|BoundState|TypeMedia
if bonds is not list convert to list.
wigers (list): optional list of Siger instances of witness index signatures
cigars (list): optional list of Cigars instances of non-transferable non indexed
signatures from which to form receipt couples.
Each cigar.vefer.qb64 is pre of receiptor and cigar.qb64 is signature
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
**kwa: Absorbed for API compatibility; not used.
Returns::
bytearray: Interaction message with attached signatures.
"""
if not serder:
raise KeriError("Missing serder from remote .interact")
msg = eventing.messagize(serder, sigers=sigers, source=source, bonds=bonds,
cigars=cigars, wigers=wigers, framed=framed,
nested=nested, gvrsn=gvrsn, genusify=genusify)
self.processEvent(serder, sigers) # maybe should parse msg here
return msg
[docs]
def exchange(self, *, serder=None, save=False, sigers=None, source=None,
bonds=None, wigers=None, cigars=None, framed=False,
nested=False, gvrsn=Version, genusify=False, **kwa):
"""Messagize peer-to-peer exchange message from exchange msg serder
with provided signatures. When ``save`` is ``True`` a local copy is
parsed into the database for record keeping.
Parameters::
serder (SerderKERI): Pre-built exchange event serder.
save (bool): When ``True``, parse a copy of the assembled message
into the local database. Defaults to ``False``.
sigers (list or None): Siger instances carrying signatures
over ``serder.raw``.
source (SealEvent|SealLast|None): optiona modifier to sigers when provided
If SealEvent use attachment group code TransIdxSigGroups plus attach
triple pre+snu+dig made from (i,s,d) of seal plus ControllerIdxSigs
plus attached indexed sigs in sigers
Elif SealLast use attachment group code TransLastIdxSigGroups plus
attach uniple pre made from (i,) of seal plus ControllerIdxSigs
plus attached indexed sigs in sigers
Else None use ControllerIdxSigs plus attached indexed sigs in sigers
bonds (list[]|SealEvent|SealSource|SealLast|BlindState|BoundState|TypeMedia|None):
Non signature based authenticator typically an event reference or may
Only v2 supports BlindState|BoundState|TypeMedia
if bonds is not list convert to list.
wigers (list): optional list of Siger instances of witness index signatures
cigars (list): optional list of Cigars instances of non-transferable non indexed
signatures from which to form receipt couples.
Each cigar.vefer.qb64 is pre of receiptor and cigar.qb64 is signature
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
**kwa: Absorbed for API compatibility; not used.
Returns::
msg (bytearray): Exchange message with count code and attached
signatures.
"""
if not serder:
raise KeriError("Missing serder from remote .exchange")
msg = eventing.messagize(serder=serder, sigers=sigers, source=source,
bonds=bonds, cigars=cigars, wigers=wigers,
framed=framed, nested=nested, gvrsn=gvrsn,
genusify=genusify)
if save: # this parses not .processEvent why different?
self.psr.parseOne(ims=bytearray(msg)) # process local copy into db
# maybe not being able to do messagize with source seal.
return msg
[docs]
def processEvent(self, serder, sigers):
"""Process an event through the local Kevery, re-raising all exceptions.
Unlike ``GroupHab.processEvent``, this method does **not** swallow
``MissingSignatureError``; any exception from the Kevery is wrapped
in a ``ConfigurationError`` and re-raised.
Parameters::
serder (SerderKERI): Event serder to process.
sigers (list[Siger]| None): Signature instances over
``serder.raw``.
Raises::
ConfigurationError: If the Kevery raises any exception during
event processing.
"""
try:
# verify event, update kever state, and escrow if group
self.kvy.processEvent(serder=serder, sigers=sigers)
except Exception:
raise ConfigurationError(f"Improper Habitat event type={serder.ked['t']} for "
f"pre={self.pre}.")
[docs]
def replyEndRole(self, cid, role=None, eids=None, scheme="", **kwa):
"""Build a reply message stream for endpoint role authorisations.
Assembles a ``rpy`` message stream containing the KEL replay for
``cid`` plus any authorised endpoint role records and location scheme
records relevant to the requested role and scheme filters. Uses a
promiscuous discovery model — future versions may restrict discovery
via an identity constraint graph.
The returned stream content depends on the combination of arguments:
* ``cid`` only — end authz for all eids in all roles, loc URLs for
all schemes at each eid (optionally restricted to ``eids``).
* ``cid`` + ``scheme`` — end authz for all eids in all roles, loc URL
for ``scheme`` at each eid (optionally restricted to ``eids``).
* ``cid`` + ``role`` — end authz for all eids in ``role``, loc URLs
for all schemes at each eid (optionally restricted to ``eids``).
* ``cid`` + ``role`` + ``scheme`` — end authz for all eids in
``role``, loc URL for ``scheme`` at each eid (optionally restricted
to ``eids``).
When ``role`` is ``Roles.witness`` and this hab is itself one of
``cid``'s witnesses, the KEL replay is used as the authorisation
instead of explicit end-role records.
Parameters::
cid (str): qb64 identifier prefix of the controller whose
endpoint authorisations are being requested.
role (str or None): Authorised role to filter by. ``None`` means
all roles.
eids (list or None): When provided, restricts returned records to
only the endpoint identifiers listed here.
scheme (str): URL scheme to filter location records by. An empty
string (default) means all schemes.
**kwa: keyword arguments forwarded to ``eventing.reply``, including:
route (str): route path string indicating the data flow handler.
data (list): dicts of committed data such as seals.
dts (str): date-time-stamp of message at time of creation.
version (Version): version instance.
kind (str): serialization kind.
Returns::
bytearray: Concatenated reply message stream containing KEL
replay, location scheme records, and end-role records.
"""
msgs = bytearray()
if eids is None:
eids = []
# introduce yourself, please
msgs.extend(self.replay(cid))
if role == 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, **kwa))
if witness: # we are witness, set KEL as authz
msgs.extend(self.replay(cid))
for (_, erole, eid), end in self.db.ends.getTopItemIter(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):
"""Remote-signer group (multisig) habitat.
Extends ``SignifyHab`` to support multisig group identifiers whose private
keys are held by a remote Signify agent. Tracks the group signing member
aids (``smids``) and rotating member aids (``rmids``) alongside the local
participant member hab (``mhab``).
Attributes:
mhab (SignifyHab): The local participant member hab for this group.
smids (list[str]): qb64 prefixes of current signing members of the
group.
rmids (list[str]): qb64 prefixes of rotating members of the group.
Defaults to ``smids`` when not supplied.
"""
def __init__(self, smids, mhab=None, rmids=None, **kwa):
self.mhab = mhab
self.smids = smids # group signing member aids in this group hab
self.rmids = rmids or smids # group rotating member aids in this group hab
super(SignifyGroupHab, self).__init__(**kwa)
[docs]
def incept(self, *, serder, sigers, **kwa):
"""Finish setting up this SignifyGroupHab from a pre-built inception event.
Registers the group prefix, processes the inception event, persists the
habitat record (including group member metadata), and marks the hab as
initialised.
Args:
serder (SerderKERI): Pre-built inception event serder. The prefix
``serder.ked["i"]`` is assigned to ``self.pre``.
sigers (list[Siger]): Siger instances carrying the remote
agent's signatures over ``serder.raw``.
**kwa: Absorbed for API compatibility; not used.
"""
self.pre = serder.ked["i"] # new pre
self.prefixes.add(self.pre)
self.processEvent(serder, sigers)
habord = HabitatRecord(hid=self.pre, mid=self.mhab.pre, smids=self.smids, rmids=self.rmids,
sid=self.pre, name=self.name, domain=self.ns)
self.save(habord)
self.inited = True
[docs]
def processEvent(self, serder, sigers):
"""Process an event through the local Kevery, tolerating missing signatures.
``MissingSignatureError`` is silently swallowed so that multisig events
can be created and stored with only a single local member's signature,
pending collection of the remaining co-signers' contributions.
Args:
serder (SerderKERI): Event serder to process.
sigers (list): Signature instances over
``serder.raw``.
Raises:
ValidationError: If the Kevery raises any exception other than
``MissingSignatureError``.
"""
try:
# verify event, update kever state, and escrow if group
self.kvy.processEvent(serder=serder, sigers=sigers)
except MissingSignatureError:
pass
except Exception:
raise ValidationError(f"Improper Habitat event type={serder.ked['t']} for "
f"pre={self.pre}.")
[docs]
def rotate(self, *, smids=None, rmids=None, serder=None, sigers=None, **kwa):
"""Perform a rotation operation and update group member lists.
Delegates the core rotation to ``SignifyHab.rotate``, then updates the
``smids`` and ``rmids`` on both the instance and the persisted
``HabitatRecord``.
Args:
smids (list or None): Updated qb64 prefixes of signing members
after rotation.
rmids (list or None): Updated qb64 prefixes of rotating members
after rotation.
serder (SerderKERI): Pre-built rotation event serder.
sigers (list[Siger]): Siger instances carrying the remote
agent's signatures over ``serder.raw``.
**kwa: passed through to superclass rotate
Raises:
ValidationError: If the habitat record for ``self.pre`` does not
exist in the database.
"""
if (habord := self.db.habs.get(keys=(self.pre,))) is None:
raise ValidationError(f"Missing HabitatRecord for pre={self.pre}")
super(SignifyGroupHab, self).rotate(serder=serder, sigers=sigers, **kwa)
self.smids = smids
self.rmids = rmids
habord.smids = smids
habord.rmids = rmids
self.db.habs.pin(keys=(self.pre,), val=habord)
[docs]
class GroupHab(BaseHab):
"""Local group (multisig) habitat for a given identifier controller.
Provides the local resource environment (hab or habitat) for a multisig
group controller, including dependency injection of the database, keystore,
configuration file, Kevery, and key store Manager.
Signing is performed through the local member hab (``mhab``) by walking
its KEL to locate the correct contributed key material. ``MissingSignatureError``
is silently swallowed during inception and rotation processing so that group
events can be created and stored with a single local member's signature
while awaiting the remaining co-signers' contributions.
Attributes:
ks (keeping.Keeper): LMDB key store. (Injected)
db (basing.Baser): LMDB database for KEL etc. (Injected)
cf (configing.Configer): Configuration file instance. (Injected)
mgr (keeping.Manager): Creates and rotates keys in the key store. (Injected)
rtr (routing.Router): Routes reply ``rpy`` messages. (Injected)
rvy (routing.Revery): Factory that processes reply ``rpy`` messages. (Injected)
kvy (eventing.Kevery): Factory for local processing of local event messages. (Injected)
psr (parsing.Parser): Parses local messages for ``.kvy`` and ``.rvy``. (Injected)
name (str): Alias of the controller.
pre (str): qb64 prefix of the own local controller, or ``None`` if new.
mhab (Hab or None): Local participant member hab of this group hab.
smids (list or None): qb64 prefixes of current signing members of
the group.
rmids (list or None): qb64 prefixes of rotating members of the
group. Defaults to a copy of ``smids`` when not supplied.
temp (bool): ``True`` means testing — uses 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 with respect to
databases; ``False`` means not yet fully initialized.
delpre (str or None): Delegator prefix if any, else ``None``.
kevers (dict): Kever instances from KELs in the local db, keyed by
qb64 prefix. Read-through cache of states for KELs in
``db.states``. (Read-only property)
iserder (serdering.SerderKERI): Own inception event. (Read-only property)
prefixes (oset.OrderedSet): Local prefixes for ``.db``. (Read-only property)
accepted (bool): ``True`` means accepted into the local KEL,
``False`` otherwise. (Read-only property)
"""
[docs]
def __init__(self, smids, mhab=None, rmids=None, **kwa):
"""Initialise a GroupHab instance.
Args:
smids (list[str]): qb64 prefixes of the current signing members of
the multisig group.
mhab (Hab or None): Local participant member hab. The ``mhab.pre``
aid may appear in ``smids``, ``rmids``, or both.
rmids (list or None): qb64 prefixes of the rotating members of
the multisig group. Defaults to ``smids`` when ``None``.
**kwa: Keyword arguments forwarded to ``BaseHab.__init__``,
including all injected dependencies (``ks``, ``db``, ``cf``,
``mgr``, ``rtr``, ``rvy``, ``kvy``, ``psr``), ``name``,
``pre``, and ``temp``.
"""
self.mhab = mhab # local participant Hab of this group hab
self.smids = smids # group signing member aids in this group hab
self.rmids = rmids or smids # group rotating member aids in this group hab
super(GroupHab, self).__init__(**kwa)
[docs]
def incept(self, *, code=MtrDex.Blake3_256, transferable=True, isith=None, nsith=None,
toad=None, wits=None, delpre=None, estOnly=False, DnD=False,
merfers, migers=None, data=None, kind=Kinds.json, version=Version):
"""Finish setting up or making this GroupHab from parameters, including inception.
Assembles the group inception event from the collected member key
material (``merfers``, ``migers``), signs it via the local member hab
(``mhab``), persists the habitat record, and processes the event
through the local Kevery. ``MissingSignatureError`` is silently
swallowed during delegated-identifier initialisation.
Assumes injected dependencies have already been set up.
Args:
code (str): Prefix derivation code. Defaults to
``MtrDex.Blake3_256``.
transferable (bool): ``True`` means the prefix is transferable
(default). ``False`` forces ``nsith='0'`` and
``code=MtrDex.Ed25519N``.
isith (int, str, list, or None): Incepting signing threshold as an
int, hex str, or weighted list. Computed from ``verfers`` when
``None``.
nsith (int, str, list, or None): Next signing threshold as an int,
hex str, or weighted list. Defaults to ``isith`` when ``None``.
toad (int, str, or None): Witness threshold as an int or hex str.
Computed from the number of witnesses when ``None``.
wits (list or None): qb64 prefixes of witnesses, if any.
delpre (str or None): qb64 delegator identifier prefix, if any.
estOnly (bool): ``True`` adds ``TraitDex.EstOnly``, restricting
the KEL to establishment events only. Defaults to ``False``.
DnD (bool): ``True`` adds ``TraitDex.DnD``, disallowing delegated
identifiers from this identifier. Defaults to ``False``.
merfers (list[Verfer]): ``Verfer`` instances of the public signing
keys contributed by each multisig group member. Exactly one
key per member; the zeroth element of each member's key list
by convention.
migers (list or None): ``Diger`` instances of the public
next-key digests contributed by each multisig group member.
``None`` means no pre-rotation material is included.
data (list or None): Seal dicts to embed in the inception event.
kind (str): serialization value of Kinds ("json","cbor","mgpk","cesr")
version (Versionage): version for incept key event message
Raises:
ClosedError: If the key store, database, or config file is not
open.
ConfigurationError: If inception event processing fails for any
reason other than a missing signature.
"""
if not (self.ks.opened and self.db.opened and self.cf.opened):
raise ClosedError("Attempt to make Hab with unopened "
"resources.")
if nsith is None:
nsith = isith
if not transferable:
nsith = '0'
code = MtrDex.Ed25519N
verfers = merfers
digers = migers
serder = super(GroupHab, self).incept(isith=isith,
verfers=verfers,
nsith=nsith,
digers=digers,
code=code,
toad=toad,
wits=wits,
estOnly=estOnly,
DnD=DnD,
delpre=delpre,
data=data,
kind=kind,
version=version)
self.pre = serder.ked["i"] # new pre
# sign handles group hab with .mhab case
sigers = self.sign(ser=serder.raw, verfers=verfers)
habord = HabitatRecord(hid=self.pre,
mid=self.mhab.pre,
name=self.name,
domain=self.ns,
smids=self.smids,
rmids=self.rmids)
self.save(habord)
self.prefixes.add(self.pre)
# 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 ConfigurationError("Improper Habitat inception for "
"pre={} {}".format(self.pre, ex))
self.inited = True
[docs]
def rotate(self, smids=None, rmids=None, serder=None, framed=False,
nested=False, gvrsn=Version, genusify=False, **kwa):
"""Perform a rotation operation and update group member lists.
When ``serder`` is ``None``, delegates entirely to
``BaseHab.rotate(**kwa)`` for a locally-driven rotation. When a
pre-built rotation ``serder`` is provided, the local member hab signs
it, the result is processed through the local Kevery, and the
``smids``/``rmids`` member lists are updated on both the instance and
the persisted ``HabitatRecord``.
Args:
smids (list or None): Updated qb64 prefixes of signing members
after rotation.
rmids (list or None): Updated qb64 prefixes of rotating members
after rotation.
serder (SerderKERI or None): Pre-built rotation event serder. When
``None`` a rotation event is generated by ``BaseHab.rotate``.
framed (bool): True means may assume each message plus its attachments
is isolated as frame when parsing so do not need
attachment group when messagizing
False means may not assume eash message plus its attachments
is isolated as frame when parsing so do need
attachment group when messagizing
nested (bool): True means messagize for non-top level
This forces non-native serializion to be embedded
in non-native group code
False means messagize for top level of stream.
This allows bare non-native serialization of message
gvrsn (Versionage): CESR Genus version for attachment group codes or
nesting group code (useful when serder.gvrsn < 2)
gvrsn = max(svrsn, gvrsn) where svrsn = serder.gvrsn
if serder.gvrsn else serder.pvrsn
genusify (bool): True means prepend genus version code from gvrsn before
serder to override default stream genus version
False means do nothing
**kwa: Keyword arguments forwarded to ``BaseHab.rotate`` when
``serder`` is ``None``.
Returns:
bytearray: Rotation message with attached signatures.
Raises:
ValidationError: If the habitat record for ``self.pre`` does not
exist in the database, or if rotation event processing fails
for any reason other than a missing signature.
"""
if serder is None:
return super(GroupHab, self).rotate(framed=framed, nested=nested,
gvrsn=gvrsn, genusify=genusify,
**kwa)
if (habord := self.db.habs.get(keys=(self.pre,))) is None:
raise ValidationError(f"Missing HabitatRecord for pre={self.pre}")
# 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, framed=framed,
nested=nested, gvrsn=gvrsn, genusify=genusify)
try:
self.kvy.processEvent(serder=serder, sigers=sigers)
except MissingSignatureError:
pass
except Exception as ex:
raise ValidationError("Improper Habitat rotation for "
"pre={self.pre}.") from ex
self.smids = smids
self.rmids = rmids
habord.smids = smids
habord.rmids = rmids
self.db.habs.pin(keys=(self.pre,), val=habord)
return msg
[docs]
def sign(self, ser, verfers=None, indexed=True, rotated=False,
indices=None, ondices=None):
"""Sign a serialisation using the local member hab's key material.
Walks the member hab's (``mhab``) KEL to locate the latest event at
which ``mhab`` contributed signing key material to the group, then
delegates to ``mhab.sign`` with the appropriate current index
(``csi``) and, for rotation events, the optional prior-next other
index (``pni``).
By convention the contributed member key is always the zeroth element
of the member's signing key list, and the contributed member next-key
digest is always the zeroth element of the member's next-key digest
list.
Args:
ser (bytes): Serialisation to sign.
verfers (list or None): ``Verfer`` instances representing
the group's current signing keys. ``None`` means use
``self.kever.verfers``.
indexed (bool): ``True`` means return indexed ``Siger`` instances;
``False`` means return unindexed ``Cigar`` instances.
Defaults to ``True``.
rotated (bool): When ``True``, compute dual-indexed signatures —
the current signing index (``csi``) and the prior-next other
index (``pni``) — for use in a rotation event. When ``False``
(default), only the current signing index is used and ``pni``
is set to ``None``.
indices (list or None): Explicit current signing indices.
Passed through to ``Manager.sign``; computed automatically
when ``None``.
ondices (list or None): Explicit prior-next other
indices. Passed through to ``Manager.sign``; computed
automatically when ``None``.
Returns:
list[Siger] or list[Cigar]: Signature instances over ``ser``.
Raises:
ValueError: If ``mhab`` did not contribute to the group event
identified by ``verfers``.
"""
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 cannot act as witnesses.
Args:
serder (SerderKERI): Ignored.
Raises:
ValueError: Always — group habs are not valid witnesses and cannot
provide witness receipts.
"""
raise ValueError("Attempt to witness by group hab ={self.pre}.")
[docs]
def query(self, pre, src, framed=False, nested=False, gvrsn=Version,
genusify=False, query=None, **kwa):
"""Create, sign, and return a signed ``qry`` query message.
Builds a query event for ``pre`` directed at attester ``src``, then
endorses it through the local member hab (``mhab``) using the last
event in ``mhab``'s KEL.
Args:
pre (str): qb64 identifier prefix being queried for.
src (str): qb64 identifier prefix of the attester being queried.
query (dict or None): Additional query modifiers to include in the
``q`` field. Defaults to an empty dict when ``None``.
**kwa: Keyword arguments forwarded to ``query``.
Returns:
bytearray: Signed query message endorsed by ``mhab``.
"""
query = query if query is not None else dict()
query['i'] = pre
query["src"] = src
serder = eventing.query(pre=self.mhab.pre, query=query, **kwa)
return self.mhab.endorse(serder, last=True, framed=framed, nested=nested,
gvrsn=gvrsn, genusify=genusify)
[docs]
def witnesser(self):
"""Return whether this member hab holds the lowest-index signing key in the group.
The member holding the lowest signing index among all signatures currently
recorded in the database is elected by convention to perform coordination
duties on behalf of the group, such as submitting delegation and witnessing
requests.
Returns:
bool: ``True`` if ``mhab``'s zeroth verfer matches the group signing
key at the lowest recorded signer index; ``False`` otherwise, including
when no signatures are found in the database for the current event.
"""
kever = self.kever
keys = [verfer.qb64 for verfer in kever.verfers]
sigers = self.db.sigs.get(keys=(self.pre, kever.serder.saidb))
if not sigers: # otherwise its a list of sigs
return False
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]