{ "cells": [ { "cell_type": "raw", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ ".. _active:\n", "\n", "Active Adversaries\n", "------------------\n", "\n", ":class:`Additive secret sharing ` and :ref:`Shamir secret sharing ` are popular sharing schemes provided by Cicada, but are secure only against semi-honest (also known as \"honest but curious\") adversaries. With this security model, private information is secure against disclosure, but a malicious player (an \"active adversary\") could deliberately alter a calculation to produce an invalid result, and none of the other players would know.\n", "\n", "As a more robust (but slower) alternative, Cicada provides :class:`~cicada.active.ActiveProtocolSuite`, which provides honest majority security with abort. In this case, private information is still secure against disclosure, *and* tampering with a calculation can be detected by an honest player, so long as there are less than :math:`\\frac{n}{2}` dishonest players.\n", "\n", ":class:`~cicada.active.ActiveProtocolSuite` supports the same mathematical and logical operations as :class:`~cicada.additive.AdditiveProtocolSuite` and :class:`~cicada.shamir.ShamirProtocolSuite`, and its secret shares are implemented using a combination of additive and Shamir secret sharing. The security against malicious alteration in this case is derived from the fact that attempts to alter both types of share in parallel give rise to inconsistencies that can be detected.\n", "\n", "Note that the *identity* of dishonest players may not be detectable by this scheme, nor is there any way to recover a correct value from the computation - the only resort for honest players is to discontinue the computation (abort) and restart it with a different (hopefully honest) group of players.\n", "\n", "Let's begin with an example of sharing and revealing a value, without any tampering, using additive secret sharing:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:Player 0 revealed: 42.0\n", "INFO:root:Player 1 revealed: 42.0\n", "INFO:root:Player 2 revealed: 42.0\n" ] } ], "source": [ "import logging\n", "logging.basicConfig(level=logging.INFO)\n", "\n", "import numpy\n", "\n", "from cicada.additive import AdditiveProtocolSuite\n", "from cicada.communicator import SocketCommunicator\n", "from cicada.logger import Logger\n", "\n", "def main(communicator):\n", " log = Logger(logging.getLogger(), communicator)\n", " protocol = AdditiveProtocolSuite(communicator)\n", "\n", " secret = numpy.array(42) if communicator.rank == 0 else None\n", " secret_share = protocol.share(src=0, secret=secret, shape=())\n", "\n", " result = protocol.reveal(secret_share)\n", " log.info(f\"Player {communicator.rank} revealed: {result}\")\n", "\n", "SocketCommunicator.run(world_size=3, fn=main);" ] }, { "cell_type": "raw", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "This is all very straightforward, but let's see what happens when player 1 tampers with its share of the secret so that the revealed value increases by one:\n", "\n", ".. note::\n", "\n", " Keep in mind that, no matter what player 1 does, the secret itself is still secure - although the tampering will cause the revealed value to increase by 1, the value itself isn't known, until it's explicitly revealed. \n", "\n", ".. warning::\n", "\n", " The following code uses the `storage` attribute to directly modify secret shares; under normal circumstances, you should *never* modify secret shares in this way, regardless of protocol suite. We do it here to model adversarial behavior only." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:Player 0 revealed: 43.0\n", "INFO:root:Player 1 revealed: 43.0\n", "INFO:root:Player 2 revealed: 43.0\n" ] } ], "source": [ "def main(communicator):\n", " log = Logger(logging.getLogger(), communicator)\n", " protocol = AdditiveProtocolSuite(communicator)\n", "\n", " secret = numpy.array(42) if communicator.rank == 0 else None\n", " secret_share = protocol.share(src=0, secret=secret, shape=())\n", "\n", " # Evil player behavior here!\n", " if communicator.rank == 1:\n", " secret_share.storage += 65536\n", " \n", " result = protocol.reveal(secret_share)\n", " log.info(f\"Player {communicator.rank} revealed: {result}\")\n", "\n", "SocketCommunicator.run(world_size=3, fn=main);" ] }, { "cell_type": "raw", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "Note that player 1 has successfully altered its share so that the revealed value is 43 instead of 42, and that - in the general case - the other players would have no way of knowing that the tampering occurred.\n", "\n", ".. note::\n", "\n", " For this trivial example, player 0 might notice that the revealed value doesn't match the value that they originally provided, but for the vast majority of use-cases involving actual computation, this won't be possible.\n", "\n", "Now, let's try the same experiment, but using :class:`~cicada.active.ActiveProtocolSuite` instead:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "ERROR:cicada.communicator.socket:Comm world player 0 failed: ConsistencyError('Secret Shares are inconsistent in the first stage')\n", "ERROR:cicada.communicator.socket:Comm world player 1 failed: ConsistencyError('Secret Shares are inconsistent in the first stage')\n", "ERROR:cicada.communicator.socket:Comm world player 2 failed: ConsistencyError('Secret Shares are inconsistent in the first stage')\n" ] } ], "source": [ "from cicada.active import ActiveProtocolSuite\n", "\n", "def main(communicator):\n", " log = Logger(logging.getLogger(), communicator)\n", " protocol = ActiveProtocolSuite(communicator, threshold=2)\n", "\n", " secret = numpy.array(42) if communicator.rank == 0 else None\n", " active_share = protocol.share(src=0, secret=secret, shape=())\n", "\n", " # Evil player behavior here!\n", " if communicator.rank == 1:\n", " additive_share, shamir_share = active_share.storage\n", " additive_share.storage += 65536\n", " \n", " result = protocol.reveal(active_share)\n", " log.info(f\"Player {communicator.rank} revealed: {result}\")\n", "\n", "SocketCommunicator.run(world_size=3, fn=main);" ] }, { "cell_type": "raw", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "In this case, :class:`~cicada.active.ActiveProtocolSuite` detects the tampering as soon as the players attempt to reveal the secret, and raises a :class:`cicada.active.ConsistencyError` exception. Checking for tampering when secrets are revealed keeps the cost of tamper detection low by amortizing it across the entire computation, while ensuring that invalid results cannot go undetected or ignored.\n", "\n", "Players can also perform explicit checks for tampering instead of waiting for reveal: this can be useful to checkpoint correct results and/or avoid wasted effort by aborting a long-running computation if tampering is detected:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "ERROR:root:Player 0 detected tampering!\n", "ERROR:root:Player 1 detected tampering!\n", "ERROR:root:Player 2 detected tampering!\n" ] } ], "source": [ "def main(communicator):\n", " log = Logger(logging.getLogger(), communicator)\n", " protocol = ActiveProtocolSuite(communicator, threshold=2)\n", "\n", " secret = numpy.array(42) if communicator.rank == 0 else None\n", " active_share = protocol.share(src=0, secret=secret, shape=())\n", "\n", " # Evil player behavior here!\n", " if communicator.rank == 1:\n", " additive_share, shamir_share = active_share.storage\n", " additive_share.storage += 65536\n", "\n", " # Explicitly test for tampering here.\n", " if not protocol.verify(active_share):\n", " log.error(f\"Player {communicator.rank} detected tampering!\")\n", " return\n", " \n", " result = protocol.reveal(actove_share)\n", " log.info(f\"Player {communicator.rank} revealed: {result}\")\n", "\n", "SocketCommunicator.run(world_size=3, fn=main);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's attempt a more subtle tamper:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "ERROR:cicada.communicator.socket:Comm world player 0 failed: ValueError(\"Expected numpy.ndarray, got .\")\n", "ERROR:cicada.communicator.socket:Comm world player 1 failed: Timeout('Tag ALLGATHER from player 0 timed-out after 5s')\n", "ERROR:cicada.communicator.socket:Comm world player 2 failed: Timeout('Tag ALLGATHER from player 0 timed-out after 5s')\n" ] } ], "source": [ "def main(communicator):\n", " log = Logger(logging.getLogger(), communicator)\n", " protocol = ActiveProtocolSuite(communicator, threshold=2)\n", "\n", " secret = numpy.array(42) if communicator.rank == 0 else None\n", " active_share = protocol.share(src=0, secret=secret, shape=())\n", "\n", " # Evil player behavior here!\n", " if communicator.rank == 0:\n", " additive_share, shamir_share = active_share.storage\n", " \n", " modulus = 2**64-59\n", " additive_share.storage += 1\n", " #shamir_share.storage = (shamir_share.storage + pow(5, modulus-2, modulus)) % modulus\n", " shamir_share.storage = (shamir_share.storage + pow(3, modulus-2, modulus)) % modulus\n", "\n", "# # Explicitly test for tampering here.\n", "# if not protocol.verify(active_share):\n", "# log.error(f\"Player {communicator.rank} detected tampering!\")\n", "# return\n", " \n", " result = protocol.reveal(active_share)\n", " log.info(f\"Player {communicator.rank} revealed: {result}\")\n", "\n", "SocketCommunicator.run(world_size=3, fn=main);" ] }, { "cell_type": "raw", "metadata": { "raw_mimetype": "text/restructuredtext" }, "source": [ "Since :class:`~cicada.active.ActiveProtocolSuite` provides the same operations as the additive and Shamir suites, you can use it to perform any computation that you would have performed with the latter, but with detection for the influences of active malicious adversaries. The cost of this security is roughly equal to the sum of the computational and communication cost of the additive and Shamir protocol suites individually. " ] } ], "metadata": { "celltoolbar": "Raw Cell Format", "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.2" } }, "nbformat": 4, "nbformat_minor": 4 }