Skip to content

Commit

Permalink
Improve/expand examples; add content/tests to docstrings; add Coveral…
Browse files Browse the repository at this point in the history
…ls badge.
  • Loading branch information
lapets committed Dec 7, 2023
1 parent b81d871 commit baeb25d
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 34 deletions.
26 changes: 16 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ tinybio

Minimal pure-Python library that implements a basic version of a `secure decentralized biometric authentication <https://nillion.pub/decentralized-multifactor-authentication.pdf>`__ functionality via a `secure multi-party computation protocol <https://eprint.iacr.org/2023/1740>`__.

|pypi| |readthedocs| |actions|
|pypi| |readthedocs| |actions| |coveralls|

.. |pypi| image:: https://badge.fury.io/py/tinybio.svg
:target: https://badge.fury.io/py/tinybio
Expand Down Expand Up @@ -47,35 +47,41 @@ Suppose that a workflows is supported by three nodes (parties performing the dec
>>> nodes = [node(), node(), node()]
The preprocessing phase that the nodes must execute can be simulated:
The preprocessing phase that the nodes must execute can be simulated. The second parameter specifies the length of a biometric descriptor (*i.e.*, list of floating point values):

.. code-block:: python
>>> preprocess(nodes, 3)
>>> preprocess(nodes, length=4)
It is then possible to register some data (such as a biometric descriptor represented as a vector of floating point values) by requesting the masks from each node and submitting a registration *token* (*i.e.*, a masked descriptor that is computed locally by the registering party) to the nodes:
.. |token| replace:: ``token``
.. _token: https://tinybio.readthedocs.io/en/0.1.0/_source/tinybio.html#tinybio.tinybio.token

.. |float| replace:: ``float``
.. _float: https://docs.python.org/3/library/functions.html#float

Suppose the client has a biometric descriptor represented as a vector of |float|_ values. The client can create a request for masks and then obtain masks from each node. The client can then locally generate a registration |token|_ (*i.e.*, a masked descriptor that is computed locally by the registering party):

.. code-block:: python
>>> reg_descriptor = [0.5, 0.3, 0.7]
>>> reg_descriptor = [0.5, 0.3, 0.7, 0.1]
>>> reg_masks = [node.masks(request.registration(reg_descriptor)) for node in nodes]
>>> reg_token = token.registration(reg_masks, reg_descriptor)
At a later point, it is possible to perform an authentication workflow. After requesting masks for the authentication descriptor, the authentication token (*i.e.*, a masked descriptor) can be generated locally by the party interested in authenticating itself:
At a later point, the client can perform an authentication workflow. After requesting masks for the authentication descriptor in a manner similar to the above, the client can generate an authentication |token|_ (*i.e.*, a masked descriptor) locally:

.. code-block:: python
>>> auth_descriptor = [0.1, 0.4, 0.8]
>>> auth_descriptor = [0.1, 0.4, 0.8, 0.2]
>>> auth_masks = [node.masks(request.authentication(auth_descriptor)) for node in nodes]
>>> auth_token = token.authentication(auth_masks, auth_descriptor)
Finally, the party interested in authenticating itself can broadcast its original registration token together with its authentication token. Each node then computes locally its share of the authentication result. These shares can be reconstructed by a designated authority to obtain a result:
Finally, the party interested in authenticating itself can broadcast its original registration token together with its authentication token. Each node can then compute locally its share of the authentication result. These shares can be reconstructed by the validating party to obtain the result (*i.e.*, the Euclidean distance between the registration and authentication descriptors):

.. code-block:: python
>>> shares = [node.authenticate([reg_token, auth_token]) for node in nodes]
>>> shares = [node.authenticate(reg_token, auth_token) for node in nodes]
>>> reveal(shares) # Floating point results may differ slightly.
0.42261581368491463
0.43375208257785347
Development
-----------
Expand Down
150 changes: 126 additions & 24 deletions src/tinybio/tinybio.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,55 +41,83 @@ class node(tinynmc.node):
and performing node operations.
Suppose that a workflows is supported by three nodes (parties performing
the decentralized registration and authentication functions). The node
the decentralized registration and authentication functions). The :obj:`node`
objects would be instantiated locally by each of these three parties.
>>> nodes = [node(), node(), node()]
The preprocessing phase that the nodes must execute can be simulated using
the :obj:`preprocess` function. It is assumed that biometric descriptors
used for registration and authentication are represented as lists of
floating point numbers. All such descriptors must be of the same length,
:obj:`float` values. All such descriptors must be of the same length,
and this length must be supplied as the second argument to the
:obj:`preprocess` function.
>>> preprocess(nodes, 3)
>>> preprocess(nodes, length=4)
It is then possible to register some data (*i.e.*, a biometric descriptor
represented as a vector of floating point values) by requesting the masks
from each node and submitting a registration *token* (*i.e.*, a masked
descriptor that is computed locally by the registering party) to the nodes.
It is then possible for a client to register itself by obtaining
a registration :obj:`token`. Suppose the client has a biometric
descriptor represented as a vector of floating point values. The
client can create a :obj:`request` for masks using the
:obj:`request.registration` method.
>>> reg_descriptor = [0.5, 0.3, 0.7, 0.1]
>>> reg_request = request.registration(reg_descriptor)
The client can deliver the request to each node, at which point that node
can locally use its :obj:`~tinynmc.tinynmc.node.masks` method (inherited
from the :obj:`tinynmc.tinynmc.node` class) to generate masks that can
be returned to the requesting client.
>>> reg_masks = [node.masks(reg_request) for node in nodes]
The client can then generate locally a registration :obj:`token` (*i.e.*, a
masked descriptor) via the :obj:`token.registration` method.
>>> reg_descriptor = [0.5, 0.3, 0.7]
>>> reg_masks = [node.masks(request.registration(reg_descriptor)) for node in nodes]
>>> reg_token = token.registration(reg_masks, reg_descriptor)
At a later point, it is possible to perform an authentication workflow.
After requesting masks for the authentication descriptor, the authentication
token (*i.e.*, a masked descriptor) can be generated locally by the party
interested in authenticating itself.
At any later point, it is possible to perform an authentication workflow.
Masks for the authentication descriptor can be requested via a process
that parallels the one for registration (in this case using the
:obj:`request.authentication` method).
>>> auth_descriptor = [0.1, 0.4, 0.8, 0.2]
>>> auth_request = request.authentication(auth_descriptor)
>>> auth_masks = [node.masks(auth_request) for node in nodes]
Given the masks for the authentication descriptor, the authentication
:obj:`token` (*i.e.*, a masked descriptor) can be generated locally by
the client via the :obj:`token.authentication` method.
>>> auth_descriptor = [0.1, 0.4, 0.8]
>>> auth_masks = [node.masks(request.authentication(auth_descriptor)) for node in nodes]
>>> auth_token = token.authentication(auth_masks, auth_descriptor)
Finally, the party interested in authenticating itself can broadcast its
original registration token together with its authentication token. Each
node then computes locally its share of the authentication result. These
shares can be reconstructed by a designated authority to obtain a result.
Finally, the client can broadcast its original registration token together
with its authentication token. Each node can then compute locally its share of
the authentication result. These shares can be reconstructed by the validating
party using the :obj:`reveal` function to obtain the Euclidean distance
between the registration and authentication descriptors.
>>> shares = [node.authenticate([reg_token, auth_token]) for node in nodes]
>>> abs(reveal(shares) - 0.42) <= 0.1 # Use comparison for floating point value.
>>> shares = [node.authenticate(reg_token, auth_token) for node in nodes]
>>> abs(reveal(shares) - 0.43) <= 0.05 # Use comparison for floating point value.
True
"""
def authenticate(
self: node,
tokens: Sequence[dict[tuple[int, int], modulo]]
registration_token: token,
authentication_token: token
) -> modulo:
"""
Perform computation associated with an authentication workflow.
:param registration_token: Registration token to be used in the
local computation by this node.
:param authentication_token: Authentication token to be used in the
local computation by this node.
"""
return self.compute(getattr(self, '_signature'), tokens)
return self.compute(
getattr(self, '_signature'),
[registration_token, authentication_token]
)

class request(list[tuple[int, int]]):
"""
Expand All @@ -99,13 +127,31 @@ class request(list[tuple[int, int]]):
def registration(descriptor: Sequence[float]) -> request:
"""
Encode descriptor into a registration request.
:param descriptor: Biometric descriptor to be used for registration.
This request can be submitted to each node to obtain masks for the
descriptor.
>>> reg_descriptor = [0.5, 0.3, 0.7, 0.1]
>>> isinstance(request.registration(reg_descriptor), request)
True
"""
return request(_encode(descriptor, False).keys())

