├── .gitignore ├── CITATION.cff ├── LICENSE ├── README.md ├── pyproject.toml ├── scripts └── bench_batch_size.py ├── src └── tno │ └── mpc │ └── protocols │ └── distributed_keygen │ ├── __init__.py │ ├── distributed_keygen.py │ ├── paillier_shared_key.py │ ├── py.typed │ ├── test │ ├── __init__.py │ ├── conftest.py │ ├── test_data │ │ ├── distributed_key_threshold_0_3parties_0.obj │ │ ├── distributed_key_threshold_0_3parties_1.obj │ │ ├── distributed_key_threshold_0_3parties_2.obj │ │ ├── distributed_key_threshold_0_4parties_0.obj │ │ ├── distributed_key_threshold_0_4parties_1.obj │ │ ├── distributed_key_threshold_0_4parties_2.obj │ │ ├── distributed_key_threshold_0_4parties_3.obj │ │ ├── distributed_key_threshold_0_5parties_0.obj │ │ ├── distributed_key_threshold_0_5parties_1.obj │ │ ├── distributed_key_threshold_0_5parties_2.obj │ │ ├── distributed_key_threshold_0_5parties_3.obj │ │ ├── distributed_key_threshold_0_5parties_4.obj │ │ ├── distributed_key_threshold_1_3parties_0.obj │ │ ├── distributed_key_threshold_1_3parties_1.obj │ │ ├── distributed_key_threshold_1_3parties_2.obj │ │ ├── distributed_key_threshold_1_4parties_0.obj │ │ ├── distributed_key_threshold_1_4parties_1.obj │ │ ├── distributed_key_threshold_1_4parties_2.obj │ │ ├── distributed_key_threshold_1_4parties_3.obj │ │ ├── distributed_key_threshold_1_5parties_0.obj │ │ ├── distributed_key_threshold_1_5parties_1.obj │ │ ├── distributed_key_threshold_1_5parties_2.obj │ │ ├── distributed_key_threshold_1_5parties_3.obj │ │ └── distributed_key_threshold_1_5parties_4.obj │ ├── test_distributed_keygen.py │ └── test_serialization.py │ └── utils.py └── stubs └── sympy ├── __init__.pyi └── ntheory ├── __init__.pyi ├── generate.pyi └── residue_ntheory.pyi /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,pycharm 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,pycharm 3 | 4 | ### PyCharm ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | .idea/* 9 | 10 | # CMake 11 | cmake-build-*/ 12 | 13 | # Mongo Explorer plugin 14 | .idea/**/mongoSettings.xml 15 | 16 | # File-based project format 17 | *.iws 18 | 19 | # IntelliJ 20 | out/ 21 | 22 | # mpeltonen/sbt-idea plugin 23 | .idea_modules/ 24 | 25 | # JIRA plugin 26 | atlassian-ide-plugin.xml 27 | 28 | # Cursive Clojure plugin 29 | .idea/replstate.xml 30 | 31 | # SonarLint plugin 32 | .idea/sonarlint/ 33 | 34 | # Crashlytics plugin (for Android Studio and IntelliJ) 35 | com_crashlytics_export_strings.xml 36 | crashlytics.properties 37 | crashlytics-build.properties 38 | fabric.properties 39 | 40 | # Editor-based Rest Client 41 | .idea/httpRequests 42 | 43 | # Android studio 3.1+ serialized cache file 44 | .idea/caches/build_file_checksums.ser 45 | 46 | ### PyCharm Patch ### 47 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 48 | 49 | # *.iml 50 | # modules.xml 51 | # .idea/misc.xml 52 | # *.ipr 53 | 54 | # Sonarlint plugin 55 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 56 | .idea/**/sonarlint/ 57 | 58 | # SonarQube Plugin 59 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 60 | .idea/**/sonarIssues.xml 61 | 62 | # Markdown Navigator plugin 63 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 64 | .idea/**/markdown-navigator.xml 65 | .idea/**/markdown-navigator-enh.xml 66 | .idea/**/markdown-navigator/ 67 | 68 | # Cache file creation bug 69 | # See https://youtrack.jetbrains.com/issue/JBR-2257 70 | .idea/$CACHE_FILE$ 71 | 72 | # CodeStream plugin 73 | # https://plugins.jetbrains.com/plugin/12206-codestream 74 | .idea/codestream.xml 75 | 76 | # Azure Toolkit for IntelliJ plugin 77 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij 78 | .idea/**/azureSettings.xml 79 | 80 | ### Python ### 81 | # Byte-compiled / optimized / DLL files 82 | __pycache__/ 83 | *.py[cod] 84 | *$py.class 85 | 86 | # C extensions 87 | *.so 88 | 89 | # Distribution / packaging 90 | .Python 91 | build/ 92 | develop-eggs/ 93 | dist/ 94 | downloads/ 95 | eggs/ 96 | .eggs/ 97 | lib/ 98 | lib64/ 99 | parts/ 100 | sdist/ 101 | var/ 102 | wheels/ 103 | share/python-wheels/ 104 | *.egg-info/ 105 | .installed.cfg 106 | *.egg 107 | MANIFEST 108 | 109 | # PyInstaller 110 | # Usually these files are written by a python script from a template 111 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 112 | *.manifest 113 | *.spec 114 | 115 | # Installer logs 116 | pip-log.txt 117 | pip-delete-this-directory.txt 118 | 119 | # Unit test / coverage reports 120 | htmlcov/ 121 | .tox/ 122 | .nox/ 123 | .coverage 124 | .coverage.* 125 | .cache 126 | nosetests.xml 127 | coverage.xml 128 | *.cover 129 | *.py,cover 130 | .hypothesis/ 131 | .pytest_cache/ 132 | cover/ 133 | 134 | # Translations 135 | *.mo 136 | *.pot 137 | 138 | # Django stuff: 139 | *.log 140 | local_settings.py 141 | db.sqlite3 142 | db.sqlite3-journal 143 | 144 | # Flask stuff: 145 | instance/ 146 | .webassets-cache 147 | 148 | # Scrapy stuff: 149 | .scrapy 150 | 151 | # Sphinx documentation 152 | docs/_build/ 153 | 154 | # PyBuilder 155 | .pybuilder/ 156 | target/ 157 | 158 | # Jupyter Notebook 159 | .ipynb_checkpoints 160 | 161 | # IPython 162 | profile_default/ 163 | ipython_config.py 164 | 165 | # pyenv 166 | # For a library or package, you might want to ignore these files since the code is 167 | # intended to run in multiple environments; otherwise, check them in: 168 | # .python-version 169 | 170 | # pipenv 171 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 172 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 173 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 174 | # install all needed dependencies. 175 | #Pipfile.lock 176 | 177 | # poetry 178 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 179 | # This is especially recommended for binary packages to ensure reproducibility, and is more 180 | # commonly ignored for libraries. 181 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 182 | #poetry.lock 183 | 184 | # pdm 185 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 186 | #pdm.lock 187 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 188 | # in version control. 189 | # https://pdm.fming.dev/#use-with-ide 190 | .pdm.toml 191 | 192 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 193 | __pypackages__/ 194 | 195 | # Celery stuff 196 | celerybeat-schedule 197 | celerybeat.pid 198 | 199 | # SageMath parsed files 200 | *.sage.py 201 | 202 | # Environments 203 | .env 204 | .venv 205 | env/ 206 | venv/ 207 | ENV/ 208 | env.bak/ 209 | venv.bak/ 210 | 211 | # Spyder project settings 212 | .spyderproject 213 | .spyproject 214 | 215 | # Rope project settings 216 | .ropeproject 217 | 218 | # mkdocs documentation 219 | /site 220 | 221 | # mypy 222 | .mypy_cache/ 223 | .dmypy.json 224 | dmypy.json 225 | 226 | # Pyre type checker 227 | .pyre/ 228 | 229 | # pytype static type analyzer 230 | .pytype/ 231 | 232 | # Cython debug symbols 233 | cython_debug/ 234 | 235 | # PyCharm 236 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 237 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 238 | # and can be added to the global gitignore or merged into this file. For a more nuclear 239 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 240 | #.idea/ 241 | 242 | ### Python Patch ### 243 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 244 | poetry.toml 245 | 246 | # ruff 247 | .ruff_cache/ 248 | 249 | # LSP config files 250 | pyrightconfig.json 251 | 252 | ### VisualStudioCode ### 253 | .vscode/* 254 | 255 | # Local History for Visual Studio Code 256 | .history/ 257 | 258 | # Built Visual Studio Code Extensions 259 | *.vsix 260 | 261 | ### VisualStudioCode Patch ### 262 | # Ignore all local history of files 263 | .history 264 | .ionide 265 | 266 | # End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,pycharm 267 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | license: Apache-2.0 3 | message: If you use this software, please cite it using these metadata. 4 | authors: 5 | - name: TNO PET Lab 6 | city: The Hague 7 | country: NL 8 | email: petlab@tno.nl 9 | website: https://pet.tno.nl 10 | type: software 11 | url: https://pet.tno.nl 12 | contact: 13 | - name: TNO PET Lab 14 | city: The Hague 15 | country: NL 16 | email: petlab@tno.nl 17 | website: https://pet.tno.nl 18 | repository-code: https://github.com/TNO-MPC/protocols.distributed_keygen 19 | repository-artifact: https://pypi.org/project/tno.mpc.protocols.distributed_keygen 20 | title: TNO PET Lab - secure Multi-Party Computation (MPC) - Protocols - Distributed Key Generation 21 | version: 4.2.2 22 | date-released: 2024-11-29 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021-2024 The Netherlands Organisation for Applied Scientific Research (TNO) 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TNO PET Lab - secure Multi-Party Computation (MPC) - Protocols - Distributed Key Generation 2 | 3 | An implementation of a semi-honest distributed key generation for the Paillier 4 | Encryption Scheme, resulting in a public key and a shared secret key. A detailed 5 | description of the protocol can be found in the accompanying paper: [An 6 | implementation of the Paillier crypto system with threshold decryption without 7 | a trusted dealer](https://eprint.iacr.org/2019/1136.pdf). 8 | 9 | ### PET Lab 10 | 11 | The TNO PET Lab consists of generic software components, procedures, and functionalities developed and maintained on a regular basis to facilitate and aid in the development of PET solutions. The lab is a cross-project initiative allowing us to integrate and reuse previously developed PET functionalities to boost the development of new protocols and solutions. 12 | 13 | The package `tno.mpc.protocols.distributed_keygen` is part of the [TNO Python Toolbox](https://github.com/TNO-PET). 14 | 15 | _Limitations in (end-)use: the content of this software package may solely be used for applications that comply with international export control laws._ 16 | _This implementation of cryptographic software has not been audited. Use at your own risk._ 17 | 18 | ## Documentation 19 | 20 | Documentation of the `tno.mpc.protocols.distributed_keygen` package can be found 21 | [here](https://docs.pet.tno.nl/mpc/protocols/distributed_keygen/4.2.2). 22 | 23 | ## Install 24 | 25 | Easily install the `tno.mpc.protocols.distributed_keygen` package using `pip`: 26 | 27 | ```console 28 | $ python -m pip install tno.mpc.protocols.distributed_keygen 29 | ``` 30 | 31 | _Note:_ If you are cloning the repository and wish to edit the source code, be 32 | sure to install the package in editable mode: 33 | 34 | ```console 35 | $ python -m pip install -e 'tno.mpc.protocols.distributed_keygen' 36 | ``` 37 | 38 | If you wish to run the tests you can use: 39 | 40 | ```console 41 | $ python -m pip install 'tno.mpc.protocols.distributed_keygen[tests]' 42 | ``` 43 | _Note:_ A significant performance improvement can be achieved by installing the GMPY2 library. 44 | 45 | ```console 46 | $ python -m pip install 'tno.mpc.protocols.distributed_keygen[gmpy]' 47 | ``` 48 | 49 | ## Protocol description 50 | 51 | A more elaborate protocol description can be found in [An implementation of the Paillier crypto system with threshold decryption without a trusted dealer](https://eprint.iacr.org/2019/1136.pdf). 52 | 53 | ## Usage 54 | 55 | The distributed keygen module can be used by first creating a `Pool` 56 | from the `tno.mpc.communication` library. 57 | 58 | ```python 59 | from tno.mpc.communication.pool import Pool 60 | 61 | pool = Pool(...) # initialize pool with ips etc 62 | ``` 63 | 64 | ### Starting the protocol 65 | 66 | After initializing a pool, you can use the class method `DistributedPaillier.from_security_parameter()` to create an instance of the `DistributedPaillier` class. The class method automatically starts the protocol between the parties inside the pool to jointly generate a public key and a shared secret key. 67 | 68 | Under `Appendix` at the end of this README, you can find 3 files: 69 | 70 | - `distributed_keygen_example_local.py`: this script runs the protocol in one python instance on different ports of the same machine. 71 | - `distributed_keygen_example_distributed.py`: this script runs the protocol for one machine only and this script should be run on each machine. 72 | - `run_protocol.sh`: this batch script takes one parameter, the number of parties, and starts `distributed_keygen_example_distributed.py` with the right arguments for each machine on `localhost`. 73 | 74 | There are a couple of parameters that need to be passed to the class method `DistributedPaillier.from_security_parameter()`. We list them here and provide information on how to choose the right values. 75 | 76 | - `pool`: This pool should be initialised for each party (one pool per party). See the documentation for `tno.mpc.communication.pool` for more information. 77 | - `corruption_threshold`: This is the `t` in `t-out-of-n` secret sharing. The secret sharing is used to distribute the secret key. We require a dishonest minority, so we require for the 78 | number of parties in the pool and the corruption threshold that `number_of_parties >= 2 * corruption_threshold + 1`. The default value is `1`. 79 | - `key_length`: This is the bit length of the biprime `N` used in the modulus of the scheme. The safety is similar to that of RSA, so typical values are `1024`, `2048` and `4096`. However, this comes at a performance cost. If you simply wish to play around with the code, we recommend using `128`, so the protocol will on average finish in under 1 minute. We stress that this is _NOT_ safe and should never be done in production environments. The default value is `2048`. 80 | - `prime_threshold`: This is an upper bound on the prime values that are checked before the expensive biprimality test is run. A higher value means that bad candidates are discarded faster. The default value is `2000`. 81 | - `correct_param_biprime`: This parameter determines the certainty level that the produced `N` is indeed the product of 2 primes. The value indicates the number of random values that are sampled and checked. The probability that a check passes, but `N` is not biprime is less than 0.5, so the probability that `N` is not biprime is less than `2**(-correct_param_biprime)`. The default value is `40`. 82 | - `stat_sec_shamir`: security parameter for the shamir secret sharing over the integers. The higher this parameter, the larger the interval of random masking values will be and the smaller the statistical distance from uniform will be. The default value is `40`. 83 | - `distributed`: This value determines how the resulting `DistributedPaillier` instance is stored. When the protocol is run within 1 python instance (such as in `distributed_keygen_example_local.py`), this value should be set to `False` and if each party uses their own python instance, this should be set to `True`. The default value is `True`. 84 | - `precision`: This determines the fixed-point precision of the computations in the resulting encryption scheme. A precision of `n` gives `n` decimals behind the comma of precision. 85 | 86 | ### After initialization 87 | 88 | When a DistributedPaillier instance has been generated (either locally or distributedly), the public key can be used to encrypt messages and the shared secret key 89 | can be used to distributively decrypt. Note that these methods are async methods, so they either 90 | need to be run in an event loop or inside another async method using await. 91 | 92 | In the following example we show how to use this library to make computations using a scheme that is distributed over 3 93 | parties ("party1", "party2", and "party3"). We show the code for all 3 parties and assume that a `distributed_scheme` 94 | has already been generated by the parties. 95 | 96 | Note that in order to decrypt, the ciphertext must be known to all parties. Also, all parties must participate in the 97 | decryption, even in the case that they do not receive any other shares or the result. 98 | 99 | _Beware: When sending a ciphertext to more than one party, the method `pool.broadcast()` MUST be used. When using 100 | `pool.send()` the parties will receive different ciphertexts due to intermediate re-randomization. For more details on 101 | why this happens read the text below the examples._ 102 | 103 | ```python 104 | # Party 1 105 | 106 | # The assumption here is that this code is placed inside an async method 107 | ciphertext = distributed_scheme.encrypt(42) # encryption of 42 108 | await distributed_scheme.pool.send("party2", ciphertext, msg_id="step1") # send the ciphertext to party 2 109 | 110 | final_ciphertext = await distributed_scheme.recv("party3", msg_id="step3") # receive the ciphertext from party 3 111 | 112 | # all parties need to participate in the decryption protocol 113 | plaintext = await distributed_scheme.decrypt(final_ciphertext) 114 | assert plaintext == 426 115 | 116 | # alternative decryption of which the shares (and result) are only obtained by party 2 117 | # note: even though we do not receive the result, we are required to participate 118 | await distributed_scheme.decrypt(final_ciphertext, receivers=["party2"]) 119 | ``` 120 | 121 | ```python 122 | # Party 2 123 | 124 | # The assumption here is that this code is placed inside an async method 125 | ciphertext = await distributed_scheme.pool.recv("party1", msg_id="step1") # receive the ciphertext from party 1 126 | 127 | ciphertext += 100 # add 100 to the ciphertext (value is now 142) 128 | await distributed_scheme.pool.send("party3", ciphertext, msg_id="step2") # send the updated ciphertext to party 3 129 | 130 | final_ciphertext = await distributed_scheme.recv("party3", msg_id="step3") # recieve the ciphertext from party 3 131 | 132 | # all parties need to participate in the decryption protocol 133 | plaintext = await distributed_scheme.decrypt(final_ciphertext) 134 | assert plaintext == 426 135 | 136 | # alternative decryption of which the shares (and result) are only obtained by party 2 137 | # note: even though we do not receive the result, we are required to participate 138 | plaintext = await distributed_scheme.decrypt(final_ciphertext, receivers=["self"]) 139 | assert plaintext == 426 140 | ``` 141 | 142 | ```python 143 | # Party 3 144 | 145 | # The assumption here is that this code is placed inside an async method 146 | final_ciphertext = await distributed_scheme.pool.recv("party2", msg_id="step2") # receive the ciphertext from party 1 147 | 148 | final_ciphertext *= 3 # multiply the ciphertext by 3 (value is now 426) 149 | # send the ciphertext to multiple parties (we cannot use `pool.send` now). 150 | distributed_scheme.pool.broadcast(final_ciphertext, msg_id="step3", handler_names=["party1", "party2"]) # receivers=None does the same 151 | 152 | # all parties need to participate in the decryption protocol 153 | plaintext = await distributed_scheme.decrypt(final_ciphertext) 154 | assert plaintext == 426 155 | 156 | # alternative decryption of which the shares (and result) are only obtained by party 2 157 | # note: even though we do not receive the result, we are required to participate 158 | await distributed_scheme.decrypt(final_ciphertext, receivers=["party2"]) 159 | ``` 160 | 161 | Running this example will show several warnings. The remainder of this documentation explains why the warnings are issued and how to get rid of them depending on the users' preferences. 162 | 163 | ## Fresh and unfresh ciphertexts 164 | 165 | An encrypted message is called a ciphertext. A ciphertext in the current package has a property `is_fresh` that indicates whether this ciphertext has fresh randomness, in which case it can be communicated to another player securely. More specifically, a ciphertext `c` is fresh if another user, knowledgeable of all prior communication and all current ciphertexts marked as fresh, cannot deduce any more private information from learning `c`. 166 | 167 | The package understands that the freshness of the result of a homomorphic operation depends on the freshness of the inputs, and that the homomorphic operation renders the inputs unfresh. For example, if `c1` and `c2` are fresh ciphertexts, then `c12 = c1 + c2` is marked as a fresh encryption (no rerandomization needed) of the sum of the two underlying plaintexts. After the operation, ciphertexts `c1` and `c2` are no longer fresh. 168 | 169 | The fact that `c1` and `c2` were both fresh implies that, at some point, we randomized them. After the operation `c12 = c1 + c2`, only `c12` is fresh. This implies that one randomization was lost in the process. In particular, we wasted resources. An alternative approach was to have unfresh `c1` and `c2` then compute the unfresh result `c12` and only randomize that ciphertext. This time, no resources were wasted. The package issues a warning to inform the user this and similar efficiency opportunities. 170 | 171 | The package integrates naturally with `tno.mpc.communication` and if that is used for communication, its serialization logic will ensure that all sent ciphertexts are fresh. A warning is issued if a ciphertext was randomized in the proces. A ciphertext is always marked as unfresh after it is serialized. Similarly, all received ciphertexts are considered unfresh. 172 | 173 | ## Tailor behavior to your needs 174 | 175 | The crypto-neutral developer is facilitated by the package as follows: the package takes care of all bookkeeping, and the serialization used by `tno.mpc.communication` takes care of all randomization. The warnings can be [disabled](#warnings) for a smoother experience. 176 | 177 | The eager crypto-youngster can improve their understanding and hone their skills by learning from the warnings that the package provides in a safe environment. The package is safe to use when combined with `tno.mpc.communication`. It remains to be safe while you transform your code from 'randomize-early' (fresh encryptions) to 'randomize-late' (unfresh encryptions, randomize before exposure). At that point you have optimized the efficiency of the library while ensuring that all exposed ciphertexts are fresh before they are serialized. In particular, you no longer rely on our serialization for (re)randomizing your ciphertexts. 178 | 179 | Finally, the experienced cryptographer can turn off warnings / turn them into exceptions, or benefit from the `is_fresh` flag for own purposes (e.g. different serializer or communication). 180 | 181 | ### Warnings 182 | 183 | By default, the `warnings` package prints only the first occurence of a warning for each location (module + line number) where the warning is issued. The user may easily [change this behaviour](https://docs.python.org/3/library/warnings.html#the-warnings-filter) to never see warnings: 184 | 185 | ```python 186 | from tno.mpc.encryption_schemes.paillier import EncryptionSchemeWarning 187 | 188 | warnings.simplefilter("ignore", EncryptionSchemeWarning) 189 | ``` 190 | 191 | Alternatively, the user may pass `"once"`, `"always"` or even `"error"`. 192 | 193 | Finally, note that some operations issue two warnings, e.g. `c1-c2` issues a warning for computing `-c2` and a warning for computing `c1 + (-c2)`. 194 | 195 | ### Advanced usage 196 | 197 | The basic usage in the example above can be improved upon by explicitly randomizing as late as possible, i.e. by 198 | only randomizing non-fresh ciphertexts directly before they are communicated using the `randomize()` method. 199 | 200 | ### Speed-up encrypting and randomizing 201 | 202 | Encrypting messages and randomizing ciphertexts is an involved operation that requires randomly generating large values and processing them in some way. This process can be sped up which will boost the performance of your script or package. The base package `tno.mpc.encryption_schemes.paillier` provides several ways to more quickly generate randomness. We refer to [the documentation of `tno.mpc.encryption_schemes.paillier`](https://ci.tno.nl/gitlab/pet/lab/mpc/python-packages/microlibs/encryption_schemes/microlibs/paillier/-/blob/master/README.md#speed-up-encrypting-and-randomizing) for more information and examples on this part. The information there directly translates to this package. 203 | 204 | ## Benchmarks 205 | 206 | The repository includes a benchmark script which generates the graphs as they appear in the paper: [An implementation of the Paillier crypto system with threshold decryption without a trusted dealer](https://eprint.iacr.org/2019/1136.pdf). 207 | 208 | To use the script, first install the "bench" dependency group: 209 | - `python -m pip install ".[bench]"` 210 | 211 | For information on how to use the script, type: 212 | - `python ./scripts/bench_batch_size.py --help` 213 | 214 | ## Appendix 215 | 216 | _NOTE_: If you want to run `distributed_keygen_example_local.py` in a Jupyter Notebook, you will run into the issue that the event loop is already running upon calling `run_until_complete`. 217 | In this case, you should add the following code to the top of the notebook: 218 | 219 | ```python 220 | import nest_asyncio 221 | nest_asyncio.apply() 222 | ``` 223 | 224 | distributed_keygen_example_local.py: 225 | 226 | ```python 227 | import asyncio 228 | from typing import List 229 | 230 | from tno.mpc.communication import Pool 231 | 232 | from tno.mpc.protocols.distributed_keygen import DistributedPaillier 233 | 234 | corruption_threshold = 1 # corruption threshold 235 | key_length = 128 # bit length of private key 236 | prime_thresh = 2000 # threshold for primality check 237 | correct_param_biprime = 40 # correctness parameter for biprimality test 238 | stat_sec_shamir = ( 239 | 40 # statistical security parameter for secret sharing over the integers 240 | ) 241 | 242 | PARTIES = 4 # number of parties that will be involved in the protocol, you can change this to any number you like 243 | 244 | 245 | def setup_local_pool(server_port: int, ports: List[int]) -> Pool: 246 | pool = Pool() 247 | pool.add_http_server(server_port) 248 | for client_port in (port for port in ports if port != server_port): 249 | pool.add_http_client(f"client{client_port}", "localhost", client_port) 250 | return pool 251 | 252 | 253 | local_ports = [3000 + i for i in range(PARTIES)] 254 | local_pools = [ 255 | setup_local_pool(server_port, local_ports) for server_port in local_ports 256 | ] 257 | 258 | loop = asyncio.get_event_loop() 259 | async_coroutines = [ 260 | DistributedPaillier.from_security_parameter( 261 | pool, 262 | corruption_threshold, 263 | key_length, 264 | prime_thresh, 265 | correct_param_biprime, 266 | stat_sec_shamir, 267 | distributed=False, 268 | ) 269 | for pool in local_pools 270 | ] 271 | print("Starting distributed key generation protocol.") 272 | distributed_paillier_schemes = loop.run_until_complete( 273 | asyncio.gather(*async_coroutines) 274 | ) 275 | print("The protocol has completed.") 276 | ``` 277 | 278 | distributed_keygen_example_distributed.py: 279 | 280 | ```python 281 | import argparse 282 | import asyncio 283 | from typing import List, Tuple 284 | 285 | from tno.mpc.communication import Pool 286 | 287 | from tno.mpc.protocols.distributed_keygen import DistributedPaillier 288 | 289 | corruption_threshold = 1 # corruption threshold 290 | key_length = 128 # bit length of private key 291 | prime_thresh = 2000 # threshold for primality check 292 | correct_param_biprime = 40 # correctness parameter for biprimality test 293 | stat_sec_shamir = ( 294 | 40 # statistical security parameter for secret sharing over the integers 295 | ) 296 | 297 | 298 | def setup_local_pool(server_port: int, others: List[Tuple[str, int]]) -> Pool: 299 | pool = Pool() 300 | pool.add_http_server(server_port) 301 | for client_ip, client_port in others: 302 | pool.add_http_client( 303 | f"client_{client_ip}_{client_port}", client_ip, client_port 304 | ) 305 | return pool 306 | 307 | 308 | # REGION EXAMPLE SETUP 309 | # this region contains code that is used for the toy example, but can be deleted when the `others` 310 | # variable underneath the region is set to the proper values. 311 | 312 | parser = argparse.ArgumentParser(description="Set the parameters to run the protocol.") 313 | 314 | parser.add_argument( 315 | "--party", 316 | type=int, 317 | help="Identifier for this party. This should be different for all scripts but should be in the " 318 | "set [0, ..., nr_of_parties - 1].", 319 | ) 320 | 321 | parser.add_argument( 322 | "--nr_of_parties", 323 | type=int, 324 | help="Total number of parties involved. This should be the same for all scripts.", 325 | ) 326 | 327 | parser.add_argument( 328 | "--base-port", 329 | type=int, 330 | default=8888, 331 | help="port first player used for communication, incremented for other players" 332 | ) 333 | 334 | args = parser.parse_args() 335 | party_number = args.party 336 | nr_of_parties = args.nr_of_parties 337 | 338 | base_port = args.base_port 339 | # ENDREGION 340 | 341 | # Change this to the ips and server ports of the other machines 342 | others = [ 343 | ("localhost", base_port + i) for i in range(nr_of_parties) if i != party_number 344 | ] 345 | 346 | # Change this to the port you want this machine to listen on (note that this should correspond 347 | # to the port of this party in the scripts on the other machines) 348 | server_port = base_port + party_number 349 | pool = setup_local_pool(server_port, others) 350 | 351 | loop = asyncio.get_event_loop() 352 | protocol_coroutine = DistributedPaillier.from_security_parameter( 353 | pool, 354 | corruption_threshold, 355 | key_length, 356 | prime_thresh, 357 | correct_param_biprime, 358 | stat_sec_shamir, 359 | distributed=True, 360 | ) 361 | distributed_paillier_scheme = loop.run_until_complete(protocol_coroutine) 362 | ``` 363 | 364 | run_protocol.sh: 365 | 366 | ```shell 367 | #!/bin/bash 368 | # 369 | # This is a helper script to run the distributed version of the keygen example 370 | # on localhost. This means that for each party, a seperate process is started 371 | # on the same machine. 372 | # 373 | # Usage: ./run_protocol.sh NUMBER_OF_PARTIES 374 | # 375 | # Arguments: 376 | # NUMBER_OF_PARTIES The total number of parties to initialize. This should be an integer greater than 0. 377 | # 378 | # Example: 379 | # To run the distributed keygen with 5 parties, use the script as follows: 380 | # ./run_protocol.sh 5 381 | # 382 | 383 | for ((PARTY=0; PARTY < $1; PARTY++)) 384 | do 385 | echo "Initializing party $PARTY" 386 | python distributed_keygen_example_distributed.py --party $PARTY --nr_of_parties $1 & 387 | echo "Done" 388 | done 389 | wait 390 | echo "The protocol has finished" 391 | echo "Press any key to quit" 392 | while [ true ] ; do 393 | read -t 3 -n 1 394 | if [ $? = 0 ] ; then 395 | exit ; 396 | else 397 | echo "waiting for the keypress" 398 | fi 399 | done 400 | ``` 401 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "tno.mpc.protocols.distributed_keygen" 7 | description = "Distributed key generation using Paillier homomorphic encryption" 8 | readme = "README.md" 9 | authors = [{ name = "TNO PET Lab", email = "petlab@tno.nl" }] 10 | maintainers = [{ name = "TNO PET Lab", email = "petlab@tno.nl" }] 11 | keywords = [ 12 | "TNO", 13 | "MPC", 14 | "multi-party computation", 15 | "encryption schemes", 16 | "distributed", 17 | "paillier", 18 | "cryptosystem", 19 | "protocols", 20 | ] 21 | license = { text = "Apache License, Version 2.0" } 22 | classifiers = [ 23 | "License :: OSI Approved :: Apache Software License", 24 | "Programming Language :: Python :: 3", 25 | "Operating System :: OS Independent", 26 | "Intended Audience :: Developers", 27 | "Intended Audience :: Information Technology", 28 | "Intended Audience :: Science/Research", 29 | "Typing :: Typed", 30 | "Topic :: Security :: Cryptography", 31 | ] 32 | urls = { Homepage = "https://pet.tno.nl/", Documentation = "https://docs.pet.tno.nl/mpc/protocols/distributed_keygen/4.2.2", Source = "https://github.com/TNO-MPC/protocols.distributed_keygen" } 33 | dynamic = ["version"] 34 | requires-python = ">=3.8" 35 | dependencies = [ 36 | "sympy", 37 | "tno.mpc.communication~=4.8", 38 | "tno.mpc.encryption_schemes.paillier~=3.0,>=3.0.1", 39 | "tno.mpc.encryption_schemes.shamir>=1.3.0,<2.0", 40 | "tno.mpc.encryption_schemes.utils~=0.10", 41 | "typing_extensions; python_version<'3.12'", 42 | ] 43 | 44 | [project.optional-dependencies] 45 | gmpy = [ 46 | "tno.mpc.encryption_schemes.paillier[gmpy]", 47 | "tno.mpc.encryption_schemes.shamir[gmpy]", 48 | "tno.mpc.encryption_schemes.utils[gmpy]", 49 | ] 50 | tests = [ 51 | "pytest", 52 | "pytest-asyncio>=0.17", 53 | ] 54 | bench = [ 55 | "numpy", 56 | "matplotlib", 57 | "pandas", 58 | "seaborn", 59 | "tqdm", 60 | "tikzplotlib" 61 | ] 62 | 63 | [tool.setuptools] 64 | platforms = ["any"] 65 | 66 | [tool.setuptools.dynamic] 67 | version = {attr = "tno.mpc.protocols.distributed_keygen.__version__"} 68 | 69 | [tool.setuptools.package-data] 70 | "*" = ["py.typed"] 71 | "tno.mpc.protocols.distributed_keygen" = ["test/test_data/*.obj"] 72 | 73 | [tool.coverage.run] 74 | branch = true 75 | omit = ["*/test/*"] 76 | 77 | [tool.coverage.report] 78 | precision = 2 79 | show_missing = true 80 | 81 | [tool.isort] 82 | profile = "black" 83 | known_tno = "tno" 84 | known_first_party = "tno.mpc.protocols.distributed_keygen" 85 | sections = "FUTURE,STDLIB,THIRDPARTY,TNO,FIRSTPARTY,LOCALFOLDER" 86 | no_lines_before = "LOCALFOLDER" 87 | 88 | [tool.pytest.ini_options] 89 | addopts = "--fixture-pool-scope package" 90 | 91 | [tool.mypy] 92 | mypy_path = "src,stubs" 93 | strict = true 94 | show_error_context = true 95 | namespace_packages = true 96 | explicit_package_bases = true 97 | 98 | [tool.tbump.version] 99 | current = "4.2.2" 100 | regex = ''' 101 | \d+\.\d+\.\d+(-(.*))? 102 | ''' 103 | 104 | [tool.tbump.git] 105 | message_template = "Bump to {new_version}" 106 | tag_template = "v{new_version}" 107 | 108 | [[tool.tbump.file]] 109 | src = "pyproject.toml" 110 | search = "current = \"{current_version}\"" 111 | 112 | [[tool.tbump.file]] 113 | src = "src/tno/mpc/protocols/distributed_keygen/__init__.py" 114 | search = "__version__ = \"{current_version}\"" 115 | 116 | [[tool.tbump.file]] 117 | src = "CITATION.cff" 118 | search = "version: {current_version}" 119 | 120 | [[tool.tbump.file]] 121 | src = "README.md" 122 | search = '\[here\]\(https:\/\/docs.pet.tno.nl/[^\.]*\/{current_version}' 123 | -------------------------------------------------------------------------------- /scripts/bench_batch_size.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script implements a benchmark the runtime of the distributed key generation protocol for different batch sizes. 3 | 4 | Assuming you have installed the package into your Python environment, you can run this script as follows: 5 | `bench_batch_size --parties 3 --threshold 1 --key-length 1024 --stat-sec-shamir 40 --test-small-prime-div-param 20000 --test-biprime-param 40 --iterations 100` 6 | """ 7 | 8 | import argparse 9 | import asyncio 10 | import logging 11 | import os 12 | import pickle 13 | import re 14 | import time 15 | from dataclasses import dataclass, field 16 | 17 | import matplotlib.pyplot as plt 18 | import numpy as np 19 | import pandas as pd 20 | import seaborn as sns 21 | from pytest import MonkeyPatch 22 | from tikzplotlib import save as tikz_save 23 | from tqdm import tqdm 24 | from tqdm.contrib.logging import logging_redirect_tqdm 25 | 26 | from tno.mpc.communication import Pool 27 | 28 | from tno.mpc.protocols.distributed_keygen.distributed_keygen import DistributedPaillier 29 | 30 | logging.basicConfig(level=logging.WARNING) 31 | logger = logging.getLogger(__name__) 32 | logger.setLevel(logging.DEBUG) 33 | logging.getLogger("tno.mpc.communication.httphandlers").setLevel(logging.WARNING) 34 | logging.getLogger("tno.mpc.protocols.distributed_keygen").setLevel(logging.INFO) 35 | 36 | # General parameters 37 | PARTIES = 3 38 | THRESHOLD = 1 39 | 40 | # DistributedPaillier parameter defaults 41 | KEY_LENGTH = 1024 42 | STAT_SEC_SHAMIR = 40 43 | # Parameter $B$. All small prime divisors p up to $B$ are tested. 44 | TEST_SMALL_PRIME_DIV_PARAM = 20000 45 | # How many tests to perform for the biprimality test. 46 | TEST_BIPRIME_PARAM = 40 47 | 48 | # Benchmark parameters 49 | BATCH_SIZES = np.power(2, range(11)) 50 | ITERATIONS = 100 51 | 52 | # Ensure out directory exists 53 | os.makedirs("out", exist_ok=True) 54 | 55 | 56 | class DistributedPaillierTimer: 57 | """A class that monkeypatches the DistributedPaillier class to track the total time spent in some of its functions. 58 | 59 | The tracked functions are: 60 | - DistributedPaillier.__small_prime_divisors_test 61 | - DistributedPaillier.__biprime_test 62 | """ 63 | 64 | total_times = {} 65 | monkeypatch: MonkeyPatch 66 | 67 | def __init__(self): 68 | self.monkeypatch = MonkeyPatch() 69 | self._monkeypatch() 70 | 71 | def _monkeypatch(self): 72 | def time_execution(method): 73 | def timed(*args, **kw): 74 | ts = time.time() 75 | result = method(*args, **kw) 76 | te = time.time() 77 | self.total_times.setdefault(method.__name__, 0) 78 | self.total_times[method.__name__] += te - ts 79 | return result 80 | 81 | return timed 82 | 83 | def async_time_execution(method): 84 | async def timed(*args, **kw): 85 | ts = time.time() 86 | result = await method(*args, **kw) 87 | te = time.time() 88 | self.total_times.setdefault(method.__name__, 0) 89 | self.total_times[method.__name__] += te - ts 90 | return result 91 | 92 | return timed 93 | 94 | self.monkeypatch.setattr( 95 | DistributedPaillier, 96 | "_DistributedPaillier__small_prime_divisors_test", 97 | time_execution( 98 | getattr( 99 | DistributedPaillier, 100 | "_DistributedPaillier__small_prime_divisors_test", 101 | ) 102 | ), 103 | ) 104 | self.monkeypatch.setattr( 105 | DistributedPaillier, 106 | "_DistributedPaillier__biprime_test", 107 | async_time_execution( 108 | getattr(DistributedPaillier, "_DistributedPaillier__biprime_test") 109 | ), 110 | ) 111 | 112 | def reset(self): 113 | self.total_times = {} 114 | 115 | def get_total_times(self): 116 | return self.total_times 117 | 118 | 119 | class BechmarkLoggingHandler(logging.Handler): 120 | """A logging handler that captures the failed_small_prime and 121 | failed_biprime counts from the log messages. Before each run of the 122 | distributed keygen protocol, one should call reset.""" 123 | 124 | def __init__(self): 125 | super().__init__() 126 | self.failed_small_prime = 0 127 | self.failed_biprime = 0 128 | 129 | def emit(self, record): 130 | message = self.format(record) 131 | small_primes_match = re.search( 132 | r"Checked (\d+) primes for small prime divisors", message 133 | ) 134 | biprimality_candidates_match = re.search( 135 | r"Checked (\d+) candidates for biprimality", message 136 | ) 137 | 138 | if small_primes_match: 139 | self.failed_small_prime = int(small_primes_match.group(1)) 140 | 141 | if biprimality_candidates_match: 142 | self.failed_biprime = int(biprimality_candidates_match.group(1)) 143 | 144 | def reset(self): 145 | self.failed_small_prime = 0 146 | self.failed_biprime = 0 147 | 148 | 149 | def setup_pools(): 150 | port_base = 3001 151 | pools = [] 152 | 153 | for i in range(PARTIES): 154 | pool = Pool() 155 | pool.add_http_server(port_base + i) 156 | for j in (j for j in range(PARTIES) if j != i): 157 | pool.add_http_client(f"local{j}", "localhost", port=port_base + j) 158 | 159 | pools.append(pool) 160 | 161 | return pools 162 | 163 | 164 | async def perform_keygen(pools, batch_size=1): 165 | """Run a single iteration of the distributed keygen protocol.""" 166 | 167 | async_coroutines = [ 168 | DistributedPaillier.from_security_parameter( 169 | pool, 170 | THRESHOLD, 171 | KEY_LENGTH, 172 | TEST_SMALL_PRIME_DIV_PARAM, 173 | TEST_BIPRIME_PARAM, 174 | STAT_SEC_SHAMIR, 175 | distributed=False, 176 | batch_size=batch_size, 177 | ) 178 | for pool in pools 179 | ] 180 | 181 | await asyncio.gather(*async_coroutines) 182 | 183 | 184 | async def benchmark_keygen(pools, batch_size): 185 | """Run the distributed keygen protocol for a given batch size and return the runtime.""" 186 | start_time = time.perf_counter() 187 | await perform_keygen(pools, batch_size=batch_size) 188 | 189 | end_time = time.perf_counter() 190 | runtime = end_time - start_time # returns time in seconds 191 | 192 | return runtime 193 | 194 | 195 | @dataclass 196 | class BenchmarkState: 197 | iterations: int = 0 198 | runtimes: list[float] = field(default_factory=list) 199 | """List of runtimes for each iteration.""" 200 | failed_small_prime_test: list[int] = field(default_factory=list) 201 | """List of counters for each iteration. The counter tracks the number of failed small prime divisors tests in a single iteration.""" 202 | failed_biprime_test: list[int] = field(default_factory=list) 203 | """List of counters for each iteration. The counter tracks the number of failed biprime tests in a single iteration.""" 204 | time_small_prime_divisors_test: list[float] = field(default_factory=list) 205 | """List of timers for each iteration. Each timer tracks the total time spent in the small prime divisors test in a single iteration.""" 206 | time_biprime_test: list[float] = field(default_factory=list) 207 | """List of timers for each iteration. Each timer tracks the total time spent in the biprime test in a single iteration.""" 208 | 209 | def trim_iterations(self, iterations): 210 | """Trim the lists to the given number of iterations.""" 211 | self.iterations = iterations 212 | self.runtimes = self.runtimes[: self.iterations] 213 | self.failed_small_prime_test = self.failed_small_prime_test[: self.iterations] 214 | self.failed_biprime_test = self.failed_biprime_test[: self.iterations] 215 | self.time_small_prime_divisors_test = self.time_small_prime_divisors_test[ 216 | : self.iterations 217 | ] 218 | self.time_biprime_test = self.time_biprime_test[: self.iterations] 219 | 220 | 221 | async def run_benchmark(): 222 | pools = setup_pools() 223 | 224 | # This dictionary contains a BenchmarkState object for each batch size 225 | benchmark_states: dict[int, BenchmarkState] = {} 226 | 227 | # Load the benchmark state if it exists to continue where we left off 228 | pickle_file = f"out/bs{{batch_size}}_n{PARTIES}_t{THRESHOLD}_l{KEY_LENGTH}_s{STAT_SEC_SHAMIR}_small{TEST_SMALL_PRIME_DIV_PARAM}_biprime{TEST_BIPRIME_PARAM}.pkl" 229 | 230 | # We store the results for each batch size in a separate file 231 | for batch_size in BATCH_SIZES: 232 | filename = pickle_file.format(batch_size=batch_size) 233 | if not os.path.exists(filename): 234 | logger.info(f"Initializing new benchmark state for {batch_size}") 235 | benchmark_states[batch_size] = BenchmarkState() 236 | continue 237 | 238 | logger.info(f"Loading benchmark state from {filename}") 239 | with open(filename, "rb") as f: 240 | # Load the saved state into memory 241 | saved_state: BenchmarkState = pickle.load(f) 242 | benchmark_states[batch_size] = saved_state 243 | 244 | # If the requested number of iterations is smaller than the number 245 | # than the number of iterations found in the saved state, trim the 246 | # in memory benchmark state 247 | if saved_state.iterations > ITERATIONS: 248 | benchmark_states[batch_size].trim_iterations(ITERATIONS) 249 | 250 | # Capture logging output in order to record failed_small_prime and failed_biprime counts 251 | handler = BechmarkLoggingHandler() 252 | logging.getLogger( 253 | "tno.mpc.protocols.distributed_keygen.distributed_keygen" 254 | ).addHandler(handler) 255 | # Capture runtime of small_prime_divisors_test and biprime_test 256 | dp_timer = DistributedPaillierTimer() 257 | 258 | with logging_redirect_tqdm(): 259 | # Add a progress bar for batch sizes 260 | for batch_size in tqdm(BATCH_SIZES, desc="Batch sizes", ncols=70): 261 | # Count the completed runs for the current batch size 262 | completed_runs = benchmark_states[batch_size].iterations 263 | 264 | # Skip completed runs and add another progress bar for the remaining iterations 265 | for _ in tqdm( 266 | range(completed_runs, ITERATIONS), 267 | desc=f"Batch size {batch_size}", 268 | leave=False, 269 | ncols=70, 270 | ): 271 | # Reset failed_biprime and failed_small_prime counts 272 | handler.reset() 273 | # Reset the timer 274 | dp_timer.reset() 275 | 276 | # Run the benchmark 277 | runtime = await benchmark_keygen(pools, batch_size) 278 | 279 | # Save the results 280 | benchmark_states[batch_size].iterations += 1 281 | benchmark_states[batch_size].runtimes.append(runtime) 282 | benchmark_states[batch_size].failed_small_prime_test.append( 283 | handler.failed_small_prime 284 | ) 285 | benchmark_states[batch_size].failed_biprime_test.append( 286 | handler.failed_biprime 287 | ) 288 | benchmark_states[batch_size].time_small_prime_divisors_test.append( 289 | dp_timer.get_total_times()["__small_prime_divisors_test"] 290 | ) 291 | benchmark_states[batch_size].time_biprime_test.append( 292 | dp_timer.get_total_times()["__biprime_test"] 293 | ) 294 | 295 | # Log the results of this iteration 296 | logger.info(f"Batch size: {batch_size}, time: {runtime}") 297 | logger.info( 298 | f"failed small primes: {handler.failed_small_prime} (took {dp_timer.get_total_times()['__small_prime_divisors_test']})" 299 | ) 300 | logger.info( 301 | f"failed biprimes: {handler.failed_biprime} (took {dp_timer.get_total_times()['__biprime_test']})" 302 | ) 303 | 304 | # Store the benchmark state after each run 305 | with open(pickle_file.format(batch_size=batch_size), "wb") as f: 306 | logger.info(f"Saving benchmark state to {f.name}") 307 | pickle.dump(benchmark_states[batch_size], f) 308 | 309 | def plot_time(): 310 | # Convert the times into a Pandas DataFrame 311 | df_time = pd.DataFrame( 312 | [ 313 | (bs, time) 314 | for bs in BATCH_SIZES 315 | for time in benchmark_states[bs].runtimes 316 | ], 317 | columns=["BatchSize", "Time"], 318 | ) 319 | 320 | # Use seaborn to plot with confidence intervals 321 | sns.lineplot(x="BatchSize", y="Time", data=df_time, errorbar="sd") 322 | plt.ylabel("Time (s)") 323 | plt.yscale("linear") 324 | 325 | # Display the plot in a new window 326 | plt.savefig("out/plot_time.png") 327 | tikz_save("out/plot_time.tex") 328 | logger.info(f"Saved {os.getcwd()}/out/plot_time.png,tex") 329 | plt.clf() 330 | 331 | def plot_histogram_small_prime(): 332 | # Convert the failure counts into a Pandas DataFrame 333 | df_small_prime = pd.DataFrame( 334 | [ 335 | (bs, count) 336 | for bs in BATCH_SIZES 337 | for count in benchmark_states[bs].failed_small_prime_test 338 | ], 339 | columns=["BatchSize", "FailedSmallPrime"], 340 | ) 341 | 342 | plt.hist(df_small_prime["FailedSmallPrime"], bins="auto") 343 | plt.title("Histogram of Failed Small Prime Tests") 344 | plt.xlabel("Number of failed small prime tests") 345 | plt.ylabel("Frequency") 346 | 347 | # Calculate mean and standard deviation 348 | mean = np.mean(df_small_prime["FailedSmallPrime"]) 349 | std = np.std(df_small_prime["FailedSmallPrime"]) 350 | # Add mu and sigma symbol to legend as two separate lines 351 | plt.text(0.8, 0.9, f"N: {ITERATIONS}", transform=plt.gca().transAxes) 352 | plt.text(0.8, 0.85, f"μ: {mean:.2f}", transform=plt.gca().transAxes) 353 | plt.text(0.8, 0.80, f"σ: {std:.2f}", transform=plt.gca().transAxes) 354 | 355 | plt.savefig("out/plot_histogram_small_prime.png") 356 | tikz_save("out/plot_histogram_small_prime.tex") 357 | logger.info(f"Saved {os.getcwd()}/out/plot_histogram_small_prime.png,tex") 358 | plt.clf() 359 | 360 | def plot_histogram_biprime(): 361 | # Convert the failure counts into a Pandas DataFrame 362 | df_biprime = pd.DataFrame( 363 | [ 364 | (bs, count) 365 | for bs in BATCH_SIZES 366 | for count in benchmark_states[bs].failed_biprime_test 367 | ], 368 | columns=["BatchSize", "FailedBiprime"], 369 | ) 370 | 371 | plt.hist(df_biprime["FailedBiprime"], bins="auto") 372 | plt.title("Histogram of Failed Biprime Tests") 373 | plt.xlabel("Number of failed biprime tests") 374 | plt.ylabel("Frequency") 375 | 376 | # Calculate mean and standard deviation 377 | mean = np.mean(df_biprime["FailedBiprime"]) 378 | std = np.std(df_biprime["FailedBiprime"]) 379 | # Add mu and sigma symbol to legend as two separate lines 380 | plt.text(0.8, 0.9, f"N: {ITERATIONS}", transform=plt.gca().transAxes) 381 | plt.text(0.8, 0.85, f"μ: {mean:.2f}", transform=plt.gca().transAxes) 382 | plt.text(0.8, 0.80, f"σ: {std:.2f}", transform=plt.gca().transAxes) 383 | 384 | plt.savefig("out/plot_histogram_biprime.png") 385 | tikz_save("out/plot_histogram_biprime.tex") 386 | logger.info(f"Saved {os.getcwd()}/out/plot_histogram_biprime.png,tex") 387 | plt.clf() 388 | 389 | def plot_histogram_function_runtimes(): 390 | # Convert the failure counts into a Pandas DataFrame 391 | df_func_rt = pd.DataFrame( 392 | [ 393 | (bs, small_prime_rt, biprime_rt) 394 | for bs in BATCH_SIZES 395 | for small_prime_rt, biprime_rt in zip( 396 | benchmark_states[bs].time_small_prime_divisors_test, 397 | benchmark_states[bs].time_biprime_test, 398 | ) 399 | ], 400 | columns=["BatchSize", "SmallPrimeRuntime", "BiprimeRuntime"], 401 | ) 402 | 403 | # Set the style of the plots 404 | sns.set(style="whitegrid") 405 | # Create a figure and a set of subplots 406 | _, ax = plt.subplots() 407 | # Plot the histogram of SmallPrimeRuntime 408 | sns.histplot( 409 | df_func_rt, 410 | x="SmallPrimeRuntime", 411 | bins="auto", 412 | color="blue", 413 | label="SmallPrimeRuntime", 414 | kde=False, 415 | ax=ax, 416 | ) 417 | # Plot the histogram of BiprimeRuntime 418 | sns.histplot( 419 | df_func_rt, 420 | x="BiprimeRuntime", 421 | bins="auto", 422 | color="red", 423 | label="BiprimeRuntime", 424 | kde=False, 425 | ax=ax, 426 | ) 427 | 428 | # plt.hist(df_func_rt["SmallPrimeRuntime"], bins="auto", label="Small prime test") 429 | # plt.hist(df_func_rt["BiprimeRuntime"], bins="auto", label="Biprime test", color="orange") 430 | plt.title("Execution times of primality tests") 431 | plt.xlabel("Time (seconds)") 432 | plt.ylabel("Frequency") 433 | 434 | plt.savefig("out/plot_histogram_primality_tests_runtimes.png") 435 | tikz_save("out/plot_histogram_primality_tests_runtimes.tex") 436 | logger.info( 437 | f"Saved {os.getcwd()}/out/plot_histogram_primality_tests_runtimes.png,tex" 438 | ) 439 | plt.clf() 440 | 441 | plot_time() 442 | plot_histogram_small_prime() 443 | plot_histogram_biprime() 444 | plot_histogram_function_runtimes() 445 | 446 | 447 | def main(): 448 | parser = argparse.ArgumentParser( 449 | formatter_class=argparse.RawDescriptionHelpFormatter, 450 | description=""" 451 | Run the benchmarks to generate the graphs as used in the paper. 452 | 453 | === Parameters === 454 | 455 | The following parameters are directly passed to 456 | `from_security_parameter` function of the DistributedPaillier class. To 457 | find more information about these parameters, please refer to the 458 | documentation of the DistributedPaillier class or the README. 459 | - threshold -> corruption_threshold 460 | - stat-sec-shamir -> stat_sec_shamir 461 | - test-small-prime-div-param -> prime_treshold 462 | - test-biprime-param -> correct_param_biprime 463 | 464 | The following parameters are alter the behaviour of the benchmark: 465 | - iterations -> How often to repeat each experiment. As the protocol is 466 | probabilistic, this is necessary to get a good estimate 467 | of the runtime. In the paper, iterations=1067 is used 468 | (with `--batch-sizes` fixed to 1). 469 | - batch_sizes -> To benchmark the performance of the protocol for 470 | different batch_sizes, this parameter can be set to 471 | a list of values. Recommended is to use a 472 | logarithmic scale, i.e. [1, 2, 4, 8, ..., 1024]. 473 | Beware that this greatly increases the duration of 474 | the benchmark and should only be used when 475 | benchmarking specifically the batch_sizes. When 476 | benchmarking the how often the primality tests fail 477 | on a realistic key size (i.e. 1024), either set the 478 | batch size equal to the optimal value (faster) 479 | or 1 (much slower). 480 | 481 | === Reproducing the graphs from the paper === 482 | 483 | To get the exact graphs from the paper, run two benchmarks: 484 | - `python3 benchmark.py --batch-sizes "1,2,4,8,16,32,64,128,256,512,1024" --iterations 100 --key-length 512` 485 | - Copy: 486 | - out/plot_time.png 487 | - `python3 benchmark.py --batch-sizes 1 --iterations 1067 --key-length 1024` 488 | - Copy: 489 | - out/plot_histogram_small_prime.png 490 | - out/plot_histogram_biprime.png 491 | 492 | Beware: when rerunning a benchmark, the plots in the out/ folder will be 493 | overwritten. 494 | 495 | === Features === 496 | 497 | - While running, the script shows a progress bar to indicate the 498 | progress and estimated remaining time. 499 | - The script is quite robust. Intermidiate results are saved to disk, 500 | so that the script can be stopped and restarted without losing 501 | progress. 502 | The script automatically creates a `./out` folder to store the 503 | (intermediate) results in. 504 | 505 | 506 | """, 507 | ) 508 | parser.add_argument("--parties", type=int, default=3, help="The number of parties.") 509 | parser.add_argument( 510 | "--threshold", 511 | type=int, 512 | default=1, 513 | help="The threshold value for corrupted parties.", 514 | ) 515 | parser.add_argument("--key-length", type=int, default=512, help="The key length.") 516 | parser.add_argument( 517 | "--stat-sec-shamir", 518 | type=int, 519 | default=40, 520 | help="Statistical security parameter.", 521 | ) 522 | parser.add_argument( 523 | "--test-small-prime-div-param", 524 | type=int, 525 | default=20000, 526 | help="Upper bound for small prime divisor test.", 527 | ) 528 | parser.add_argument( 529 | "--test-biprime-param", 530 | type=int, 531 | default=40, 532 | help="Statistical security parameter for biprime test.", 533 | ) 534 | parser.add_argument( 535 | "--batch-sizes", 536 | type=lambda s: [int(item) for item in s.split(",")], 537 | default=np.power(2, range(11)), 538 | help="Batch sizes. Pass a comma-separated list without spaces, like: 1,2,4,8", 539 | ) 540 | parser.add_argument( 541 | "--iterations", 542 | type=int, 543 | default=100, 544 | help="The number of benchmarks to execute for each batch size.", 545 | ) 546 | args = parser.parse_args() 547 | 548 | global PARTIES, THRESHOLD, KEY_LENGTH, STAT_SEC_SHAMIR, TEST_SMALL_PRIME_DIV_PARAM, TEST_BIPRIME_PARAM, BATCH_SIZES, ITERATIONS 549 | 550 | PARTIES = args.parties 551 | THRESHOLD = args.threshold 552 | KEY_LENGTH = args.key_length 553 | STAT_SEC_SHAMIR = args.stat_sec_shamir 554 | TEST_SMALL_PRIME_DIV_PARAM = args.test_small_prime_div_param 555 | TEST_BIPRIME_PARAM = args.test_biprime_param 556 | BATCH_SIZES = args.batch_sizes 557 | ITERATIONS = args.iterations 558 | 559 | loop = asyncio.get_event_loop() 560 | loop.run_until_complete(run_benchmark()) 561 | 562 | 563 | if __name__ == "__main__": 564 | main() 565 | -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Distributed key generation using Paillier homomorphic encryption. 3 | """ 4 | 5 | # Explicit re-export of all functionalities, such that they can be imported properly. Following 6 | # https://www.python.org/dev/peps/pep-0484/#stub-files and 7 | # https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-no-implicit-reexport 8 | from tno.mpc.protocols.distributed_keygen.distributed_keygen import ( 9 | DistributedPaillier as DistributedPaillier, 10 | ) 11 | from tno.mpc.protocols.distributed_keygen.paillier_shared_key import ( 12 | PaillierSharedKey as PaillierSharedKey, 13 | ) 14 | 15 | __version__ = "4.2.2" 16 | -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/distributed_keygen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Code for a single player in the Paillier distributed key-generation protocol. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import copy 8 | import logging 9 | import math 10 | import secrets 11 | import warnings 12 | from dataclasses import asdict 13 | from random import randint 14 | from typing import Any, Iterable, TypedDict, cast, overload 15 | 16 | # ormsgpack dependency already included by the communication package 17 | import ormsgpack as ormsgpack 18 | import sympy 19 | 20 | from tno.mpc.communication.httphandlers import HTTPClient 21 | from tno.mpc.communication.pool import Pool 22 | from tno.mpc.encryption_schemes.paillier import ( 23 | Paillier, 24 | PaillierCiphertext, 25 | PaillierPublicKey, 26 | PaillierSecretKey, 27 | paillier, 28 | ) 29 | from tno.mpc.encryption_schemes.shamir import IntegerShares 30 | from tno.mpc.encryption_schemes.shamir import ( 31 | ShamirSecretSharingIntegers as IntegerShamir, 32 | ) 33 | from tno.mpc.encryption_schemes.shamir import ShamirSecretSharingScheme as Shamir 34 | from tno.mpc.encryption_schemes.templates.encryption_scheme import EncodedPlaintext 35 | from tno.mpc.encryption_schemes.utils import pow_mod 36 | 37 | from tno.mpc.protocols.distributed_keygen.paillier_shared_key import PaillierSharedKey 38 | from tno.mpc.protocols.distributed_keygen.utils import ( 39 | AdditiveVariable, 40 | Batched, 41 | ShamirVariable, 42 | Shares, 43 | exchange_reconstruct, 44 | exchange_shares, 45 | ) 46 | 47 | try: 48 | from tno.mpc.communication import ( 49 | RepetitionError, 50 | Serialization, 51 | SupportsSerialization, 52 | ) 53 | 54 | COMMUNICATION_INSTALLED = True 55 | except ModuleNotFoundError: 56 | COMMUNICATION_INSTALLED = False 57 | 58 | logger = logging.getLogger(__name__) 59 | # the generators must have a jacobi symbol of 1. To ensure we have sufficient number of generators with a jacobi symbol of 1 we generate four times as many as we need (we need `correct_biprime_param` amount) 60 | JACOBI_CORRECTION_FACTOR = 4 61 | 62 | DIST_KEY_STORAGE_PACK_OPTIONS = ( 63 | ormsgpack.OPT_PASSTHROUGH_BIG_INT 64 | | ormsgpack.OPT_PASSTHROUGH_TUPLE 65 | | ormsgpack.OPT_PASSTHROUGH_DATACLASS 66 | | ormsgpack.OPT_SERIALIZE_NUMPY 67 | | ormsgpack.OPT_NON_STR_KEYS 68 | ) 69 | 70 | 71 | class SessionIdError(Exception): 72 | """ 73 | Used to raise exceptions when a session ID is invalid 74 | """ 75 | 76 | 77 | class DistributedPaillier(Paillier, SupportsSerialization): 78 | """ 79 | Class that acts as one of the parties involved in distributed Paillier secret key generation. 80 | The pool represents the network of parties involved in the key generation protocol. 81 | """ 82 | 83 | default_key_length = 2048 84 | default_prime_threshold = 2000 85 | default_biprime_param = 40 86 | default_sec_shamir = 40 87 | default_corruption_threshold = 1 88 | _global_instances: dict[int, dict[int, DistributedPaillier]] = {} 89 | _local_instances: dict[int, DistributedPaillier] = {} 90 | 91 | @classmethod 92 | async def from_security_parameter( # type: ignore[override] 93 | cls, 94 | pool: Pool, 95 | corruption_threshold: int = default_corruption_threshold, 96 | key_length: int = default_key_length, 97 | prime_threshold: int = default_prime_threshold, 98 | correct_param_biprime: int = default_biprime_param, 99 | stat_sec_shamir: int = default_sec_shamir, 100 | distributed: bool = True, 101 | precision: int = 0, 102 | batch_size: int = 100, 103 | ) -> DistributedPaillier: 104 | r""" 105 | Function that takes security parameters related to secret sharing and Paillier and 106 | initiates a protocol to create a shared secret key between the parties in the provided 107 | pool. 108 | 109 | :param precision: precision of the fixed point encoding in Paillier 110 | :param pool: The network of involved parties 111 | :param corruption_threshold: Maximum number of allowed corruptions. We require for the 112 | number of parties in the pool and the corruption threshold that 113 | $$\text{number_of_parties} >= 2 * \text{corruption_threshold} + 1$$. 114 | This is because we need to multiply secret sharings that both use polynomials of 115 | degree corruption_threshold. The resulting secret sharing then becomes a polynomial 116 | of degree $2*\text{corruption_threshold}$ and it requires at least $2*text{corruption_threshold}+1$ 117 | evaluation points to reconstruct the secret in that sharing. 118 | :param key_length: desired bit length of the modulus $N$ 119 | :param prime_threshold: Upper bound on the number of prime numbers to check during 120 | primality tests 121 | :param correct_param_biprime: parameter that affects the certainty of the generated $N$ 122 | to be the product of two primes 123 | :param stat_sec_shamir: security parameter for the Shamir secret sharing over the integers 124 | :param distributed: Whether the different parties are run on different python instances 125 | :param precision: precision (number of decimals) to ensure 126 | :param batch_size: How many $p$'s and $q$'s to generate at once (drastically 127 | reduces communication at the expense of potentially wasted computation) 128 | :raise ValueError: In case the number of parties $n$ and the corruption threshold $t$ do 129 | not satisfy that $n \geq 2*t + 1$ 130 | :raise SessionIdError: In case the parties agree on a session ID that is already being used. 131 | :return: DistributedPaillier scheme containing a regular Paillier public key and a shared 132 | secret key. 133 | """ 134 | ( 135 | number_of_players, 136 | prime_length, 137 | prime_list, 138 | shamir_scheme_t, 139 | shamir_scheme_2t, 140 | shares, 141 | ) = cls.setup_input(pool, key_length, prime_threshold, corruption_threshold) 142 | index, party_indices, session_id = await cls.setup_protocol(pool) 143 | 144 | # check if number_of_parties >= 2 * corruption_threshold + 1 145 | if number_of_players < 2 * corruption_threshold + 1: 146 | raise ValueError( 147 | "For a secret sharing scheme that needs to do a homomorphic " 148 | f"multiplication, \nwhich is the case during distributed key generation " 149 | f"with Paillier,\nwe require for the number of parties n and the corruption " 150 | f"threshold t that n >= 2*t + 1.\n" 151 | f"The given pool contains {number_of_players} parties (n) and the given corruption " 152 | f"threshold (t) is {corruption_threshold}." 153 | ) 154 | 155 | # generate keypair 156 | public_key, secret_key = await cls.generate_keypair( 157 | stat_sec_shamir, 158 | number_of_players, 159 | corruption_threshold, 160 | shares, 161 | index, 162 | pool, 163 | prime_list, 164 | prime_length, 165 | party_indices, 166 | correct_param_biprime, 167 | shamir_scheme_t, 168 | shamir_scheme_2t, 169 | session_id, 170 | batch_size, 171 | ) 172 | 173 | scheme = cls( 174 | public_key=public_key, 175 | secret_key=secret_key, 176 | precision=precision, 177 | pool=pool, 178 | index=index, 179 | party_indices=party_indices, 180 | session_id=session_id, 181 | distributed=distributed, 182 | corruption_threshold=corruption_threshold, 183 | ) 184 | 185 | cls.__register_scheme(scheme, distributed) 186 | 187 | if key_length < 1024: 188 | warnings.warn( 189 | f"The key length={key_length} is lower than the advised minimum of 1024." 190 | ) 191 | 192 | return scheme 193 | 194 | @classmethod 195 | def __register_scheme(cls, scheme: DistributedPaillier, distributed: bool) -> None: 196 | """ 197 | Register the scheme such that the deserialization reuses the existing scheme and does not 198 | create a Paillier object. 199 | 200 | :param scheme: The scheme to register 201 | :param distributed: Whether the different parties are run on different python instances 202 | """ 203 | # We need to distinguish the case where the parties share a python instance and where they 204 | # are run in different python instances. If the same python instance is used, then we need 205 | # to save a different DistributedPaillier instance for each party. If different python 206 | # instances are used, then we have exactly one DistributedPaillier instance in the python 207 | # instance for that session. 208 | if distributed: 209 | if scheme.session_id in cls._local_instances: 210 | raise SessionIdError( 211 | "An already existing session ID is about to be overwritten. " 212 | "This can only happen if multiple sessions are run within the same python " 213 | "instance and one of those session has the same ID" 214 | ) 215 | cls._local_instances[scheme.session_id] = scheme 216 | else: 217 | if scheme.index in cls._global_instances: 218 | if scheme.session_id in cls._global_instances[scheme.index]: 219 | raise SessionIdError( 220 | "An already existing session ID is about to be overwritten. " 221 | "This can only happen if multiple sessions are run within the same python " 222 | "instance and one of those session has the same ID" 223 | ) 224 | cls._global_instances[scheme.index][scheme.session_id] = scheme 225 | else: 226 | cls._global_instances[scheme.index] = {scheme.session_id: scheme} 227 | 228 | def __init__( 229 | self, 230 | public_key: PaillierPublicKey, 231 | secret_key: PaillierSharedKey, 232 | precision: int, 233 | pool: Pool, 234 | index: int, 235 | party_indices: dict[str, int], 236 | session_id: int, 237 | distributed: bool, 238 | corruption_threshold: int, 239 | **kwargs: Any, 240 | ) -> None: 241 | """ 242 | Initializes a DistributedPaillier instance with a public Paillier key and a shared 243 | secret Paillier key. 244 | 245 | :param public_key: The Paillier public key 246 | :param secret_key: The shared secret Paillier key 247 | :param precision: The precision of the resulting scheme 248 | :param pool: The pool with connections of parties involved in the shared secret key 249 | :param index: The index of the party who owns this instance within the pool 250 | :param party_indices: Dictionary mapping parties in the pool to their indices 251 | :param session_id: The unique session identifier belonging to the protocol that generated 252 | the keys for this DistributedPaillier scheme. 253 | :param distributed: Boolean value indicating whether the protocol that generated the keys 254 | for this DistributedPaillier scheme was run in different Python instances (True) or in a 255 | single python instance (False) 256 | :param corruption_threshold: The corruption threshold used during the generation of the key 257 | :param kwargs: Any keyword arguments that are passed to the super __init__ function 258 | """ 259 | super().__init__( 260 | public_key, cast(PaillierSecretKey, secret_key), precision, False, **kwargs 261 | ) 262 | 263 | # these variables are necessary during decryption 264 | self.pool = pool 265 | self.index = index 266 | self.party_indices = party_indices 267 | self.session_id = session_id 268 | self.distributed = distributed 269 | self.corruption_threshold = corruption_threshold 270 | 271 | def __eq__(self, other: object) -> bool: 272 | """ 273 | Compare this Distributed Paillier scheme with another to determine (in)equality. Does not 274 | take the secret key into account as it might not be known and the public key combined 275 | with the precision and the session id. 276 | 277 | :param other: Object to compare this Paillier scheme with. 278 | :return: Boolean value representing (in)equality of both objects. 279 | """ 280 | # Equality should still hold if the secret key is not available 281 | return ( 282 | isinstance(other, DistributedPaillier) 283 | and self.precision == other.precision 284 | and self.public_key == other.public_key 285 | and self.session_id == other.session_id 286 | ) 287 | 288 | # region Decryption 289 | async def decrypt( # type: ignore[override] 290 | self, 291 | ciphertext: PaillierCiphertext, 292 | apply_encoding: bool = True, 293 | receivers: list[str] | None = None, 294 | ) -> paillier.Plaintext | None: 295 | """ 296 | Decrypts the input ciphertext. Starts a protocol between the parties involved to create 297 | local decryptions, send them to the other parties and combine them into full decryptions 298 | for each party. 299 | 300 | :param ciphertext: Ciphertext to be decrypted. 301 | :param apply_encoding: Boolean indicating whether the decrypted ciphertext is decoded 302 | before it is returned. Defaults to True. 303 | :param receivers: An optional list specifying the names of the receivers, your own 'name' 304 | is "self". 305 | :return: Plaintext decrypted value. 306 | """ 307 | decrypted_ciphertext = await self._decrypt_raw(ciphertext, receivers) 308 | return ( 309 | self.apply_encoding(decrypted_ciphertext, apply_encoding) 310 | if decrypted_ciphertext is not None 311 | else None 312 | ) 313 | 314 | async def _decrypt_raw( # type: ignore[override] 315 | self, 316 | ciphertext: PaillierCiphertext, 317 | receivers: list[str] | None = None, 318 | ) -> EncodedPlaintext[int] | None: 319 | """ 320 | Function that starts a protocol between the parties involved to create local decryptions, 321 | send them to the other parties and combine them into full decryptions for each party. 322 | 323 | :param ciphertext: The ciphertext to be decrypted. 324 | :param receivers: An optional list specifying the names of the receivers, your own 'name' 325 | is "self". If none is provided it is sent to all parties in the pool. 326 | :return: The encoded plaintext corresponding to the ciphertext. 327 | """ 328 | receivers_without_self: list[str] | None 329 | if receivers is not None: 330 | # If we are part of the receivers, we expect the other parties to send us partial 331 | # decryptions 332 | 333 | # We will broadcast our partial decryption to all receivers, but we do not need to send 334 | # anything to ourselves. 335 | if self_receive := "self" in receivers: 336 | receivers_without_self = [recv for recv in receivers if recv != "self"] 337 | else: 338 | receivers_without_self = receivers 339 | else: 340 | # If no receivers are specified, we assume everyone will receive the partial decryptions 341 | self_receive = True 342 | receivers_without_self = receivers 343 | 344 | # generate the local partial decryption 345 | partial_decryption_shares = { 346 | self.index: cast(PaillierSharedKey, self.secret_key).partial_decrypt( 347 | ciphertext 348 | ) 349 | } 350 | 351 | # send the partial decryption to all other parties in the provided network 352 | encryption_hash = bin(ciphertext.peek_value()).zfill(32)[2:34] 353 | message_id = ( 354 | f"distributed_decryption_session#{self.session_id}_hash#{encryption_hash}" 355 | ) 356 | if receivers_without_self is None or len(receivers_without_self) != 0: 357 | self.pool.async_broadcast( 358 | { 359 | "content": "partial_decryption", 360 | "value": partial_decryption_shares[self.index], 361 | }, 362 | msg_id=message_id, 363 | handler_names=receivers_without_self, 364 | ) 365 | 366 | if self_receive: 367 | # receive the partial decryption from the other parties 368 | other_partial_decryption_shares: tuple[tuple[str, dict[str, Any]]] = ( 369 | await self.pool.recv_all(msg_id=message_id) 370 | ) 371 | for party, message in other_partial_decryption_shares: 372 | msg_content = message["content"] 373 | err_msg = f"received a share for {msg_content}, but expected partial_decryption" 374 | assert msg_content == "partial_decryption", err_msg 375 | partial_decryption_shares[self.party_indices[party]] = message["value"] 376 | 377 | # combine all partial decryption to obtain the full decryption 378 | decryption = cast(PaillierSharedKey, self.secret_key).decrypt( 379 | partial_decryption_shares 380 | ) 381 | return EncodedPlaintext(decryption, scheme=self) 382 | return None 383 | 384 | def apply_encoding( 385 | self, decrypted_ciphertext: EncodedPlaintext[int], apply_encoding: bool 386 | ) -> paillier.Plaintext: 387 | """ 388 | Function which decodes a decrypted ciphertext 389 | 390 | :param decrypted_ciphertext: ciphertext to decode 391 | :param apply_encoding: Boolean indicating if `decrypted_ciphertext` needs to be decoded. 392 | :return: The decoded plaintext 393 | """ 394 | return ( 395 | self.decode(decrypted_ciphertext) 396 | if apply_encoding 397 | else decrypted_ciphertext.value 398 | ) 399 | 400 | async def decrypt_sequence( # type: ignore[override] 401 | self, 402 | ciphertext_sequence: Iterable[PaillierCiphertext], 403 | apply_encoding: bool = True, 404 | receivers: list[str] | None = None, 405 | ) -> list[paillier.Plaintext] | None: 406 | """ 407 | Decrypts the list of ciphertexts 408 | 409 | :param ciphertext_sequence: Sequence of Ciphertext to be decrypted 410 | :param apply_encoding: Boolean indicating whether the decrypted ciphertext is decoded before it is returned. 411 | Defaults to True. 412 | :param receivers: The receivers of all (partially) decrypted ciphertexts. If None is given it is sent to all 413 | parties. If a list is provided it is sent to those receivers. 414 | :return: The list of encoded plaintext corresponding to the ciphertext, or None if 'self' is not in the 415 | receivers list. 416 | """ 417 | 418 | decrypted_ciphertext_list = await self._decrypt_sequence_raw( 419 | ciphertext_sequence, receivers 420 | ) 421 | return ( 422 | None 423 | if decrypted_ciphertext_list is None 424 | else [ 425 | self.apply_encoding(decryption, apply_encoding) 426 | for decryption in decrypted_ciphertext_list 427 | ] 428 | ) 429 | 430 | async def _decrypt_sequence_raw( 431 | self, 432 | ciphertext_sequence: Iterable[PaillierCiphertext], 433 | receivers: list[str] | None = None, 434 | ) -> list[EncodedPlaintext[int]] | None: 435 | """ 436 | Function that starts a protocol between the parties involved to create local decryptions, 437 | send them to the other parties and combine them into full decryptions for each party. 438 | 439 | :param ciphertext_sequence: The sequence of ciphertext to be decrypted. 440 | :param receivers: An optional list specifying the names of the receivers, your own 'name' 441 | is "self". If None is provided it is sent to all parties. 442 | :return: The list of encoded plaintext corresponding to the ciphertext, or None if 'self' is not in the 443 | receivers list. 444 | """ 445 | 446 | receivers_without_self: list[str] | None 447 | if receivers is not None: 448 | # If we are part of the receivers, we expect the other parties to send us partial 449 | # decryptions 450 | 451 | # We will broadcast our partial decryption to all receivers, but we do not need to send 452 | # anything to ourselves. 453 | if self_receive := "self" in receivers: 454 | receivers_without_self = [recv for recv in receivers if recv != "self"] 455 | else: 456 | receivers_without_self = receivers 457 | else: 458 | # If no receivers are specified, we assume everyone will receive the partial decryptions 459 | self_receive = True 460 | receivers_without_self = receivers 461 | 462 | # partially decrypt the received cipher texts 463 | partially_decrypted_shares = [ 464 | cast(PaillierSharedKey, self.secret_key).partial_decrypt(ciphertext) 465 | for ciphertext in ciphertext_sequence 466 | ] 467 | 468 | # send the partial decryption to all other parties in the provided network 469 | encryption_hash = ( 470 | bin(next(iter(ciphertext_sequence)).peek_value()).zfill(32)[2:34] 471 | + f"{len(partially_decrypted_shares)}" 472 | ) 473 | message_id = ( 474 | f"distributed_decryption_session#{self.session_id}_hash#{encryption_hash}" 475 | ) 476 | if receivers_without_self is None or len(receivers_without_self) != 0: 477 | self.pool.async_broadcast( 478 | { 479 | "content": "partial_decryption_sequence", 480 | "value": partially_decrypted_shares, 481 | }, 482 | msg_id=message_id, 483 | handler_names=receivers_without_self, 484 | ) 485 | 486 | if self_receive: 487 | # store the partial decryptions per party 488 | shares_dict_per_decryption: list[dict[int, int]] = [ 489 | {self.index: partially_decrypted_share} 490 | for partially_decrypted_share in partially_decrypted_shares 491 | ] 492 | 493 | # receive the partial decryption from the other parties 494 | partial_decryptions_other_parties = await self.pool.recv_all( 495 | msg_id=message_id, 496 | ) 497 | for party, message in partial_decryptions_other_parties: 498 | msg_content = message["content"] 499 | err_msg = f"received a share for {msg_content}, but expected partial_decryption_sequence" 500 | assert msg_content == "partial_decryption_sequence", err_msg 501 | partial_decryptions_party = message["value"] 502 | for shares_dict, partial_decryption in zip( 503 | shares_dict_per_decryption, partial_decryptions_party 504 | ): 505 | shares_dict[self.party_indices[party]] = partial_decryption 506 | 507 | # decrypt all the shares 508 | decryption_results = [] 509 | 510 | for shares_dict in shares_dict_per_decryption: 511 | # combine all partial decryption to obtain the full decryption 512 | decryption = cast(PaillierSharedKey, self.secret_key).decrypt( 513 | shares_dict 514 | ) 515 | decryption_results.append(EncodedPlaintext(decryption, scheme=self)) 516 | return decryption_results 517 | return None 518 | 519 | # endregion 520 | 521 | # region Setup functions 522 | 523 | @classmethod 524 | def setup_input( 525 | cls, 526 | pool: Pool, 527 | key_length: int, 528 | prime_threshold: int, 529 | corruption_threshold: int, 530 | ) -> tuple[int, int, list[int], Shamir, Shamir, Shares]: 531 | r""" 532 | Function that sets initial variables for the process of creating a shared secret key 533 | 534 | :param pool: network of involved parties 535 | :param key_length: desired bit length of the modulus $N = p \cdot q$ 536 | :param prime_threshold: Bound on the number of prime numbers to be checked for primality 537 | tests 538 | :param corruption_threshold: Number of parties that are allowed to be corrupted 539 | :return: A tuple of initiated variables, containing first the number_of_players, 540 | second the length of the primes $p$ and $q$, third a list of small primes for the 541 | small_prime test (empty if the length of $p$ and $q$ is smaller than the 542 | prime_threshold), fourth a regular Shamir Sharing scheme, fifth a Shares data structure 543 | for holding relevant shares, and last a list of the names of other parties. 544 | """ 545 | number_of_players = len(pool.pool_handlers) + 1 546 | 547 | # key length of primes p and q 548 | prime_length = key_length // 2 549 | 550 | # if the primes are smaller than the small prime threshold, 551 | # there's no point in doing a small prime test 552 | if prime_length < math.log(prime_threshold): 553 | prime_threshold = 1 554 | prime_list: list[int] = list(sympy.primerange(3, prime_threshold + 1)) 555 | shamir_scheme_t = cls.__init_shamir_scheme( 556 | prime_length, number_of_players, corruption_threshold 557 | ) 558 | shamir_scheme_2t = cls.__init_shamir_scheme( 559 | prime_length, number_of_players, corruption_threshold * 2 560 | ) 561 | 562 | shares = Shares() 563 | 564 | return ( 565 | number_of_players, 566 | prime_length, 567 | prime_list, 568 | shamir_scheme_t, 569 | shamir_scheme_2t, 570 | shares, 571 | ) 572 | 573 | @classmethod 574 | async def setup_protocol(cls, pool: Pool) -> tuple[int, dict[str, int], int]: 575 | """ 576 | Runs the indices protocol and sets own ID. 577 | 578 | :param pool: network of involved parties 579 | :return: This party's index, a dictionary with indices for the other parties, the session id 580 | """ 581 | # start indices protocol 582 | party_indices, session_id = await cls.get_indices(pool) 583 | index = party_indices["self"] 584 | return index, party_indices, session_id 585 | 586 | @classmethod 587 | async def get_indices(cls, pool: Pool) -> tuple[dict[str, int], int]: 588 | """ 589 | Function that initiates a protocol to determine IDs (indices) for each party 590 | 591 | :param pool: network of involved parties 592 | :return: dictionary from party name to index, where the entry "self" contains this party's 593 | index 594 | """ 595 | success = False 596 | list_to_sort: list[tuple[str, int]] = [] 597 | attempt = 0 598 | while not success: 599 | success = True 600 | attempt += 1 601 | 602 | # generate random number 603 | random_number_self = randint(0, 1000000) 604 | 605 | # send random number to all other parties 606 | pool.async_broadcast( 607 | random_number_self, msg_id=f"distributed_keygen_random_number#{attempt}" 608 | ) 609 | 610 | # receive random numbers from the other parties 611 | responses = await pool.recv_all( 612 | msg_id=f"distributed_keygen_random_number#{attempt}" 613 | ) 614 | 615 | list_to_sort = [("self", random_number_self)] 616 | for party, random_number_party in responses: 617 | if random_number_party not in [rn for _, rn in list_to_sort]: 618 | list_to_sort.append((party, random_number_party)) 619 | else: 620 | success = False 621 | 622 | # sort the list based on the random numbers 623 | sorted_list = sorted(list_to_sort, key=lambda j: j[1]) 624 | party_indices = {} 625 | 626 | # extract the party names from the sorted list and assign an index based on the position. 627 | # this dictionary should be the same for each party 628 | for index, party in enumerate([party_name for party_name, _ in sorted_list]): 629 | party_indices[party] = index + 1 630 | 631 | session_id = sum(i[1] for i in sorted_list) % 1000000 632 | 633 | return party_indices, session_id 634 | 635 | @classmethod 636 | def __init_shamir_scheme( 637 | cls, prime_length: int, number_of_players: int, corruption_threshold: int 638 | ) -> Shamir: 639 | """ 640 | Function to initialize the regular Shamir scheme 641 | 642 | :param prime_length: bit length of the shamir prime 643 | :param number_of_players: number of parties involved in total (n) 644 | :param corruption_threshold: number of parties allowed to be corrupted 645 | :return: Shamir secret sharing scheme 646 | """ 647 | shamir_length = 2 * (prime_length + math.ceil(math.log2(number_of_players))) 648 | shamir_scheme = Shamir( 649 | sympy.nextprime(2**shamir_length), 650 | number_of_players, 651 | corruption_threshold, 652 | ) 653 | return shamir_scheme 654 | 655 | @classmethod 656 | async def generate_keypair( 657 | cls, 658 | stat_sec_shamir: int, 659 | number_of_players: int, 660 | corruption_threshold: int, 661 | shares: Shares, 662 | index: int, 663 | pool: Pool, 664 | prime_list: list[int], 665 | prime_length: int, 666 | party_indices: dict[str, int], 667 | correct_param_biprime: int, 668 | shamir_scheme_t: Shamir, 669 | shamir_scheme_2t: Shamir, 670 | session_id: int, 671 | batch_size: int = 1, 672 | ) -> tuple[PaillierPublicKey, PaillierSharedKey]: 673 | """ 674 | Function to distributively generate a shared secret key and a corresponding public key 675 | 676 | :param stat_sec_shamir: security parameter for Shamir secret sharing over the integers 677 | :param number_of_players: number of parties involved in the protocol 678 | :param corruption_threshold: number of parties that are allowed to be corrupted 679 | :param shares: dictionary that keeps track of shares for parties for certain numbers 680 | :param index: index of this party 681 | :param pool: network of involved parties 682 | :param prime_list: list of prime numbers 683 | :param prime_length: desired bit length of $p$ and $q$ 684 | :param party_indices: mapping from party names to indices 685 | :param correct_param_biprime: correctness parameter that affects the certainty that the 686 | generated $N$ is a product of two primes 687 | :param shamir_scheme_t: $t$-out-of-$n$ Shamir secret sharing scheme 688 | :param shamir_scheme_2t: $2t$-out-of-$n$ Shamir secret sharing scheme 689 | :param session_id: The unique session identifier belonging to the protocol that generated 690 | the keys for this DistributedPaillier scheme. 691 | :param batch_size: How many $p$'s and $q$'s to generate at once (drastically 692 | reduces communication at the expense of potentially wasted computation) 693 | :return: regular Paillier public key and a shared secret key 694 | """ 695 | secret_key = await cls.generate_secret_key( 696 | stat_sec_shamir, 697 | number_of_players, 698 | corruption_threshold, 699 | shares, 700 | index, 701 | pool, 702 | prime_list, 703 | prime_length, 704 | party_indices, 705 | correct_param_biprime, 706 | shamir_scheme_t, 707 | shamir_scheme_2t, 708 | session_id, 709 | batch_size, 710 | ) 711 | modulus = secret_key.n 712 | public_key = PaillierPublicKey(modulus, modulus + 1) 713 | 714 | logger.info("Key generation complete") 715 | return public_key, secret_key 716 | 717 | @classmethod 718 | async def _generate_pq( 719 | cls, 720 | pool: Pool, 721 | index: int, 722 | prime_length: int, 723 | party_indices: dict[str, int], 724 | shamir_scheme_t: Shamir, 725 | shamir_scheme_2t: Shamir, 726 | session_id: int, 727 | batch_size: int = 1, 728 | msg_id: str = "", 729 | ) -> tuple[ 730 | Batched[ShamirVariable], 731 | Batched[ShamirVariable], 732 | Batched[ShamirVariable], 733 | list[int], 734 | list[int], 735 | ]: 736 | """ 737 | Generate secretively two random prime candidates $p$ and $q$. 738 | 739 | These primes must be tested for primality before using the modulus N=pq. 740 | 741 | The number q is picked such that q = 3 mod 4, as this is needed for the 742 | biprimality test. 743 | 744 | This method supports batching (set batch_size > 1) to generate and share 745 | multiple $p$'s and $q$'s in one go. This potentially speeds up the key 746 | generation as less communication is required. Setting the batch_size to 747 | high will mean that potentially more p and q pairs are generated than 748 | needed, resulting in wasted computation. The batch_size is a trade-off 749 | between wasted computation and reduced communication. 750 | 751 | :param pool: network of involved parties 752 | :param index: index of this party 753 | :param prime_length: desired bit length of $p$ and $q$ 754 | :param party_indices: mapping from party names to indices 755 | :param shamir_scheme_t: $t$-out-of-$n$ Shamir secret sharing scheme 756 | :param shamir_scheme_2t: $2t$-out-of-$n$ Shamir secret sharing scheme 757 | :param session_id: The unique session identifier belonging to the protocol that generated 758 | the keys for this DistributedPaillier scheme. 759 | :param batch_size: How many $p$'s and $q$'s to generate at once (drastically 760 | reduces communication at the expense of potentially wasted computation) 761 | :param msg_id: prefix used for the message id 762 | :return: sharings of $p$ and $q$ 763 | """ 764 | 765 | def x_j(x: str, j: int, shamir: Shamir) -> Batched[ShamirVariable]: 766 | r""" 767 | Helper function to generate a ShamirVariable with label $x_j$, owned 768 | by party $j$. 769 | 770 | :param x: label of the variable 771 | :param j: index of the party 772 | :return: Batched[ShamirVariable] with label $x_j$ 773 | """ 774 | return Batched( 775 | ShamirVariable(shamir=shamir, label=f"{x}_{j}", owner=j), 776 | batch_size=batch_size, 777 | ) 778 | 779 | # We store all required Variables in a list to easily merge 780 | # communication for these Variables into the same message. 781 | group: list[Batched[ShamirVariable]] = [] 782 | 783 | # Generate the local additive share of p and q, respectively p_i and q_i 784 | p_i = Batched( 785 | ShamirVariable( 786 | shamir=shamir_scheme_t, label="p_" + str(index), owner=index 787 | ), 788 | batch_size=batch_size, 789 | ) 790 | p_i.set_plaintexts( 791 | [ 792 | cls._generate_prime_candidate(index, prime_length) 793 | for _ in range(batch_size) 794 | ] 795 | ) 796 | # Generate the local additive share of q, namely q_i 797 | q_i = Batched( 798 | ShamirVariable( 799 | shamir=shamir_scheme_t, label="q_" + str(index), owner=index 800 | ), 801 | batch_size=batch_size, 802 | ) 803 | q_i.set_plaintexts( 804 | [ 805 | cls._generate_prime_candidate(index, prime_length) 806 | for _ in range(batch_size) 807 | ] 808 | ) 809 | # Generate a local additive share of 0, namely 0_i 810 | zero_i = Batched( 811 | ShamirVariable( 812 | shamir=shamir_scheme_2t, label="zero_" + str(index), owner=index 813 | ), 814 | batch_size=batch_size, 815 | ) 816 | zero_i.set_plaintexts([0 for _ in range(batch_size)]) 817 | 818 | group.append(p_i) 819 | group.append(q_i) 820 | group.append(zero_i) 821 | # Create variables to represent the p_i of all other parties and 822 | # store their shares (idem for q_i and 0_i) 823 | other_parties = [_ for _ in party_indices.values() if _ != index] 824 | group.extend([x_j("p", _, shamir_scheme_t) for _ in other_parties]) 825 | group.extend([x_j("q", _, shamir_scheme_t) for _ in other_parties]) 826 | group.extend([x_j("zero", _, shamir_scheme_2t) for _ in other_parties]) 827 | 828 | # Create sharings of p_i to send to other parties 829 | p_i.share(index) 830 | q_i.share(index) 831 | zero_i.share(index) 832 | 833 | # Exchange shares of all p_i 834 | # We send over one share of our p_i to each party 835 | # And we receive one share per p_i of each other party 836 | shamir_msg_id = msg_id or f"distributed_keygen_session#{session_id}_shamir" 837 | await exchange_shares(group, index, pool, party_indices, msg_id=shamir_msg_id) 838 | 839 | # p = sum(p_i) 840 | p_i__s = [v for v in group if v.label.startswith("p_")] 841 | p = sum(p_i__s[1:], p_i__s[0]) 842 | # q = sum(q_i) 843 | q_i__s = [v for v in group if v.label.startswith("q_")] 844 | q = sum(q_i__s[1:], q_i__s[0]) 845 | # zero = sum(zero_i) 846 | zero_i__s = [v for v in group if v.label.startswith("zero_")] 847 | zero = sum(zero_i__s[1:], zero_i__s[0]) 848 | 849 | # We also return our local additive share of P (p_i) 850 | p_additive = [p_i[_].get_plaintext() for _ in range(batch_size)] 851 | q_additive = [q_i[_].get_plaintext() for _ in range(batch_size)] 852 | 853 | return p, q, zero, p_additive, q_additive 854 | 855 | @classmethod 856 | def _generate_prime_candidate(cls, index: int, prime_length: int) -> int: 857 | r""" 858 | Generate a random value between $2^(\text{length}-1)$ and 2^\text{length}. 859 | the function will ensure that the random 860 | value is equal to $3 \mod 4$ for the fist player, and to $0 \mod 4$ for all 861 | other players. 862 | This is necessary to generate additive shares of $p$ and $q$, or the 863 | bi-primality test will not work. 864 | 865 | :param index: index of this party 866 | :param prime_length: desired bit length of primes $p$ and $q$ 867 | :return: a random integer of the desired bit length and value modulo $4$ 868 | """ 869 | if index == 1: 870 | mod4 = 3 871 | else: 872 | mod4 = 0 873 | 874 | random_number = secrets.randbits(prime_length - 3) << 2 875 | additive_share: int = 2 ** (prime_length - 1) + random_number + mod4 876 | return additive_share 877 | 878 | @classmethod 879 | def int_shamir_share_and_send( 880 | cls, 881 | content: str, 882 | shares: Shares, 883 | int_shamir_scheme: IntegerShamir, 884 | index: int, 885 | pool: Pool, 886 | party_indices: dict[str, int], 887 | msg_id: str | None = None, 888 | ) -> None: 889 | r""" 890 | Create a secret-sharing of the input value, and send each share to 891 | the corresponding player, together with the label content 892 | 893 | :param content: string identifying the number to be shared and sent 894 | :param shares: dictionary keeping track of shares for different parties and numbers 895 | :param int_shamir_scheme: Shamir secret sharing scheme over the integers 896 | :param index: index of this party 897 | :param pool: network of involved parties 898 | :param party_indices: mapping from party names to indices 899 | :param msg_id: Optional message id. 900 | :raise NotImplementedError: In case the given content is not "lambda\_" or "beta". 901 | """ 902 | # retrieve the local additive share for content 903 | value = asdict(shares)[content]["additive"] 904 | 905 | # create a shamir sharing of this value 906 | value_sharing = int_shamir_scheme.share_secret(value) 907 | 908 | # Save this player's shamir share of the local additive share 909 | if content == "lambda_": 910 | shares.lambda_.shares[index] = value_sharing.shares[index] 911 | elif content == "beta": 912 | shares.beta.shares[index] = value_sharing.shares[index] 913 | else: 914 | raise NotImplementedError( 915 | f"Don't know what to do with this content: {content}" 916 | ) 917 | 918 | # Send the other players' shares of the local additive share 919 | other_parties = pool.pool_handlers.keys() 920 | for party in other_parties: 921 | party_share = value_sharing.shares[party_indices[party]] 922 | pool.asend(party, {"content": content, "value": party_share}, msg_id=msg_id) 923 | 924 | @classmethod 925 | def __int_add_received_shares( 926 | cls, 927 | content: str, 928 | int_shamir_scheme: IntegerShamir, 929 | shares: Shares, 930 | index: int, 931 | corruption_threshold: int, 932 | ) -> IntegerShares: 933 | """ 934 | Fetch shares labeled with content and add them to own_share_value. 935 | 936 | :param content: string identifying the number to be retrieved 937 | :param int_shamir_scheme: Shamir secret sharing scheme over the integers 938 | :param shares: dictionary keeping track of shares for different parties and numbers 939 | :param index: index of this party 940 | :param corruption_threshold: number of parties that are allowed to be corrupted 941 | :return: sum of the integer sharing of the number identified by content 942 | """ 943 | integer_shares = [ 944 | IntegerShares( 945 | int_shamir_scheme, 946 | {index: v}, 947 | corruption_threshold, 948 | scaling=math.factorial(int_shamir_scheme.number_of_parties), 949 | ) 950 | for v in asdict(shares)[content]["shares"].values() 951 | ] 952 | for i in range(1, len(integer_shares)): 953 | integer_shares[0] += integer_shares[i] 954 | return integer_shares[0] 955 | 956 | @classmethod 957 | async def gather_shares( 958 | cls, 959 | content: str, 960 | pool: Pool, 961 | shares: Shares, 962 | party_indices: dict[str, int], 963 | msg_id: str | None = None, 964 | ) -> None: 965 | r""" 966 | Gather all shares with label content 967 | 968 | :param content: string identifying a number 969 | :param pool: network of involved parties 970 | :param shares: dictionary keeping track of shares of different parties for certain numbers 971 | :param party_indices: mapping from party names to indices 972 | :param msg_id: Optional message id. 973 | :raise AttributeError: In case the given content is not any of the possible values 974 | for which we store shares ("p", "q", "n", "biprime", "lambda\_", "beta", "secret_key"). 975 | """ 976 | shares_from_other_parties = await pool.recv_all(msg_id=msg_id) 977 | for party, message in shares_from_other_parties: 978 | # Check if received content corresponds to the expected content 979 | msg_content = message["content"] 980 | err_msg = f"received a share for {msg_content}, but expected {content}" 981 | assert msg_content == content, err_msg 982 | 983 | # Check that the identifier 'content' exists in the Shares object 984 | try: 985 | value = getattr(shares, content) 986 | except AttributeError as e: 987 | err_msg = f"Don't know what to do with this content: {content}" 988 | raise AttributeError(err_msg) from e 989 | 990 | if isinstance(value, list): 991 | assert isinstance( 992 | message["value"], list 993 | ), "The value {content} is stored as a list (to support batching) but the received message is not a list." 994 | 995 | for i, v in enumerate(value): 996 | v.shares[party_indices[party]] = message["value"][i] 997 | else: 998 | value.shares[party_indices[party]] = message["value"] 999 | 1000 | @classmethod 1001 | async def __biprime_test_g_generation( 1002 | cls, 1003 | correct_param_biprime: int, 1004 | index: int, 1005 | candidate_n_list: list[int], 1006 | party_indices: dict[str, int], 1007 | pool: Pool, 1008 | msg_id: str, 1009 | ) -> list[list[int]]: 1010 | r""" 1011 | Function to generate the random $g$ values used for biprimality test of the entire batch of $N$ values. 1012 | 1013 | The $g$ is jointly picked at random. We pick more generators than needed 1014 | to ensure sufficient values with a $\operatorname{JacobiSymbol}(g/N)=1$. 1015 | In a later step we check if we can generate sufficient values. 1016 | 1017 | We can batch the joint picking of $g$ in a single communication round to optimize 1018 | communication at the expesive of wasted computational resources. 1019 | 1020 | :param correct_param_biprime: correctness parameter that affects the 1021 | certainty that the generated modulus is biprime 1022 | :param pool: network of involved parties 1023 | :param index: index of this party 1024 | :param party_indices: mapping from party name to indices 1025 | :param msg_id: Message id. 1026 | :return: a list of jointly picked $g$ values 1027 | """ 1028 | batch_g_size = correct_param_biprime * JACOBI_CORRECTION_FACTOR 1029 | 1030 | large_batch_of_gs = [] 1031 | for candidate_n in candidate_n_list: 1032 | # The parties must agree on a random number g for each candidate n 1033 | # Therefore every party picks a random number and sets it as its local 1034 | # additive share of g 1035 | batched_g_sharing = Batched( 1036 | AdditiveVariable(label="biprime", modulus=candidate_n), 1037 | batch_size=batch_g_size, 1038 | ) 1039 | 1040 | batched_g_sharing.set_share( 1041 | index, 1042 | [randint(0, candidate_n) for _ in range(batch_g_size)], 1043 | ) 1044 | large_batch_of_gs.append(batched_g_sharing) 1045 | 1046 | # The parties exchange their additive shares of g 1047 | await exchange_reconstruct( 1048 | large_batch_of_gs, index, pool, party_indices, msg_id=f"{msg_id}_g" 1049 | ) 1050 | # We reconstruct by adding the shares modulo N 1051 | batched_g: list[list[int]] = [ 1052 | batched_g_sharing.reconstruct() for batched_g_sharing in large_batch_of_gs 1053 | ] 1054 | return batched_g 1055 | 1056 | @classmethod 1057 | def __biprime_test_v_calculation( 1058 | cls, 1059 | g_values: list[int], 1060 | index: int, 1061 | modulus: int, 1062 | p_i: int, 1063 | q_i: int, 1064 | correct_param_biprime: int, 1065 | ) -> Batched[AdditiveVariable]: 1066 | r""" 1067 | Function to calculate the $v$ values for the biprimality test of each $N$. 1068 | 1069 | For the the biprimality test we calculate $v$ values. The $v$ values are based on the $g$ values generated in `biprime_test_g_generation`. $g$ values with a $\operatorname{JacobiSymbol}(g/N)!=1$ are skipped. We need at least `correct_param_biprime` values for the biprime test of $N$. The $v$ values are calculated for each $N$. 1070 | 1071 | :param g_values: The $g$ values generated by `biprime_test_g_generation` 1072 | :param index: index of this party 1073 | :param modulus: the modulus $N$ 1074 | :param p_i: The p share of the corresponding modulus $N$. 1075 | :param q_i: The q share of the correcsponding modulus $N$. 1076 | :param correct_param_biprime: correctness parameter that affects the 1077 | certainty that the generated modulus is biprime 1078 | :return: `correct_param_biprime` number of $v$ values. 1079 | """ 1080 | v_values: list[int] = [] 1081 | N = modulus 1082 | # Every party calculates their value of v_i where i is the index of the 1083 | # party 1084 | for g in g_values: 1085 | # no need to compute more values than needed 1086 | if len(v_values) == correct_param_biprime: 1087 | break 1088 | 1089 | if sympy.jacobi_symbol(g, modulus) != 1: 1090 | # We check if the Jacobi symbol of g and N is 1 1091 | continue 1092 | if index == 1: 1093 | # The party with index 1 calculates v_1 1094 | v = int(pow_mod(g, (N - p_i - q_i + 1) // 4, N)) 1095 | else: 1096 | # The other parties calculate v_i 1097 | v = int(pow_mod(g, (p_i + q_i) // 4, N)) 1098 | 1099 | v_values.append(v) 1100 | 1101 | # Though we don't care for the sum of the v_i's, we use the 1102 | # AdditiveVariable named 'v' to easily exchange the values of v_i 1103 | batched_v_i = Batched( 1104 | AdditiveVariable(label="v", modulus=modulus), 1105 | batch_size=correct_param_biprime, 1106 | ) 1107 | batched_v_i.set_share(index, v_values) 1108 | return batched_v_i 1109 | 1110 | @classmethod 1111 | def __biprime_test_with_v_i( 1112 | cls, 1113 | batched_v_i: Batched[AdditiveVariable], 1114 | modulus: int, 1115 | correct_param_biprime: int, 1116 | party_indices: dict[str, int], 1117 | ) -> bool: 1118 | r""" 1119 | Function to test for biprimality of $N$. 1120 | 1121 | To test the biprimality of $N$, we need to successfully perform a certain 1122 | number of tests (set by 'correct_param_biprime'). All tests need to 1123 | succeed successively, if any test fails we return False (early return). 1124 | 1125 | We can batch multiple tests in a single communication round to optimize 1126 | communication at the expesive of wasted computational resources. 1127 | 1128 | The more tests we perform in a batch, the more likely it is that we can 1129 | clear a biprime in a single batch. However, the more tests we perform in 1130 | a batch, the more computational resources we waste because of early 1131 | returns (e.g. Test 2 returns False). 1132 | 1133 | :param v_values: the v values calculated in `biprime_test_v_calculateion` for this $n$ 1134 | :param modulus: the modulus $N$ 1135 | :param correct_param_biprime: correctness parameter that affects the 1136 | certainty that the generated modulus is biprime 1137 | :param party_indices: mapping from party name to indices 1138 | :return: true if the test succeeds and false if it fails 1139 | """ 1140 | biprime_test_attempts = 0 1141 | successful_biprime_tests = 0 1142 | 1143 | for v_i in batched_v_i.variables: 1144 | biprime_test_attempts += 1 1145 | 1146 | # Test whether a primality check holds 1147 | product = 1 1148 | sharing = {i: v_i.get_share(i) for i in party_indices.values()} 1149 | for key, value in sharing.items(): 1150 | if key != 1: 1151 | product *= value 1152 | value1 = v_i.get_share(1) 1153 | 1154 | # The below test determines if N is "probably" the product of two primes (if the 1155 | # statement is True). Otherwise, N is definitely not the product of two primes. 1156 | success = ((value1 % modulus) == (product % modulus)) or ( 1157 | (value1 % modulus) == (-product % modulus) 1158 | ) 1159 | 1160 | if not success: 1161 | logger.debug( 1162 | f"Biprime test failed! Took {biprime_test_attempts} attempts" 1163 | ) 1164 | return False 1165 | 1166 | successful_biprime_tests += 1 1167 | 1168 | if successful_biprime_tests >= correct_param_biprime: 1169 | logger.debug( 1170 | f"Biprime test succeeded! Took {biprime_test_attempts} attempts" 1171 | ) 1172 | return True 1173 | 1174 | # not enough batched v_i available with jacobi symbol of 1 1175 | return False 1176 | 1177 | @classmethod 1178 | def __generate_lambda_addit_share( 1179 | cls, 1180 | index: int, 1181 | modulus: int, 1182 | shares: Shares, 1183 | ) -> int: 1184 | """ 1185 | Function to generate an additive share of lambda 1186 | 1187 | :param index: index of this party 1188 | :param modulus: modulus $N$ 1189 | :param shares: dictionary keeping track of shares for different parties for certain numbers 1190 | :return: additive share of lambda 1191 | """ 1192 | if index == 1: 1193 | return modulus - shares.p.additive - shares.q.additive + 1 1194 | # else 1195 | return 0 - shares.p.additive - shares.q.additive 1196 | 1197 | @classmethod 1198 | def __small_prime_divisors_test(cls, prime_list: list[int], modulus: int) -> bool: 1199 | """ 1200 | Function to test $N$ for small prime divisors 1201 | 1202 | :param prime_list: list of prime numbers 1203 | :param modulus: modulus $N$ 1204 | :return: true if $N$ has small divisors and false otherwise 1205 | """ 1206 | for prime in prime_list: 1207 | if modulus % prime == 0: 1208 | return True 1209 | return False 1210 | 1211 | @classmethod 1212 | async def compute_modulus( 1213 | cls, 1214 | shares: Shares, 1215 | index: int, 1216 | pool: Pool, 1217 | prime_list: list[int], 1218 | party_indices: dict[str, int], 1219 | prime_length: int, 1220 | shamir_scheme_t: Shamir, 1221 | shamir_scheme_2t: Shamir, 1222 | correct_param_biprime: int, 1223 | session_id: int, 1224 | batch_size: int = 1, 1225 | ) -> int: 1226 | r""" 1227 | Function that starts a protocol to generate candidates for $p$ and $q$ the multiplication of the two is then checked for biprimality to ensure it is a valid modulus. This is run until it succeeds. 1228 | 1229 | :param shares: dictionary that keeps track of shares for parties for certain numbers 1230 | :param index: index of this party 1231 | :param pool: network of involved parties 1232 | :param prime_list: list of prime numbers 1233 | :param party_indices: mapping from party names to indices 1234 | :param prime_length: desired bit length of $p$ and $q$ 1235 | :param shamir_scheme_t: $t$-out-of-$n$ Shamir secret sharing scheme 1236 | :param shamir_scheme_2t: $2t$-out-of-$n$ Shamir secret sharing scheme 1237 | :param correct_param_biprime: correctness parameter that affects the certainty that the 1238 | generated $N$ is a product of two primes 1239 | :param session_id: The unique session identifier belonging to the protocol that generated 1240 | the keys for this DistributedPaillier scheme. 1241 | :param batch_size: How many $p$'s and $q$'s to generate at once (drastically 1242 | reduces communication at the expense of potentially wasted computation) 1243 | :raises RuntimeError: thrown if the protocol is interrupted 1244 | :return: modulus $N$ 1245 | """ 1246 | sp_err_counter = 0 1247 | bip_err_counter = 0 1248 | 1249 | bip = False 1250 | rounds = 0 1251 | 1252 | while not bip: 1253 | rounds += 1 1254 | 1255 | # secreting sharings of p and q 1256 | ( 1257 | prime_candidate_p, 1258 | prime_candidate_q, 1259 | zero, 1260 | p_additive, 1261 | q_additive, 1262 | ) = await cls._generate_pq( 1263 | pool, 1264 | index, 1265 | prime_length, 1266 | party_indices, 1267 | shamir_scheme_t, 1268 | shamir_scheme_2t, 1269 | session_id, 1270 | batch_size=batch_size, 1271 | msg_id=f"distributed_keygen_session#{session_id}_generate_pq_{rounds}", 1272 | ) 1273 | 1274 | candidate_n: Batched[ShamirVariable] = prime_candidate_p * prime_candidate_q 1275 | 1276 | # Add 0-share to fix distribution 1277 | candidate_n += zero 1278 | 1279 | # Reconstruct n 1280 | msg_id = f"distributed_keygen_session#{session_id}_n_{rounds}" 1281 | await exchange_reconstruct( 1282 | candidate_n, index, pool, party_indices, msg_id=msg_id 1283 | ) 1284 | candidate_n_plaintext: list[int] = candidate_n.reconstruct() 1285 | zipped_list = zip( 1286 | candidate_n_plaintext, prime_candidate_q, p_additive, q_additive 1287 | ) 1288 | candidate_n_small_prime_tested = [ 1289 | (n, prime_candidate_q, p_additive, q_additive) 1290 | for (n, prime_candidate_q, p_additive, q_additive) in zipped_list 1291 | if not cls.__small_prime_divisors_test(prime_list, n) 1292 | ] 1293 | sp_err_counter += len(candidate_n_plaintext) - len( 1294 | candidate_n_small_prime_tested 1295 | ) 1296 | 1297 | if len(candidate_n_small_prime_tested) == 0: 1298 | continue 1299 | 1300 | g_values = await cls.__biprime_test_g_generation( 1301 | correct_param_biprime, 1302 | index, 1303 | [n for (n, _, _, _) in candidate_n_small_prime_tested], 1304 | party_indices, 1305 | pool, 1306 | f"distributed_keygen_session#{session_id}_biprime_test_g_{rounds}", 1307 | ) 1308 | 1309 | candidate_n_small_prime_tests_with_g = [ 1310 | (gs,) + candidate 1311 | for gs, candidate in zip(g_values, candidate_n_small_prime_tested) 1312 | ] 1313 | list_to_exchange_v_i = [ 1314 | cls.__biprime_test_v_calculation( 1315 | g_values, 1316 | index, 1317 | n, 1318 | p_additive, 1319 | q_additive, 1320 | correct_param_biprime, 1321 | ) 1322 | for ( 1323 | g_values, 1324 | n, 1325 | _, 1326 | p_additive, 1327 | q_additive, 1328 | ) in candidate_n_small_prime_tests_with_g 1329 | ] 1330 | 1331 | await exchange_reconstruct( 1332 | list_to_exchange_v_i, 1333 | index, 1334 | pool, 1335 | party_indices, 1336 | msg_id=f"distributed_keygen_session#{session_id}_biprime_test_v_{rounds}_v", 1337 | ) 1338 | 1339 | for i, (n, prime_candidate_q_i, p_i_additive, q_i_additive) in enumerate( 1340 | candidate_n_small_prime_tested 1341 | ): 1342 | # Once we found a modulus that is biprime, we will needs the 1343 | # shares of p and q to generate the secret key later on 1344 | shares.p = Shares.P(p_i_additive, prime_candidate_q_i.get_shares()) 1345 | shares.q = Shares.Q(q_i_additive, prime_candidate_q_i.get_shares()) 1346 | 1347 | batched_v_i = list_to_exchange_v_i[i] 1348 | bip = cls.__biprime_test_with_v_i( 1349 | batched_v_i, n, correct_param_biprime, party_indices 1350 | ) 1351 | 1352 | if not bip: 1353 | bip_err_counter += 1 1354 | else: 1355 | logger.info(f"N = {n}") 1356 | logger.info( 1357 | f"Checked {sp_err_counter} primes for small prime divisors in {rounds} rounds" 1358 | ) 1359 | logger.info(f"Checked {bip_err_counter} candidates for biprimality") 1360 | return n 1361 | 1362 | raise RuntimeError("Could not generate a valid modulus") 1363 | 1364 | @classmethod 1365 | async def generate_secret_key( 1366 | cls, 1367 | stat_sec_shamir: int, 1368 | number_of_players: int, 1369 | corruption_threshold: int, 1370 | shares: Shares, 1371 | index: int, 1372 | pool: Pool, 1373 | prime_list: list[int], 1374 | prime_length: int, 1375 | party_indices: dict[str, int], 1376 | correct_param_biprime: int, 1377 | shamir_scheme_t: Shamir, 1378 | shamir_scheme_2t: Shamir, 1379 | session_id: int, 1380 | batch_size: int, 1381 | ) -> PaillierSharedKey: 1382 | """ 1383 | Functions that generates the modulus and sets up the sharing of the private key 1384 | 1385 | :param stat_sec_shamir: security parameter for the Shamir secret sharing over the integers 1386 | :param number_of_players: total number of participants in this session (including self) 1387 | :param corruption_threshold: Maximum number of allowed corruptions 1388 | :param shares: dictionary that keeps track of shares for parties for certain numbers 1389 | :param index: index of this party 1390 | :param pool: network of involved parties 1391 | :param prime_list: list of prime numbers 1392 | :param prime_length: desired bit length of $p$ and $q$ 1393 | :param party_indices: mapping from party names to indices 1394 | :param correct_param_biprime: correctness parameter that affects the certainty that the 1395 | generated $N$ is a product of two primes 1396 | :param shamir_scheme_t: $t$-out-of-$n$ Shamir secret sharing scheme 1397 | :param shamir_scheme_2t: $2t$-out-of-$n$ Shamir secret sharing scheme 1398 | :param session_id: The unique session identifier belonging to the protocol that generated 1399 | the keys for this DistributedPaillier scheme. 1400 | :param batch_size: How many $p$'s and $q$'s to generate at once (drastically 1401 | reduces communication at the expense of potentially wasted computation) 1402 | :return: shared secret key 1403 | """ 1404 | 1405 | modulus = await cls.compute_modulus( 1406 | shares, 1407 | index, 1408 | pool, 1409 | prime_list, 1410 | party_indices, 1411 | prime_length, 1412 | shamir_scheme_t, 1413 | shamir_scheme_2t, 1414 | correct_param_biprime, 1415 | session_id, 1416 | batch_size, 1417 | ) 1418 | int_shamir_scheme = IntegerShamir( 1419 | stat_sec_shamir, 1420 | modulus, 1421 | number_of_players, 1422 | corruption_threshold, 1423 | ) 1424 | 1425 | shares.lambda_.additive = cls.__generate_lambda_addit_share( 1426 | index, modulus, shares 1427 | ) 1428 | shamir_msg_id = f"distributed_keygen_session#{session_id}_int_shamir" 1429 | cls.int_shamir_share_and_send( 1430 | "lambda_", 1431 | shares, 1432 | int_shamir_scheme, 1433 | index, 1434 | pool, 1435 | party_indices, 1436 | shamir_msg_id + "lambda", 1437 | ) 1438 | await cls.gather_shares( 1439 | "lambda_", pool, shares, party_indices, shamir_msg_id + "lambda" 1440 | ) 1441 | lambda_ = cls.__int_add_received_shares( 1442 | "lambda_", int_shamir_scheme, shares, index, corruption_threshold 1443 | ) 1444 | 1445 | secret_key_sharing: IntegerShares 1446 | while True: 1447 | shares.secret_key = Shares.SecretKey() 1448 | shares.beta = Shares.Beta() 1449 | shares.beta.additive = secrets.randbelow(modulus) 1450 | cls.int_shamir_share_and_send( 1451 | "beta", 1452 | shares, 1453 | int_shamir_scheme, 1454 | index, 1455 | pool, 1456 | party_indices, 1457 | shamir_msg_id + "beta", 1458 | ) 1459 | await cls.gather_shares( 1460 | "beta", pool, shares, party_indices, shamir_msg_id + "beta" 1461 | ) 1462 | beta = cls.__int_add_received_shares( 1463 | "beta", int_shamir_scheme, shares, index, corruption_threshold 1464 | ) 1465 | secret_key_sharing = lambda_ * beta 1466 | temp_secret_key = copy.deepcopy(secret_key_sharing) 1467 | temp_secret_key.shares = { 1468 | key: (value % modulus) for key, value in temp_secret_key.shares.items() 1469 | } 1470 | shares.secret_key.shares = temp_secret_key.shares 1471 | 1472 | pool.async_broadcast( 1473 | {"content": "secret_key", "value": temp_secret_key.shares[index]}, 1474 | msg_id=f"distributed_keygen_session#{session_id}_sk", 1475 | ) 1476 | await cls.gather_shares( 1477 | "secret_key", 1478 | pool, 1479 | shares, 1480 | party_indices, 1481 | msg_id=f"distributed_keygen_session#{session_id}_sk", 1482 | ) 1483 | reconstructed_secret_key = temp_secret_key.reconstruct_secret( 1484 | modulus=modulus 1485 | ) 1486 | theta = ( 1487 | reconstructed_secret_key 1488 | * math.factorial(int_shamir_scheme.number_of_parties) ** 3 1489 | ) % modulus 1490 | if math.gcd(theta, modulus) != 0: 1491 | break 1492 | 1493 | secret_key = PaillierSharedKey( 1494 | n=modulus, 1495 | t=corruption_threshold, 1496 | player_id=index, 1497 | theta=theta, 1498 | share=secret_key_sharing, 1499 | ) 1500 | return secret_key 1501 | 1502 | class StoredDistributedPaillier(TypedDict): 1503 | pub_key: PaillierPublicKey 1504 | priv_key: PaillierSharedKey 1505 | shares: Shares 1506 | precision: int 1507 | index: int 1508 | party_indices: dict[str, int] 1509 | corruption_threshold: int 1510 | 1511 | def store_private_key(self) -> bytes: 1512 | """ 1513 | Serialize the entire key including the private key to bytes, such that it can be stored for 1514 | later use. The key can be loaded using the function `load_private_key_from_bytes`. 1515 | 1516 | :return: byte object representing the key. 1517 | :raise ImportError: When the 'tno.mpc.communication' module is not installed 1518 | """ 1519 | if not COMMUNICATION_INSTALLED: 1520 | raise ImportError( 1521 | "Could not find the module 'tno.mpc.communication', which is needed for serialization" 1522 | ) 1523 | 1524 | object_to_serialize = { 1525 | "pub_key": self.public_key, 1526 | "priv_key": self.secret_key, 1527 | "precision": self.precision, 1528 | "index": self.index, 1529 | "party_indices": self.party_indices, 1530 | "corruption_threshold": self.corruption_threshold, 1531 | } 1532 | return Serialization.pack( 1533 | object_to_serialize, 1534 | msg_id="", 1535 | use_pickle=False, 1536 | option=DIST_KEY_STORAGE_PACK_OPTIONS, 1537 | ) 1538 | 1539 | @classmethod 1540 | async def load_private_key_from_bytes( 1541 | cls, obj_bytes: bytes, pool: Pool, distributed: bool 1542 | ) -> DistributedPaillier: 1543 | """ 1544 | Create a distributed paillier key from the bytes provided. The bytes must represent a distributed paillier key. The number of parties must be equal to the number of parties in a pool. 1545 | 1546 | :param obj_bytes: the bytes representing the key 1547 | :param pool: The pool used for the communication 1548 | :param distributed: Whether the different parties are run on different python instances 1549 | :return: The distributed paillier key derived from the obj_bytes 1550 | :raise ValueError: When the number of parties in the pool does not correspond to the number of parties expected by the key. 1551 | :raise ImportError: When the 'tno.mpc.communication' module is not installed 1552 | """ 1553 | if not COMMUNICATION_INSTALLED: 1554 | raise ImportError( 1555 | "Could not find the module 'tno.mpc.communication', which is needed for deserialization" 1556 | ) 1557 | 1558 | _, deserialized_dict = Serialization.unpack( 1559 | obj_bytes, False, ormsgpack.OPT_NON_STR_KEYS 1560 | ) 1561 | if isinstance(deserialized_dict, list): 1562 | raise TypeError("Expected a dict not a list") 1563 | deserialized = cast( 1564 | DistributedPaillier.StoredDistributedPaillier, deserialized_dict 1565 | ) 1566 | 1567 | if len(deserialized["party_indices"]) != len(pool.pool_handlers) + 1: 1568 | raise ValueError( 1569 | f"The number of parties in the pool ({len(pool.pool_handlers)+1} does not correspond with the number of parties expected by the key({len(deserialized)}." 1570 | ) 1571 | 1572 | _, session_id = await DistributedPaillier.get_indices(pool) 1573 | index = deserialized["party_indices"]["self"] 1574 | dist_paillier = DistributedPaillier( 1575 | deserialized["pub_key"], 1576 | deserialized["priv_key"], 1577 | deserialized["precision"], 1578 | pool, 1579 | index, 1580 | deserialized["party_indices"], 1581 | session_id, 1582 | distributed, 1583 | deserialized["corruption_threshold"], 1584 | ) 1585 | cls.__register_scheme(dist_paillier, distributed) 1586 | return dist_paillier 1587 | 1588 | class SerializedDistributedPaillier(Paillier.SerializedPaillier, TypedDict): 1589 | """ 1590 | Serialized DistributedPaillier for use with the communication module. 1591 | """ 1592 | 1593 | session_id: int 1594 | distributed: bool 1595 | index: int 1596 | 1597 | def serialize( 1598 | self, **_kwargs: Any 1599 | ) -> DistributedPaillier.SerializedDistributedPaillier: 1600 | r""" 1601 | Serialization function for Distributed Paillier schemes, which will be passed to 1602 | the communication module 1603 | 1604 | :param \**_kwargs: optional extra keyword arguments 1605 | :return: Dictionary containing the serialization of this DistributedPaillier scheme. 1606 | """ 1607 | return { 1608 | "session_id": self.session_id, 1609 | "distributed": self.distributed, 1610 | "index": self.index, 1611 | "prec": self.precision, 1612 | "pubkey": self.public_key, 1613 | } 1614 | 1615 | @overload 1616 | @staticmethod 1617 | def deserialize( 1618 | obj: DistributedPaillier.SerializedDistributedPaillier, 1619 | *, 1620 | origin: HTTPClient | None = ..., 1621 | **kwargs: Any, 1622 | ) -> DistributedPaillier: ... 1623 | 1624 | @overload 1625 | @staticmethod 1626 | def deserialize( 1627 | obj: Paillier.SerializedPaillier, 1628 | *, 1629 | origin: HTTPClient | None = ..., 1630 | **kwargs: Any, 1631 | ) -> Paillier: ... 1632 | 1633 | @staticmethod 1634 | def deserialize( 1635 | obj: ( 1636 | DistributedPaillier.SerializedDistributedPaillier 1637 | | Paillier.SerializedPaillier 1638 | ), 1639 | *, 1640 | origin: HTTPClient | None = None, 1641 | **kwargs: Any, 1642 | ) -> DistributedPaillier | Paillier: 1643 | r""" 1644 | Deserialization function for Distributed Paillier schemes, which will be passed to 1645 | the communication module 1646 | 1647 | :param obj: serialization of a distributed paillier scheme. 1648 | :param origin: HTTPClient representing where the message came from if applicable 1649 | :param \**kwargs: optional extra keyword arguments 1650 | :return: Deserialized DistributedPaillier scheme, local instance thereof, or a regular 1651 | Paillier scheme in case this party is not part of the distributed session. 1652 | """ 1653 | session_id = obj.get("session_id", None) 1654 | if isinstance(session_id, int): 1655 | if obj.get("distributed", False): 1656 | # The scheme should be stored in the local instances through the session ID 1657 | # If it is not, then this party was not part of the initial protocol 1658 | if session_id in DistributedPaillier._local_instances: 1659 | return DistributedPaillier._local_instances[session_id] 1660 | else: 1661 | # The scheme should be stored in the global instances through the session ID 1662 | # If it is not, then this party was not part of the initial protocol 1663 | index = obj.get("index", None) 1664 | if ( 1665 | isinstance(index, int) 1666 | and session_id in DistributedPaillier._global_instances[index] 1667 | ): 1668 | return DistributedPaillier._global_instances[index][session_id] 1669 | # This party is not part of the distributed session, so we parse it as a Paillier scheme 1670 | paillier_obj: Paillier.SerializedPaillier = { 1671 | "prec": obj["prec"], 1672 | "pubkey": obj["pubkey"], 1673 | } 1674 | return Paillier.deserialize(paillier_obj, origin=origin, **kwargs) 1675 | 1676 | # endregion 1677 | 1678 | 1679 | # Load the serialization logic into the communication module 1680 | if COMMUNICATION_INSTALLED: 1681 | try: 1682 | Serialization.register_class(DistributedPaillier, check_annotations=False) 1683 | except RepetitionError: 1684 | pass 1685 | -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/paillier_shared_key.py: -------------------------------------------------------------------------------- 1 | """ 2 | Paillier secret key that is shared amongst several parties. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from typing import Any, TypedDict 8 | 9 | # Check to see if the communication module is available 10 | try: 11 | from tno.mpc.communication import RepetitionError, Serialization 12 | 13 | COMMUNICATION_INSTALLED = True 14 | except ModuleNotFoundError: 15 | COMMUNICATION_INSTALLED = False 16 | 17 | from tno.mpc.encryption_schemes.paillier.paillier import PaillierCiphertext 18 | from tno.mpc.encryption_schemes.shamir import IntegerShares 19 | from tno.mpc.encryption_schemes.templates import SecretKey, SerializationError 20 | from tno.mpc.encryption_schemes.utils import mod_inv, pow_mod 21 | 22 | from tno.mpc.protocols.distributed_keygen.utils import mult_list 23 | 24 | 25 | class PaillierSharedKey(SecretKey): 26 | """ 27 | Class containing relevant attributes and methods of a shared paillier key. 28 | """ 29 | 30 | def __init__( 31 | self, n: int, t: int, player_id: int, share: IntegerShares, theta: int 32 | ) -> None: 33 | """ 34 | Initializes a Paillier shared key. 35 | 36 | :param n: modulus of the DistributedPaillier scheme this secret key belongs to 37 | :param t: corruption_threshold of the secret sharing 38 | :param player_id: the index of the player to whom the key belongs 39 | :param share: secret sharing of the exponent used during decryption 40 | :param theta: Value used in the computation of a full decryption after partial decryptions 41 | have been obtained. We refer to the paper for more details 42 | """ 43 | super().__init__() 44 | self.share = share 45 | self.n = n 46 | self.n_square = n * n 47 | self.t = t 48 | self.player_id = player_id 49 | self.theta = theta 50 | self.theta_inv = mod_inv(self.theta, self.n) 51 | 52 | def partial_decrypt(self, ciphertext: PaillierCiphertext) -> int: 53 | """ 54 | Function that does local computations to get a partial decryption of a ciphertext. 55 | 56 | :param ciphertext: ciphertext to be partially decrypted 57 | :raise TypeError: If the given ciphertext is not of type PaillierCiphertext. 58 | :raise ValueError: If the ciphertext is encrypted against a different key. 59 | :return: partial decryption of ciphertext 60 | """ 61 | 62 | if not isinstance(ciphertext, PaillierCiphertext): 63 | raise TypeError( 64 | f"Expected ciphertext to be a PaillierCiphertext not: {type(ciphertext)}" 65 | ) 66 | 67 | if self.n != ciphertext.scheme.public_key.n: 68 | raise ValueError("encrypted against a different key!") 69 | ciphertext_value = ciphertext.get_value() 70 | n_fac = self.share.n_fac 71 | other_honest_players = [ 72 | i + 1 for i in range(self.share.degree + 1) if i + 1 != self.player_id 73 | ] 74 | 75 | # NB: Here the reconstruction set is implicit defined, but any 76 | # large enough subset of shares will do. 77 | # reconstruction_shares = {key: shares[key] for key in list(shares.keys())[:degree + 1]} 78 | 79 | lagrange_interpol_enumerator = mult_list(other_honest_players) 80 | lagrange_interpol_denominator = mult_list( 81 | [(j - self.player_id) for j in other_honest_players] 82 | ) 83 | exp = ( 84 | n_fac * lagrange_interpol_enumerator * self.share.shares[self.player_id] 85 | ) // lagrange_interpol_denominator 86 | 87 | # Notice that the partial decryption is already raised to the power given 88 | # by the Lagrange interpolation coefficient 89 | if exp < 0: 90 | ciphertext_value = mod_inv(ciphertext_value, self.n_square) 91 | exp = -exp 92 | partial_decryption = pow_mod(ciphertext_value, exp, self.n_square) 93 | return partial_decryption 94 | 95 | def decrypt(self, partial_dict: dict[int, int]) -> int: 96 | r""" 97 | Function that uses partial decryptions of other parties to reconstruct a 98 | full decryption of the initial ciphertext. 99 | 100 | :param partial_dict: dictionary containing the partial decryptions of each party 101 | :raise ValueError: Either in case not enough shares are known in order to decrypt. 102 | Or when the combined decryption minus one is not divisible by $N$. This last case is 103 | most likely caused by the fact the ciphertext that is being decrypted, 104 | differs between parties. 105 | :return: full decryption 106 | """ 107 | 108 | partial_decryptions = [ 109 | partial_dict[i + 1] for i in range(self.share.degree + 1) 110 | ] 111 | 112 | if len(partial_decryptions) < self.share.degree + 1: 113 | raise ValueError("Not enough shares.") 114 | 115 | combined_decryption = ( 116 | mult_list(partial_decryptions[: self.share.degree + 1]) % self.n_square 117 | ) 118 | 119 | if (combined_decryption - 1) % self.n != 0: 120 | raise ValueError( 121 | "Combined decryption minus one is not divisible by N. This might be caused by the " 122 | "fact that the ciphertext that is being decrypted, differs between the parties." 123 | ) 124 | 125 | message = ((combined_decryption - 1) // self.n * self.theta_inv) % self.n 126 | 127 | return message 128 | 129 | # region Serialization logic 130 | 131 | class SerializedPaillierSharedKey(TypedDict): 132 | """ 133 | Serialized PaillierSharedKey for e.g. storing the key to disk. 134 | """ 135 | 136 | n: int 137 | t: int 138 | player_id: int 139 | share: IntegerShares 140 | theta: int 141 | 142 | def serialize( 143 | self, **_kwargs: Any 144 | ) -> PaillierSharedKey.SerializedPaillierSharedKey: 145 | r""" 146 | Serialization function for public keys, which will be passed to the communication module. 147 | 148 | :param \**_kwargs: optional extra keyword arguments 149 | :raise SerializationError: When communication library is not installed. 150 | :return: serialized version of this PaillierSharedKey. 151 | """ 152 | if not COMMUNICATION_INSTALLED: 153 | raise SerializationError() 154 | return { 155 | "n": self.n, 156 | "t": self.t, 157 | "player_id": self.player_id, 158 | "share": self.share, 159 | "theta": self.theta, 160 | } 161 | 162 | @staticmethod 163 | def deserialize( 164 | obj: PaillierSharedKey.SerializedPaillierSharedKey, **_kwargs: Any 165 | ) -> PaillierSharedKey: 166 | r""" 167 | Deserialization function for public keys, which will be passed to the communication module. 168 | 169 | :param obj: serialized version of a PaillierSharedKey. 170 | :param \**_kwargs: optional extra keyword arguments 171 | :raise SerializationError: When communication library is not installed. 172 | :return: Deserialized PaillierSharedKey from the given dict. 173 | """ 174 | if not COMMUNICATION_INSTALLED: 175 | raise SerializationError() 176 | return PaillierSharedKey( 177 | n=obj["n"], 178 | t=obj["t"], 179 | player_id=obj["player_id"], 180 | share=obj["share"], 181 | theta=obj["theta"], 182 | ) 183 | 184 | # endregion 185 | 186 | def __eq__(self, other: object) -> bool: 187 | """ 188 | Compare this PaillierSharedKey with another to determine (in)equality. 189 | 190 | :param other: Object to compare this PaillierSharedKey with. 191 | :raise TypeError: When other object is not a PaillierSharedKey. 192 | :return: Boolean value representing (in)equality of both objects. 193 | """ 194 | if not isinstance(other, PaillierSharedKey): 195 | raise TypeError( 196 | f"Expected comparison with another PaillierSharedKey, not {type(other)}" 197 | ) 198 | return ( 199 | self.share == other.share 200 | and self.n == other.n 201 | and self.t == other.t 202 | and self.player_id == other.player_id 203 | and self.theta == other.theta 204 | ) 205 | 206 | def __str__(self) -> str: 207 | """ 208 | Utility function to represent the local share of the private key as a string. 209 | 210 | :return: String representation of this private key part. 211 | """ 212 | return str( 213 | { 214 | "priv_shared_key": { 215 | "n": self.n, 216 | "t": self.t, 217 | "player_id": self.player_id, 218 | "theta": self.theta, 219 | "share": self.share, 220 | } 221 | } 222 | ) 223 | 224 | 225 | if COMMUNICATION_INSTALLED: 226 | try: 227 | Serialization.register_class(PaillierSharedKey) 228 | except RepetitionError: 229 | pass 230 | -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/py.typed -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing module of the tno.mpc.protocols.distributed_keygen library 3 | """ 4 | 5 | # Explicit re-export of all functionalities, such that they can be imported properly. Following 6 | # https://www.python.org/dev/peps/pep-0484/#stub-files and 7 | # https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-no-implicit-reexport 8 | from tno.mpc.protocols.distributed_keygen.test.conftest import ( 9 | fixture_distributed_schemes as fixture_distributed_schemes, 10 | ) 11 | -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test fixtures 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import asyncio 8 | import os.path 9 | from pathlib import Path 10 | from typing import Callable 11 | 12 | import pytest 13 | import pytest_asyncio 14 | from _pytest.fixtures import FixtureRequest 15 | 16 | from tno.mpc.communication import Pool, Serialization 17 | 18 | from tno.mpc.protocols.distributed_keygen import DistributedPaillier 19 | 20 | 21 | @pytest.fixture( 22 | name="pool_http", 23 | params=[3, 4, 5], 24 | ids=["3-party", "4-party", "5-party"], 25 | scope="module", 26 | ) 27 | def fixture_pool_http( 28 | request: FixtureRequest, 29 | http_pool_group_factory: Callable[[int], tuple[Pool, ...]], 30 | ) -> tuple[Pool, ...]: 31 | """ 32 | Creates a collection of 3, 4 and 5 communication pools 33 | 34 | :param http_pool_group_factory: Factory for generating a set of pools with configured 35 | connection of arbitrary size. 36 | :param request: A fixture request used to indirectly parametrize. 37 | :raise NotImplementedError: raised when based on the given param, no fixture can be created 38 | :return: a collection of communication pools 39 | """ 40 | return http_pool_group_factory(request.param) 41 | 42 | 43 | @pytest_asyncio.fixture( 44 | name="distributed_schemes_fresh", 45 | params=list(zip([0, 1], [1, 100])), 46 | ids=[ 47 | "corruption_threshold " + str(t) + "_batch_" + str(b) 48 | for t, b in list(zip([0, 1], [1, 100])) 49 | ], 50 | scope="module", 51 | ) 52 | async def fixture_distributed_schemes( 53 | pool_http: tuple[Pool, ...], 54 | request: FixtureRequest, 55 | ) -> tuple[DistributedPaillier, ...]: 56 | """ 57 | Constructs schemes to use for distributed key generation. 58 | 59 | :param pool_http: collection of communication pools 60 | :param request: Fixture request 61 | :return: a collection of schemes 62 | """ 63 | Serialization.register_class( 64 | DistributedPaillier, check_annotations=False, overwrite=True 65 | ) 66 | corruption_threshold: int = request.param[0] 67 | batch_size: int = request.param[1] 68 | 69 | key_length = 64 70 | prime_threshold = 200 71 | correct_param_biprime = 20 72 | stat_sec_shamir = 20 73 | distributed_schemes: tuple[DistributedPaillier, ...] = tuple( 74 | await asyncio.gather( 75 | *[ 76 | DistributedPaillier.from_security_parameter( 77 | pool, 78 | corruption_threshold, 79 | key_length, 80 | prime_threshold, 81 | correct_param_biprime, 82 | stat_sec_shamir, 83 | distributed=False, 84 | precision=8, 85 | batch_size=batch_size, 86 | ) 87 | for pool in pool_http 88 | ] 89 | ) 90 | ) 91 | return distributed_schemes 92 | 93 | 94 | @pytest_asyncio.fixture( 95 | name="distributed_schemes", 96 | params=list([0, 1]), 97 | ids=["corruption_threshold 0", "corruption_threshold 1"], 98 | scope="module", 99 | ) 100 | async def fixture_distributed_schemes_from_file( 101 | pool_http: tuple[Pool, ...], 102 | request: FixtureRequest, 103 | ) -> tuple[DistributedPaillier, ...]: 104 | """ 105 | Constructs schemes to use for distributed key generation. 106 | 107 | :param pool_http: collection of communication pools 108 | :param request: Fixture request 109 | :return: a collection of schemes 110 | """ 111 | Serialization.register_class( 112 | DistributedPaillier, check_annotations=False, overwrite=True 113 | ) 114 | corruption_threshold: int = request.param 115 | base_path = Path(f"{os.path.dirname(__file__)}/test_data") 116 | number_of_parties = len(pool_http) 117 | file_paths = [ 118 | base_path.joinpath( 119 | f"distributed_key_threshold_{corruption_threshold}_{number_of_parties}parties_{index}.obj" 120 | ) 121 | for index in range(number_of_parties) 122 | ] 123 | 124 | distributed_schemes: tuple[DistributedPaillier, ...] = tuple( 125 | await asyncio.gather( 126 | *[ 127 | DistributedPaillier.load_private_key_from_bytes( 128 | file_paths[index].read_bytes(), pool_http[index], False 129 | ) 130 | for index in range(number_of_parties) 131 | ] 132 | ) 133 | ) 134 | return distributed_schemes 135 | -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_3parties_0.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_3parties_0.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_3parties_1.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_3parties_1.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_3parties_2.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_3parties_2.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_4parties_0.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_4parties_0.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_4parties_1.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_4parties_1.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_4parties_2.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_4parties_2.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_4parties_3.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_4parties_3.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_5parties_0.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_5parties_0.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_5parties_1.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_5parties_1.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_5parties_2.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_5parties_2.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_5parties_3.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_5parties_3.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_5parties_4.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_0_5parties_4.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_3parties_0.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_3parties_0.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_3parties_1.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_3parties_1.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_3parties_2.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_3parties_2.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_4parties_0.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_4parties_0.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_4parties_1.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_4parties_1.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_4parties_2.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_4parties_2.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_4parties_3.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_4parties_3.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_5parties_0.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_5parties_0.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_5parties_1.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_5parties_1.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_5parties_2.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_5parties_2.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_5parties_3.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_5parties_3.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_5parties_4.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNO-MPC/protocols.distributed_keygen/c4e2a58b049527eef5650fc49aa1387815019204/src/tno/mpc/protocols/distributed_keygen/test/test_data/distributed_key_threshold_1_5parties_4.obj -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_distributed_keygen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests that can be run using pytest to test the distributed keygen functionality 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import asyncio 8 | import math 9 | from typing import Any, Sequence 10 | 11 | import pytest 12 | 13 | from tno.mpc.communication import Pool 14 | from tno.mpc.encryption_schemes.paillier import PaillierCiphertext 15 | from tno.mpc.encryption_schemes.paillier.paillier import Plaintext 16 | 17 | from tno.mpc.protocols.distributed_keygen import DistributedPaillier 18 | 19 | 20 | @pytest.mark.parametrize( 21 | "plaintext", [1, 2, 3, -1, -2, -3, 1.5, 42.42424242, -1.5, -42.42424242] 22 | ) 23 | @pytest.mark.asyncio 24 | async def test_distributed_paillier_with_communication( 25 | distributed_schemes: tuple[DistributedPaillier, ...], 26 | plaintext: float | int, 27 | ) -> None: 28 | """ 29 | Tests distributed encryption and decryption using communication 30 | 31 | :param distributed_schemes: a collection of schemes 32 | :param plaintext: plaintext to encrypt and decrypt 33 | """ 34 | enc = {0: distributed_schemes[0].encrypt(plaintext)} 35 | distributed_schemes[0].pool.async_broadcast(enc[0], "encryption") 36 | assert not enc[0].fresh 37 | for iplayer in range(1, len(distributed_schemes)): 38 | enc[iplayer] = await distributed_schemes[iplayer].pool.recv( 39 | "local0", "encryption" 40 | ) 41 | 42 | dec = await asyncio.gather( 43 | *[ 44 | distributed_schemes[i].decrypt(enc[i]) 45 | for i in range(len(distributed_schemes)) 46 | ] 47 | ) 48 | assert all(d == plaintext for d in dec) 49 | 50 | 51 | @pytest.mark.parametrize( 52 | "plaintext", [1, 2, 3, -1, -2, -3, 1.5, 42.42424242, -1.5, -42.42424242] 53 | ) 54 | @pytest.mark.asyncio 55 | async def test_distributed_paillier_serialization( 56 | distributed_schemes: tuple[DistributedPaillier, ...], 57 | plaintext: float | int, 58 | ) -> None: 59 | """ 60 | Tests serialization of the distributed Paillier. 61 | 62 | :param distributed_schemes: a collection of schemes 63 | :param plaintext: plaintext to encrypt 64 | """ 65 | enc = {0: distributed_schemes[0].encrypt(plaintext)} 66 | distributed_schemes[0].pool.async_broadcast(enc[0], "encryption") 67 | assert not enc[0].fresh 68 | distributed_schemes[0].pool.async_broadcast(distributed_schemes[0], "scheme") 69 | 70 | # check equality of received values 71 | for iplayer in range(1, len(distributed_schemes)): 72 | enc[iplayer] = await distributed_schemes[iplayer].pool.recv( 73 | "local0", "encryption" 74 | ) 75 | d_scheme_recv = await distributed_schemes[iplayer].pool.recv("local0", "scheme") 76 | 77 | assert enc[0] == enc[iplayer] 78 | assert d_scheme_recv == distributed_schemes[iplayer] == distributed_schemes[0] 79 | 80 | 81 | @pytest.mark.asyncio 82 | async def test_distributed_paillier_exception(pool_http: tuple[Pool, ...]) -> None: 83 | """ 84 | Tests raising of exception when corruption threshold is set incorrectly. 85 | 86 | :param pool_http: collection of communication pools 87 | """ 88 | max_corruption_threshold = math.ceil(len(pool_http) / 2) - 1 89 | corruption_threshold = max_corruption_threshold + 1 90 | key_length = 64 91 | prime_threshold = 200 92 | correct_param_biprime = 20 93 | stat_sec_shamir = 20 94 | with pytest.raises(ValueError): 95 | _distributed_schemes = await asyncio.gather( 96 | *[ 97 | DistributedPaillier.from_security_parameter( 98 | pool_http[i], 99 | corruption_threshold, 100 | key_length, 101 | prime_threshold, 102 | correct_param_biprime, 103 | stat_sec_shamir, 104 | distributed=False, 105 | ) 106 | for i in range(len(pool_http)) 107 | ] 108 | ) 109 | 110 | 111 | @pytest.mark.parametrize( 112 | "plaintext", [1, 2, 3, -1, -2, -3, 1.5, 42.42424242, -1.5, -42.42424242] 113 | ) 114 | @pytest.mark.asyncio 115 | async def test_distributed_paillier_encrypt_decrypt( 116 | distributed_schemes: tuple[DistributedPaillier, ...], 117 | plaintext: float | int, 118 | ) -> None: 119 | """ 120 | Tests distributed encryption and decryption 121 | 122 | :param distributed_schemes: a collection of schemes 123 | :param plaintext: plaintext to encrypt and decrypt 124 | """ 125 | enc = distributed_schemes[0].encrypt(plaintext) 126 | dec = await asyncio.gather( 127 | *[distributed_schemes[i].decrypt(enc) for i in range(len(distributed_schemes))] 128 | ) 129 | assert all(d == plaintext for d in dec) 130 | 131 | 132 | @pytest.mark.parametrize( 133 | "plaintext", [1, 2, 3, -1, -2, -3, 1.5, 42.42424242, -1.5, -42.42424242] 134 | ) 135 | @pytest.mark.asyncio 136 | async def test_distributed_paillier_encrypt_decrypt_parallel( 137 | distributed_schemes: tuple[DistributedPaillier, ...], 138 | plaintext: float | int, 139 | ) -> None: 140 | """ 141 | Tests distributed encryption and decryption in parallel 142 | 143 | :param distributed_schemes: a collection of schemes 144 | :param plaintext: plaintext to encrypt and decrypt 145 | """ 146 | encs = [distributed_schemes[0].encrypt(plaintext) for _ in range(3)] 147 | decs = await asyncio.gather( 148 | *[ 149 | asyncio.gather( 150 | *[ 151 | distributed_schemes[i].decrypt(enc) 152 | for i in range(len(distributed_schemes)) 153 | ] 154 | ) 155 | for enc in encs 156 | ] 157 | ) 158 | assert all(all(d == plaintext for d in dec) for dec in decs) 159 | 160 | 161 | @pytest.mark.asyncio 162 | async def test_distributed_paillier_encrypt_decrypt_sequence( 163 | distributed_schemes: tuple[DistributedPaillier, ...], 164 | ) -> None: 165 | """ 166 | Tests distributed sequence decryption 167 | 168 | :param distributed_schemes: a collection of schemes 169 | """ 170 | plaintexts = [1, 2, 3, -1, -2, -3, 1.5, 42.42424242, -1.5, -42.42424242] 171 | ciphertexts = [] 172 | for plaintext in plaintexts: 173 | ciphertexts.append(distributed_schemes[0].encrypt(plaintext)) 174 | 175 | decryptions = await asyncio.gather( 176 | *[ 177 | distributed_schemes[i].decrypt_sequence(ciphertexts) 178 | for i in range(len(distributed_schemes)) 179 | ] 180 | ) 181 | 182 | for decryption_list in decryptions: 183 | assert decryption_list is not None 184 | for idx, decryption in enumerate(decryption_list): 185 | assert plaintexts[idx] == decryption 186 | 187 | 188 | @pytest.mark.asyncio 189 | async def test_distributed_paillier_encrypt_decrypt_sequence_parallel( 190 | distributed_schemes: tuple[DistributedPaillier, ...], 191 | ) -> None: 192 | """ 193 | Tests distributed sequence decryption when run in parallel 194 | 195 | :param distributed_schemes: a collection of schemes 196 | """ 197 | plaintexts_list: list[list[float] | list[int]] = [ 198 | [1, 2, 3], 199 | [-1, -2, -3], 200 | [1.5, 42.42424242, -1.5 - 42.42424242], 201 | ] 202 | ciphertexts_list = [] 203 | for plaintext_list in plaintexts_list: 204 | ciphertexts_list.append( 205 | [distributed_schemes[0].encrypt(plaintext) for plaintext in plaintext_list] 206 | ) 207 | 208 | async def safe_decrypt_sequence( 209 | distributed_scheme: DistributedPaillier, 210 | ciphertexts: Sequence[PaillierCiphertext], 211 | ) -> list[Plaintext]: 212 | decryption = await distributed_scheme.decrypt_sequence(ciphertexts) 213 | assert decryption is not None, "Decryption result should not be None" 214 | return decryption 215 | 216 | decryption_lists: list[list[list[Plaintext]]] = await asyncio.gather( 217 | *[ 218 | asyncio.gather( 219 | *[ 220 | safe_decrypt_sequence(distributed_schemes[i], ciphertexts) 221 | for i in range(len(distributed_schemes)) 222 | ] 223 | ) 224 | for ciphertexts in ciphertexts_list 225 | ] 226 | ) 227 | 228 | for result_lists, correct_decryption_list in zip(decryption_lists, plaintexts_list): 229 | for decryption_list in result_lists: 230 | assert decryption_list == correct_decryption_list 231 | 232 | 233 | @pytest.mark.parametrize( 234 | "receivers_id,result_indices", 235 | [ 236 | (0, (0,)), 237 | (1, (0, 1)), 238 | ], 239 | ) 240 | @pytest.mark.asyncio 241 | async def test_distributed_paillier_encrypt_decrypt_receivers( 242 | distributed_schemes: tuple[DistributedPaillier, ...], 243 | receivers_id: int, 244 | result_indices: tuple[int], 245 | ) -> None: 246 | """ 247 | Tests distributed decryption revealing the results to a subset of receivers only. 248 | 249 | :param distributed_schemes: a collection of schemes 250 | :param receivers_id: parties to reveal the decryptions to 251 | :param result_indices: indices of the parties that should have received the decryptions 252 | :raises ValueError: if receivers_id is invalid 253 | """ 254 | if receivers_id == 0: 255 | receiver0_list = [["local0"]] * len(distributed_schemes) 256 | receiver0_list[0] = ["self"] 257 | receivers = tuple(receiver0_list) 258 | elif receivers_id == 1: 259 | receivers01_list = [["local0", "local1"]] * len(distributed_schemes) 260 | receivers01_list[0] = ["self", "local1"] 261 | receivers01_list[1] = ["local0", "self"] 262 | receivers = tuple(receivers01_list) 263 | else: 264 | raise ValueError("Invalid receivers_id") 265 | 266 | enc = distributed_schemes[0].encrypt(42) 267 | dec = await asyncio.gather( 268 | *[ 269 | distributed_schemes[i].decrypt(enc, receivers=receivers[i]) 270 | for i in range(len(distributed_schemes)) 271 | ] 272 | ) 273 | for i in range(len(distributed_schemes)): 274 | if i in result_indices: 275 | assert dec[i] == 42 276 | else: 277 | assert dec[i] is None 278 | 279 | 280 | @pytest.mark.parametrize( 281 | "collection_type", 282 | ( 283 | dict, 284 | list, 285 | tuple, 286 | ), 287 | ) 288 | @pytest.mark.asyncio 289 | async def test_pool_broadcast_collection( 290 | distributed_schemes: tuple[DistributedPaillier, ...], 291 | collection_type: type[Any], 292 | ) -> None: 293 | """ 294 | Test whether sending of collections of ciphertexts using the broadcast method works as expected. 295 | 296 | :param distributed_schemes: a collection of schemes 297 | :param collection_type: The type of collection that is to be communicated. 298 | """ 299 | plaintexts = [1, 2, 3, -1, -2, -3, 1.5, 42.42424242, -1.5, -42.42424242] 300 | ciphertexts = map(distributed_schemes[0].encrypt, plaintexts) 301 | if collection_type == dict: 302 | collection: Any = {} 303 | for index, ciphertext in enumerate(ciphertexts): 304 | collection[str(index)] = ciphertext 305 | else: 306 | collection = collection_type(ciphertexts) 307 | 308 | distributed_schemes[0].pool.async_broadcast(collection, "ciphertext_collection") 309 | 310 | received_collections = await asyncio.gather( 311 | *[ 312 | distributed_schemes[i].pool.recv("local0", "ciphertext_collection") 313 | for i in range(1, len(distributed_schemes)) 314 | ] 315 | ) 316 | 317 | for received_collection in received_collections: 318 | assert received_collection == collection 319 | -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/test/test_serialization.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module tests the serialization of DistributedPaillier instances. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import asyncio 8 | import os 9 | from pathlib import Path 10 | 11 | import pytest 12 | 13 | from tno.mpc.communication import Pool 14 | from tno.mpc.encryption_schemes.shamir import IntegerShares, ShamirSecretSharingIntegers 15 | 16 | from tno.mpc.protocols.distributed_keygen import DistributedPaillier, PaillierSharedKey 17 | 18 | 19 | def test_serialization_paillier_shared_key() -> None: 20 | """ 21 | Test to determine whether the secret key serialization works properly for Paillier shared keys. 22 | """ 23 | orig_key = PaillierSharedKey( 24 | n=1, 25 | t=0, 26 | player_id=0, 27 | share=IntegerShares(ShamirSecretSharingIntegers(), {1: 1}, 1, 1), 28 | theta=1, 29 | ) 30 | assert ( 31 | PaillierSharedKey.deserialize(PaillierSharedKey.serialize(orig_key)) == orig_key 32 | ) 33 | 34 | 35 | @pytest.mark.parametrize( 36 | "plaintext", [1, 2, 3, -1, -2, -3, 1.5, 42.42424242, -1.5, -42.42424242] 37 | ) 38 | @pytest.mark.asyncio 39 | async def test_storing_and_loading_key( 40 | distributed_schemes: tuple[DistributedPaillier, ...], 41 | plaintext: float | int, 42 | ) -> None: 43 | """ 44 | Test to see if we can store and load a key 45 | """ 46 | number_of_schemes = len(distributed_schemes) 47 | pools: list[Pool] = [ 48 | distributed_schemes[index].pool for index in range(number_of_schemes) 49 | ] 50 | schemes_as_bytes = [ 51 | DistributedPaillier.store_private_key(distributed_schemes[index]) 52 | for index in range(number_of_schemes) 53 | ] 54 | 55 | reconstructed_schemes = await asyncio.gather( 56 | *[ 57 | DistributedPaillier.load_private_key_from_bytes( 58 | schemes_as_bytes[index], pools[index], False 59 | ) 60 | for index in range(number_of_schemes) 61 | ] 62 | ) 63 | 64 | enc = {0: reconstructed_schemes[0].encrypt(plaintext)} 65 | reconstructed_schemes[0].pool.async_broadcast(enc[0], "encryption") 66 | assert not enc[0].fresh 67 | for iplayer in range(1, number_of_schemes): 68 | enc[iplayer] = await reconstructed_schemes[iplayer].pool.recv( 69 | "local0", "encryption" 70 | ) 71 | 72 | dec = await asyncio.gather( 73 | *[reconstructed_schemes[i].decrypt(enc[i]) for i in range(number_of_schemes)] 74 | ) 75 | assert all(d == plaintext for d in dec) 76 | 77 | 78 | # Comment out the skip to generate new key files to use for testing. 79 | @pytest.mark.skip(reason="No need to generate key files for each run") 80 | def test_store_key_to_file( 81 | distributed_schemes_fresh: tuple[DistributedPaillier, ...] 82 | ) -> None: 83 | """ 84 | Test which generates different keys and store them to the file system. These files are also included in the package. 85 | 86 | :param distributed_schemes_fresh: The schemes to store to the file system. 87 | """ 88 | base_path = Path(f"{os.path.dirname(__file__)}/test_data") 89 | for index, key in enumerate(distributed_schemes_fresh): 90 | with open( 91 | base_path.joinpath( 92 | f"distributed_key_threshold_{key.corruption_threshold}_{len(distributed_schemes_fresh)}parties_{index}.obj" 93 | ), 94 | "wb", 95 | ) as file: 96 | file.write(DistributedPaillier.store_private_key(key)) 97 | -------------------------------------------------------------------------------- /src/tno/mpc/protocols/distributed_keygen/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Useful functions for the distributed keygen module. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import operator 8 | import sys 9 | from collections import Counter 10 | from dataclasses import dataclass, field 11 | from typing import Any, Generic, Iterator, Sequence, TypeVar 12 | 13 | from tno.mpc.communication import Pool 14 | from tno.mpc.encryption_schemes.shamir import ShamirSecretSharingScheme as Shamir 15 | from tno.mpc.encryption_schemes.shamir import ShamirShares 16 | 17 | if sys.version_info >= (3, 12): 18 | from typing import override 19 | else: 20 | from typing_extensions import override 21 | 22 | 23 | def mult_list(list_: list[int], modulus: int | None = None) -> int: 24 | """ 25 | Utility function to multiply a list of numbers in a modular group 26 | 27 | :param list_: list of elements 28 | :param modulus: modulus to be applied 29 | :return: product of the elements in the list modulo the modulus 30 | """ 31 | out = 1 32 | if modulus is None: 33 | for element in list_: 34 | out = out * element 35 | else: 36 | for element in list_: 37 | out = out * element % modulus 38 | return out 39 | 40 | 41 | class Variable: 42 | """ 43 | This class represents a variable that is secret shared between 44 | parties and eases tracking the shares. 45 | 46 | A variable has a label and an owner. Each party needs to create an instance 47 | of Variable with the same label for each variable used in the computation. 48 | The label should uniquely identify the variable in the computation. The 49 | owner is the index of the party that owns the variable. The owner is the 50 | only party that can set the plaintext value of the variable (the party 51 | providing the input). 52 | 53 | It is possible for the variable to not have an owner (owner=-1) if no party 54 | knows the secret value (e.g. after a multiplication with another Variable). 55 | 56 | A Variable stores its value in two fields, _input and _sharing. The _input 57 | field stores the original plaintext value of this variable. This field can 58 | only be read and written if the party is the owner of the variable. The 59 | _sharing is the object used to store the shares this party has of the 60 | variable. 61 | """ 62 | 63 | def __init__(self, label: str, owner: int = -1) -> None: 64 | """ 65 | Create a new Variable with the given label and owner. 66 | 67 | :param label: label of this variable 68 | :param owner: index of the party that owns this variable 69 | """ 70 | self.label = label 71 | self.owner = owner 72 | 73 | self._input: int | None = None 74 | self._sharing: Any = None 75 | 76 | def get_plaintext(self) -> int: 77 | """ 78 | Return the original plaintext used as input to set this variable. 79 | Only the owner of the variable can know this value. 80 | 81 | :raises ValueError: if the plaintext value of this variable is not known 82 | :return: the plaintext value of this variable 83 | """ 84 | if self._input is None: 85 | raise ValueError( 86 | "The plaintext input value of this variable not known. Either \ 87 | the variable has not been set or you are not the owner of the \ 88 | variable (you cannot know a secret you did not create without \ 89 | reconstructing it)." 90 | ) 91 | return self._input 92 | 93 | def set_plaintext(self, value: int) -> None: 94 | """ 95 | Set the value of this variable to the given value. Only the owner of 96 | the variable can set the plaintext value. 97 | 98 | :param value: value to set this variable to 99 | """ 100 | self._input = value 101 | 102 | def clone(self) -> Any: 103 | """ 104 | Return a new instance of this class using the given instance. 105 | Essentially a shallow copy. 106 | 107 | :return: new instance of this class with the same label and owner, but 108 | no shares or plaintext value 109 | """ 110 | return self.__class__(label=self.label, owner=self.owner) 111 | 112 | def __add__(self, other: Any) -> Any: 113 | """ 114 | Add this variable with another variable. The other variable must be 115 | of the same type as this variable. 116 | 117 | :param other: variable to add with this variable 118 | :return: a new variable storing the result of adding the two variables 119 | """ 120 | raise NotImplementedError 121 | 122 | def __mul__(self, other: Any) -> Any: 123 | """ 124 | Multiply this variable with another variable. The other variable 125 | must be of the same type as this variable. 126 | 127 | :param other: variable to multiply with this variable 128 | :return: a new variable storing the result of multiplying the two variables 129 | """ 130 | raise NotImplementedError 131 | 132 | def share(self, index: int) -> None: 133 | """ 134 | Create and store a sharing of this variable. Only the owner of the 135 | variable can share if the _input field is set. 136 | 137 | :param index: index of this party 138 | """ 139 | raise NotImplementedError 140 | 141 | def reconstruct(self) -> Any: 142 | """ 143 | Reconstruct the secret value of this variable. 144 | 145 | :return: the reconstructed secret value stored in this variable 146 | """ 147 | raise NotImplementedError 148 | 149 | def get_share(self, index: int) -> Any: 150 | """ 151 | Return the share of party index. 152 | 153 | :param index: index of the party to get the share of. index must be in 154 | the range $[0, n-1]$ where $n$ is the number of parties. 155 | :return: the share of party index 156 | """ 157 | raise NotImplementedError 158 | 159 | def set_share(self, index: int, share: Any) -> None: 160 | """ 161 | Set the share of party index with the value share. 162 | 163 | :param index: index of the party to set the share of. index must be in 164 | the range $[0, n-1]$ where $n$ is the number of parties. 165 | :param share: the share to set for party index 166 | """ 167 | raise NotImplementedError 168 | 169 | def __repr__(self) -> str: 170 | return ( 171 | f"Variable(label={self.label}, owner={self.owner}, shares={self._sharing})" 172 | ) 173 | 174 | 175 | class ShamirVariable(Variable): 176 | """ 177 | Implementation of a secret-shared Variable using the Shamir Secret 178 | Sharing scheme. 179 | """ 180 | 181 | def __init__(self, shamir: Shamir, label: str, owner: int = -1) -> None: 182 | super().__init__(label, owner) 183 | 184 | self.shamir_scheme: Shamir = shamir 185 | 186 | self._input: int | None = None 187 | self._sharing: ShamirShares = ShamirShares(shamir, {}) 188 | 189 | self._index: int = -1 # Stored on .share(), used in __add__ and __mul__ 190 | 191 | @override 192 | def clone(self) -> Any: 193 | """ 194 | Return a new instance of this class using the given instance. 195 | Essentially a shallow copy. 196 | 197 | :return: new instance of this class with the same label and owner, but 198 | no shares or plaintext value 199 | """ 200 | return self.__class__( 201 | shamir=self.shamir_scheme, label=self.label, owner=self.owner 202 | ) 203 | 204 | @override 205 | def __add__(self, other: Any) -> Any: 206 | if not isinstance(other, self.__class__): 207 | raise ValueError( 208 | "Can only add a ShamirVariable with another ShamirVariable" 209 | ) 210 | if len(self._sharing.shares.keys()) == 0: 211 | raise ValueError("Cannot add a variable that has not been shared") 212 | 213 | # In case this party is the owner of the variable, it has all shares. 214 | # We need to ensure that the owner of a variable only calculates with 215 | # its own share (otherwise ShamirShares class gets confused.) 216 | self_sharing = self._sharing 217 | if len(self._sharing.shares.keys()) > 1: 218 | self_sharing = ShamirShares( 219 | self.shamir_scheme, {self._index: self._sharing.shares[self._index]} 220 | ) 221 | 222 | result = self.clone() 223 | result._sharing = self_sharing + other._sharing 224 | return result 225 | 226 | @override 227 | def __mul__(self, other: Any) -> Any: 228 | if not isinstance(other, self.__class__): 229 | raise ValueError( 230 | "Can only multiply a ShamirVariable with another ShamirVariable" 231 | ) 232 | if len(self._sharing.shares.keys()) == 0: 233 | raise ValueError("Cannot multiply a variable that has not been shared") 234 | 235 | # In case this party is the owner of the variable, it has all shares 236 | # We need to ensure that the owner of a variable only calculates with 237 | # its own share 238 | self_sharing = self._sharing 239 | if len(self._sharing.shares.keys()) > 1: 240 | self_sharing = ShamirShares( 241 | self.shamir_scheme, {self._index: self._sharing.shares[self._index]} 242 | ) 243 | 244 | result_sharing = self_sharing * other._sharing 245 | 246 | result = self.clone() 247 | result._sharing = result_sharing 248 | result.shamir_scheme = result_sharing.scheme 249 | 250 | return result 251 | 252 | @override 253 | def share(self, index: int) -> None: 254 | if self.owner != index: 255 | raise ValueError("Only the owner of a variable can share it") 256 | if self._input is None: 257 | raise ValueError("Set the value of the variable before sharing it") 258 | 259 | self._sharing = self.shamir_scheme.share_secret(self._input) 260 | self._index = index 261 | 262 | @override 263 | def reconstruct(self) -> Any: 264 | """ 265 | Reconstruct the secret value of this variable. 266 | See :meth:`ShamirShares.reconstruct_secret` for more details. 267 | 268 | :return: the reconstructed secret value stored in this variable 269 | """ 270 | return self._sharing.reconstruct_secret() 271 | 272 | @override 273 | def get_share(self, index: int) -> Any: 274 | """ 275 | Return the share of party index. 276 | 277 | :param index: index of the party to get the share of. index must be in 278 | the range $[0, n-1]$ where $n$ is the number of parties. 279 | :raise ValueError: if there is no share for party index 280 | :return: the share of party index 281 | """ 282 | if (share := self._sharing.shares.get(index)) is None: 283 | raise ValueError( 284 | f"There is no share for party {index} of variable {self.label}" 285 | ) 286 | return share 287 | 288 | @override 289 | def set_share(self, index: int, share: Any) -> None: 290 | self._sharing.shares[index] = share 291 | 292 | def get_shares(self) -> dict[int, int]: 293 | """ 294 | Return all shares of this variable. 295 | 296 | :return: a dictionary mapping party indices to shares 297 | """ 298 | return self._sharing.shares 299 | 300 | 301 | class AdditiveVariable(Variable): 302 | """ 303 | Simple additive secret sharing scheme. 304 | """ 305 | 306 | def __init__(self, label: str, modulus: int, owner: int = -1) -> None: 307 | super().__init__(label, owner) 308 | self._modulus = modulus 309 | self._sharing: dict[int, int] = {} 310 | 311 | self._index: int = -1 # Stored on .share(), used in __add__ and __mul__ 312 | 313 | @override 314 | def clone(self) -> Any: 315 | """ 316 | Return a new instance of this class using the given instance. 317 | Essentially a shallow copy. 318 | 319 | :return: new instance of this class with the same label and owner, but 320 | no shares or plaintext value 321 | """ 322 | return self.__class__(label=self.label, owner=self.owner, modulus=self._modulus) 323 | 324 | @override 325 | def __add__(self, other: Any) -> Any: 326 | if not isinstance(other, self.__class__): 327 | raise ValueError( 328 | "Can only add a AdditiveVariable with another AdditiveVariable" 329 | ) 330 | 331 | # In case this party is the owner of the variable, it has all shares. 332 | # We need to ensure that the owner of a variable only calculates with 333 | # its own share (otherwise ShamirShares class gets confused.) 334 | self_sharing = self._sharing 335 | if len(self._sharing.keys()) > 1: 336 | self_sharing = {self._index: self._sharing[self._index]} 337 | 338 | if self_sharing.keys() != other._sharing.keys(): 339 | raise ValueError( 340 | "Can only add variables that have both been shared to the same parties" 341 | ) 342 | 343 | result = self.clone() 344 | # Add the shares for each key 345 | result._sharing = Counter(self_sharing) + Counter(other._sharing) 346 | 347 | return result 348 | 349 | @override 350 | def __mul__(self, other: Any) -> Any: 351 | raise NotImplementedError("This scheme only supports addition.") 352 | 353 | @override 354 | def reconstruct(self) -> Any: 355 | """ 356 | Reconstruct the secret value of this variable. 357 | See :meth:`ShamirShares.reconstruct_secret` for more details. 358 | 359 | :return: the reconstructed secret value stored in this variable 360 | """ 361 | return sum(self._sharing.values()) % self._modulus 362 | 363 | @override 364 | def share(self, index: int) -> None: 365 | raise NotImplementedError 366 | 367 | @override 368 | def get_share(self, index: int) -> Any: 369 | """ 370 | Return the share of party index. 371 | 372 | :param index: index of the party to get the share of. index must be in 373 | the range $[0, n-1]$ where $n$ is the number of parties. 374 | :return: the share of party index 375 | """ 376 | return self._sharing[index] 377 | 378 | @override 379 | def set_share(self, index: int, share: Any) -> None: 380 | self._sharing[index] = share 381 | 382 | 383 | V = TypeVar("V", bound=Variable) 384 | 385 | 386 | class Batched(Generic[V], Variable): 387 | """ 388 | A Batched Variable is a list of variables all representing copies of the 389 | same variable (with different values). 390 | 391 | This class allows one to easily set and get the shares of all variables in 392 | the batch, useful when sending and receiving messages about this batch. 393 | Furthermore, the class implements arithmetic operations for operating on 394 | batches. 395 | 396 | This class is useful if a calculation needs to be performed many times (e.g. 397 | try until succeed). The batch allows one to describe the computation as if 398 | one were using a single variable. 399 | """ 400 | 401 | variables: list[V] 402 | batch_size: int 403 | 404 | def __init__(self, var: V, batch_size: int) -> None: 405 | """ 406 | Create a Batched Variable from a single Variable. 407 | 408 | A list of 'batch_size' Variables is created, where each variable is 409 | instantiated as a shallow copy of the given 'var', which can be seen as 410 | the blueprint. Note this will only copy over the meta-data of the 411 | variable, not the value. 412 | 413 | :param var: Variable to batch 414 | :param batch_size: number of copies of the Variable 415 | """ 416 | Variable.__init__(self, var.label, var.owner) 417 | 418 | self.variables: list[V] = [var.clone() for _ in range(batch_size)] 419 | self.batch_size = batch_size 420 | 421 | def set_plaintext(self, value: int) -> None: 422 | raise NotImplementedError("Please use set_plaintexts instead.") 423 | 424 | def set_plaintexts(self, values: list[int]) -> None: 425 | """ 426 | Set the value of all variables in the batch. Each variable is set individually. 427 | 428 | :param values: list of values to set 429 | """ 430 | for _, val in enumerate(values): 431 | self.variables[_].set_plaintext(val) 432 | 433 | @override 434 | def clone(self) -> Any: 435 | """ 436 | Return a new instance of this class using the given instance. 437 | Essentially a shallow copy. 438 | 439 | :return: new instance of this class with the same label and owner, but 440 | no shares or plaintext value 441 | """ 442 | return self.__class__(self.variables[0], self.batch_size) 443 | 444 | @override 445 | def __add__(self, other: Any) -> Any: 446 | if not isinstance(other, self.__class__): 447 | raise ValueError("Can only add a Batched with another Batched") 448 | 449 | result = self.clone() 450 | result.variables = list(map(operator.add, self.variables, other.variables)) 451 | 452 | return result 453 | 454 | @override 455 | def __mul__(self, other: Any) -> Any: 456 | if not isinstance(other, self.__class__): 457 | raise ValueError("Can only multiply a Batched with another Batched") 458 | 459 | result = self.clone() 460 | result.variables = list(map(operator.mul, self.variables, other.variables)) 461 | 462 | return result 463 | 464 | @override 465 | def reconstruct(self) -> Any: 466 | """ 467 | Reconstruct the secret value of this variable. 468 | 469 | :return: the reconstructed secret value stored in this variable 470 | """ 471 | return [v.reconstruct() for v in self.variables] 472 | 473 | @override 474 | def share(self, index: int) -> None: 475 | for i in range(self.batch_size): 476 | self.variables[i].share(index) 477 | 478 | @override 479 | def get_share(self, index: int) -> list[Any]: 480 | """ 481 | Return the share of party index. 482 | 483 | :param index: index of the party to get the share of. index must be in 484 | the range $[0, n-1]$ where $n$ is the number of parties. 485 | :return: the share of party index 486 | """ 487 | return [var.get_share(index) for var in self.variables] 488 | 489 | @override 490 | def set_share(self, index: int, share: Any) -> None: 491 | for i, share_ in enumerate(share): 492 | self.variables[i].set_share(index, share_) 493 | 494 | def __getitem__(self, index: int) -> V: 495 | """ 496 | Return the Variable at the given index. 497 | 498 | :param index: index of the Variable to return 499 | :return: Variable at the given index 500 | """ 501 | return self.variables[index] 502 | 503 | def __iter__(self) -> Iterator[V]: 504 | return iter(self.variables) 505 | 506 | 507 | async def exchange_shares( 508 | group: list[V], index: int, pool: Pool, party_indices: dict[str, int], msg_id: str 509 | ) -> None: 510 | """ 511 | All parties send, for the variables in the group they own, to the other 512 | parties the shares intended for said party. Each party receives the shares 513 | intended for them. 514 | 515 | Note: This mutates the variables in the group. 516 | 517 | :param group: a list of variables to consider in this exchange 518 | :param index: index of this party 519 | :param pool: network of involved parties 520 | :param party_indices: mapping from party names to indices 521 | :param msg_id: Optional message id. 522 | :raises ValueError: if a variable is received with an unknown label 523 | """ 524 | group_dict: dict[str, V] = {v.label: v for v in group} 525 | 526 | # Send shares to other parties for the variables we own within the group 527 | other_parties = pool.pool_handlers.keys() 528 | for party in other_parties: 529 | message: dict[str, list[dict[str, str]]] = {"value": []} 530 | 531 | # Add all variables in the group that are owned by this party to the message 532 | for label, variable in group_dict.items(): 533 | if variable.owner == index: 534 | message["value"].append( 535 | { 536 | "label": label, 537 | "value": variable.get_share(party_indices[party]), 538 | } 539 | ) 540 | 541 | # Send the message to the party 542 | pool.asend(party, message, msg_id=msg_id) 543 | 544 | # Receive shares from other parties for the variables we don't own within the group 545 | messages = await pool.recv_all(msg_id=msg_id) 546 | for party, message in messages: 547 | for received_var in message["value"]: 548 | if received_var["label"] not in group_dict: 549 | raise ValueError( 550 | f"Received a variable with unknown label {received_var['label']}" 551 | ) 552 | 553 | group_dict[received_var["label"]].set_share(index, received_var["value"]) 554 | 555 | 556 | async def exchange_reconstruct( 557 | variables: Variable | Sequence[Variable], 558 | index: int, 559 | pool: Pool, 560 | party_indices: dict[str, int], 561 | msg_id: str, 562 | ) -> None: 563 | """ 564 | Exchange shares of this variable with the other parties in the pool to 565 | allow all parties to locally reconstruct the variable. 566 | 567 | :param variables: variable to exchange shares for 568 | :param index: index of this party 569 | :param pool: network of involved parties 570 | :param party_indices: mapping from party names to indices 571 | :param msg_id: Optional message id. 572 | """ 573 | if isinstance(variables, Variable): 574 | variables = [variables] 575 | 576 | # Message containing our share of the secret 577 | message = [ 578 | { 579 | "label": var.label, 580 | "value": var.get_share(index), 581 | } 582 | for var in variables 583 | ] 584 | 585 | # Send our share to all other parties 586 | pool.async_broadcast(message, msg_id) 587 | 588 | # Gather the shares of the other parties 589 | messages = await pool.recv_all(msg_id=msg_id) 590 | for party, message in messages: 591 | for i, share in enumerate(message): 592 | variables[i].set_share(party_indices[party], share["value"]) 593 | 594 | 595 | @dataclass 596 | class Shares: 597 | r""" 598 | Shares contains all shares of this party. 599 | Every subclass contains an object for that element, such as $p$ or $q$. 600 | These objects contain up to two entries: "additive" and "shares", 601 | in "additive", the local additive share of that element is stored, 602 | in "shares", the shamir shares of the local additive share are stored. 603 | 604 | To support the batching of messages for compute_modulus, we store lists of 605 | $P$'s and $Q$'s. 606 | """ 607 | 608 | @dataclass 609 | class P: 610 | r""" 611 | Shares of $p$. 612 | """ 613 | 614 | additive: int = 0 615 | shares: dict[int, int] = field(default_factory=dict) 616 | 617 | @dataclass 618 | class Q: 619 | r""" 620 | Shares of $q$. 621 | """ 622 | 623 | additive: int = 0 624 | shares: dict[int, int] = field(default_factory=dict) 625 | 626 | @dataclass 627 | class N: 628 | r""" 629 | Shares of $n$. 630 | """ 631 | 632 | shares: dict[int, int] = field(default_factory=dict) 633 | 634 | @dataclass 635 | class Lambda: 636 | r""" 637 | Shares of $\lambda$. 638 | """ 639 | 640 | additive: int = 0 641 | shares: dict[int, int] = field(default_factory=dict) 642 | 643 | @dataclass 644 | class Beta: 645 | r""" 646 | Shares of $\beta$. 647 | """ 648 | 649 | additive: int = 0 650 | shares: dict[int, int] = field(default_factory=dict) 651 | 652 | @dataclass 653 | class SecretKey: 654 | """ 655 | Shares of the secret key. 656 | """ 657 | 658 | additive: int = 0 659 | shares: dict[int, int] = field(default_factory=dict) 660 | 661 | p: Shares.P = field(default_factory=P) 662 | q: Shares.Q = field(default_factory=Q) 663 | 664 | lambda_: Shares.Lambda = field(default_factory=Lambda) 665 | beta: Shares.Beta = field(default_factory=Beta) 666 | secret_key: Shares.SecretKey = field(default_factory=SecretKey) 667 | -------------------------------------------------------------------------------- /stubs/sympy/__init__.pyi: -------------------------------------------------------------------------------- 1 | from .ntheory import * 2 | -------------------------------------------------------------------------------- /stubs/sympy/ntheory/__init__.pyi: -------------------------------------------------------------------------------- 1 | from .generate import * 2 | from .residue_ntheory import * 3 | -------------------------------------------------------------------------------- /stubs/sympy/ntheory/generate.pyi: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def nextprime(n: int, ith: int = ...) -> int: ... 4 | def prime(nth: int) -> int: ... 5 | def primerange(a: int, b: int) -> List[int]: ... 6 | -------------------------------------------------------------------------------- /stubs/sympy/ntheory/residue_ntheory.pyi: -------------------------------------------------------------------------------- 1 | def jacobi_symbol(m: int, n: int) -> int: ... 2 | --------------------------------------------------------------------------------