@staticmethod
def authentication(descriptor: Sequence[float]) -> request:
"""
Encode descriptor into an authentication request.
:param descriptor: Biometric descriptor to be used for authentication.
This request can be submitted to each node to obtain masks for the
descriptor.
>>> auth_descriptor = [0.1, 0.4, 0.8, 0.2]
>>> isinstance(request.authentication(auth_descriptor), request)
True
"""
return request(_encode(descriptor, True).keys())

Expand All @@ -120,6 +166,23 @@ def registration(
) -> token:
"""
Mask descriptor and create a registration token.
:param masks: Collection of masks to be applied to the descriptor.
:param descriptor: Biometric descriptor to be converted into a token.
Suppose masks have already been obtained from the nodes via the steps
below.
>>> nodes = [node(), node(), node()]
>>> preprocess(nodes, 4)
>>> descriptor = [0.5, 0.3, 0.7, 0.1]
>>> masks = [node.masks(request.registration(descriptor)) for node in nodes]
This method can be used to mask the original descriptor (in preparation
for broadcasting it to the nodes).
>>> isinstance(token.registration(masks, descriptor), token)
True
"""
return token(tinynmc.masked_factors(_encode(descriptor, False), masks))

Expand All @@ -130,6 +193,23 @@ def authentication(
) -> token:
"""
Mask descriptor and create an authentication token.
:param masks: Collection of masks to be applied to the descriptor.
:param descriptor: Biometric descriptor to be converted into a token.
Suppose masks have already been obtained from the nodes via the steps
below.
>>> nodes = [node(), node(), node()]
>>> preprocess(nodes, 4)
>>> descriptor = [0.5, 0.3, 0.7, 0.1]
>>> masks = [node.masks(request.authentication(descriptor)) for node in nodes]
This method can be used to mask the original descriptor (in preparation
for broadcasting it to the nodes).
>>> isinstance(token.authentication(masks, descriptor), token)
True
"""
return token(tinynmc.masked_factors(_encode(descriptor, True), masks))

Expand All @@ -138,6 +218,13 @@ def preprocess(nodes: Sequence[node], length: int):
Simulate a preprocessing phase among the collection of nodes for a workflow
that supports registration and authentication descriptor vectors of the
specified length.
:param nodes: Collection of nodes involved in the workflow.
:param length: Number of components in each descriptor list to be used
in the workflow.
>>> nodes = [node(), node(), node()]
>>> preprocess(nodes, length=4)
"""
signature = [1, 1] + ([2] * length)
tinynmc.preprocess(signature, nodes)
Expand All @@ -147,7 +234,22 @@ def preprocess(nodes: Sequence[node], length: int):
def reveal(shares: Iterable[modulo]) -> float:
"""
Reconstruct the result of the overall workflow from its shares and convert
it into a meaningful output (as a percentage).
it into a meaningful output (*i.e.*, the Euclidean distance between the
registration descriptor and the authentication descriptor).
:param shares: Shares that can be reconstructed into a result.
Suppose the shares below are returned from the three nodes in a workflow.
>>> p = 4215209819
>>> shares = [modulo(2042458237, p), modulo(1046840547, p), modulo(1125923365, p)]
This method converts a collection of secret shares from the nodes into
a floating point value representing the Euclidean distance between the
registration and authentication descriptors.
>>> abs(reveal(shares) - 0.43) <= 0.05 # Use comparison for floating point value.
True
"""
return math.sqrt(int(sum(shares)) / (2 ** (2 * _PRECISION)))

Expand Down

0 comments on commit baeb25d

Please sign in to comment.