From 530a66a21ad09451b0015cd8ce6775eec55498eb Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 09:03:53 +0800 Subject: [PATCH 01/41] Add multi-party-ecdsa workspace member. --- Cargo.toml | 8 +- multi-party-ecdsa/.github/workflows/build.yml | 28 + .../.github/workflows/publish.yml | 19 + multi-party-ecdsa/.gitignore | 17 + multi-party-ecdsa/.rustfmt.toml | 1 + multi-party-ecdsa/CONTRIBUTING.md | 59 ++ multi-party-ecdsa/Cargo.toml | 108 ++ multi-party-ecdsa/LICENSE | 674 ++++++++++++ multi-party-ecdsa/README.md | 156 +++ .../audits/REPORT_final_2019-10-22.pdf | Bin 0 -> 306423 bytes .../benches/multi_party_ecdsa/gg18/keygen.rs | 135 +++ .../two_party_ecdsa/cclst_2019/keygen.rs | 62 ++ .../two_party_ecdsa/cclst_2019/sign.rs | 90 ++ .../two_party_ecdsa/lindell_2017/keygen.rs | 81 ++ .../two_party_ecdsa/lindell_2017/sign.rs | 75 ++ multi-party-ecdsa/demo/MP-ECDSA demo.gif | Bin 0 -> 306714 bytes multi-party-ecdsa/demo/run.sh | 40 + multi-party-ecdsa/docs/diagrams/keygen1.dot | 23 + multi-party-ecdsa/docs/diagrams/keygen2.dot | 27 + multi-party-ecdsa/docs/gg19.pdf | Bin 0 -> 189350 bytes multi-party-ecdsa/examples/common.rs | 225 ++++ .../examples/gg18_keygen_client.rs | 271 +++++ .../examples/gg18_sign_client.rs | 531 ++++++++++ multi-party-ecdsa/examples/gg18_sm_manager.rs | 143 +++ multi-party-ecdsa/examples/gg20_keygen.rs | 58 + multi-party-ecdsa/examples/gg20_signing.rs | 90 ++ multi-party-ecdsa/examples/gg20_sm_client.rs | 159 +++ multi-party-ecdsa/examples/gg20_sm_manager.rs | 193 ++++ multi-party-ecdsa/kzen-dev-setup.sh | 9 + multi-party-ecdsa/params.json | 1 + multi-party-ecdsa/src/lib.rs | 49 + multi-party-ecdsa/src/protocols/mod.rs | 18 + .../multi_party_ecdsa/gg_2018/mod.rs | 23 + .../multi_party_ecdsa/gg_2018/party_i.rs | 738 +++++++++++++ .../multi_party_ecdsa/gg_2018/test.rs | 460 ++++++++ .../multi_party_ecdsa/gg_2020/blame.rs | 458 ++++++++ .../multi_party_ecdsa/gg_2020/mod.rs | 28 + .../multi_party_ecdsa/gg_2020/party_i.rs | 990 ++++++++++++++++++ .../gg_2020/state_machine/keygen.rs | 552 ++++++++++ .../gg_2020/state_machine/keygen/rounds.rs | 373 +++++++ .../gg_2020/state_machine/mod.rs | 3 + .../gg_2020/state_machine/sign.rs | 777 ++++++++++++++ .../gg_2020/state_machine/sign/fmt.rs | 127 +++ .../gg_2020/state_machine/sign/rounds.rs | 737 +++++++++++++ .../gg_2020/state_machine/traits.rs | 11 + .../multi_party_ecdsa/gg_2020/test.rs | 783 ++++++++++++++ .../src/protocols/multi_party_ecdsa/mod.rs | 18 + .../two_party_ecdsa/cclst_2019/mod.rs | 23 + .../two_party_ecdsa/cclst_2019/party_one.rs | 419 ++++++++ .../two_party_ecdsa/cclst_2019/party_two.rs | 356 +++++++ .../two_party_ecdsa/cclst_2019/test.rs | 142 +++ .../two_party_ecdsa/lindell_2017/mod.rs | 23 + .../two_party_ecdsa/lindell_2017/party_one.rs | 607 +++++++++++ .../two_party_ecdsa/lindell_2017/party_two.rs | 426 ++++++++ .../two_party_ecdsa/lindell_2017/test.rs | 137 +++ .../src/protocols/two_party_ecdsa/mod.rs | 24 + multi-party-ecdsa/src/utilities/mod.rs | 3 + multi-party-ecdsa/src/utilities/mta/mod.rs | 215 ++++ .../src/utilities/mta/range_proofs.rs | 710 +++++++++++++ multi-party-ecdsa/src/utilities/mta/test.rs | 19 + multi-party-ecdsa/src/utilities/zk_pdl/mod.rs | 262 +++++ .../src/utilities/zk_pdl/test.rs | 58 + .../src/utilities/zk_pdl_with_slack/mod.rs | 202 ++++ .../src/utilities/zk_pdl_with_slack/test.rs | 129 +++ 64 files changed, 13181 insertions(+), 2 deletions(-) create mode 100644 multi-party-ecdsa/.github/workflows/build.yml create mode 100644 multi-party-ecdsa/.github/workflows/publish.yml create mode 100644 multi-party-ecdsa/.gitignore create mode 100644 multi-party-ecdsa/.rustfmt.toml create mode 100644 multi-party-ecdsa/CONTRIBUTING.md create mode 100644 multi-party-ecdsa/Cargo.toml create mode 100644 multi-party-ecdsa/LICENSE create mode 100644 multi-party-ecdsa/README.md create mode 100644 multi-party-ecdsa/audits/REPORT_final_2019-10-22.pdf create mode 100644 multi-party-ecdsa/benches/multi_party_ecdsa/gg18/keygen.rs create mode 100644 multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/keygen.rs create mode 100644 multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/sign.rs create mode 100644 multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/keygen.rs create mode 100644 multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/sign.rs create mode 100644 multi-party-ecdsa/demo/MP-ECDSA demo.gif create mode 100755 multi-party-ecdsa/demo/run.sh create mode 100644 multi-party-ecdsa/docs/diagrams/keygen1.dot create mode 100644 multi-party-ecdsa/docs/diagrams/keygen2.dot create mode 100644 multi-party-ecdsa/docs/gg19.pdf create mode 100644 multi-party-ecdsa/examples/common.rs create mode 100644 multi-party-ecdsa/examples/gg18_keygen_client.rs create mode 100644 multi-party-ecdsa/examples/gg18_sign_client.rs create mode 100644 multi-party-ecdsa/examples/gg18_sm_manager.rs create mode 100644 multi-party-ecdsa/examples/gg20_keygen.rs create mode 100644 multi-party-ecdsa/examples/gg20_signing.rs create mode 100644 multi-party-ecdsa/examples/gg20_sm_client.rs create mode 100644 multi-party-ecdsa/examples/gg20_sm_manager.rs create mode 100755 multi-party-ecdsa/kzen-dev-setup.sh create mode 100644 multi-party-ecdsa/params.json create mode 100644 multi-party-ecdsa/src/lib.rs create mode 100644 multi-party-ecdsa/src/protocols/mod.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/mod.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/party_i.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/test.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/mod.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/mod.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/fmt.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/traits.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs create mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs create mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/mod.rs create mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_one.rs create mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_two.rs create mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/test.rs create mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/mod.rs create mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs create mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_two.rs create mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/test.rs create mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/mod.rs create mode 100644 multi-party-ecdsa/src/utilities/mod.rs create mode 100644 multi-party-ecdsa/src/utilities/mta/mod.rs create mode 100644 multi-party-ecdsa/src/utilities/mta/range_proofs.rs create mode 100644 multi-party-ecdsa/src/utilities/mta/test.rs create mode 100644 multi-party-ecdsa/src/utilities/zk_pdl/mod.rs create mode 100644 multi-party-ecdsa/src/utilities/zk_pdl/test.rs create mode 100644 multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs create mode 100644 multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs diff --git a/Cargo.toml b/Cargo.toml index 95e88171..7f9298b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,10 @@ authors = [ "Akilesh Tangella ", ] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = [ + "multi-party-ecdsa" +] [dependencies] bincode = "1.3.3" @@ -31,7 +34,8 @@ package = "kzen-paillier" default-features = false [dependencies.multi-party-ecdsa] -git = "https://github.com/webb-tools/multi-party-ecdsa" +version = "0.8" +path = "multi-party-ecdsa" default-features = false [dev-dependencies] diff --git a/multi-party-ecdsa/.github/workflows/build.yml b/multi-party-ecdsa/.github/workflows/build.yml new file mode 100644 index 00000000..cf044f76 --- /dev/null +++ b/multi-party-ecdsa/.github/workflows/build.yml @@ -0,0 +1,28 @@ +name: Build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: '0 5 * * *' + workflow_call: + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose + - name: Check formatting + run: cargo fmt --all -- --check + - name: Run clippy + run: cargo clippy -- -D clippy::all diff --git a/multi-party-ecdsa/.github/workflows/publish.yml b/multi-party-ecdsa/.github/workflows/publish.yml new file mode 100644 index 00000000..9a2ddc31 --- /dev/null +++ b/multi-party-ecdsa/.github/workflows/publish.yml @@ -0,0 +1,19 @@ +name: Publish + +on: + push: + tags: + - v*.*.* + +jobs: + build: + uses: ZenGo-X/multi-party-ecdsa/.github/workflows/build.yml@master + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Publish crate + env: + TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + cargo publish --token "$TOKEN" diff --git a/multi-party-ecdsa/.gitignore b/multi-party-ecdsa/.gitignore new file mode 100644 index 00000000..c3063c44 --- /dev/null +++ b/multi-party-ecdsa/.gitignore @@ -0,0 +1,17 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +.idea +.DS_Store + +keys*.store +signature + diff --git a/multi-party-ecdsa/.rustfmt.toml b/multi-party-ecdsa/.rustfmt.toml new file mode 100644 index 00000000..32a9786f --- /dev/null +++ b/multi-party-ecdsa/.rustfmt.toml @@ -0,0 +1 @@ +edition = "2018" diff --git a/multi-party-ecdsa/CONTRIBUTING.md b/multi-party-ecdsa/CONTRIBUTING.md new file mode 100644 index 00000000..05a89004 --- /dev/null +++ b/multi-party-ecdsa/CONTRIBUTING.md @@ -0,0 +1,59 @@ +Contributing to the Multi-party ECDSA project +===================================== + +Pull requests are always welcome, and the KZen dev team appreciates any help the community can +give to help make Multi-party ECDSA project better. + +Contributor Agreement (CA) +---------------- + +Any contributor must sign the Contributor Agreement (CA). + +### How to sign the Contributor Agreement (CA)? + +Please send an email to [github@kzencorp.com](mailto:github@kzencorp.com) containing your github username, the CA will be send to you by email. +After signature you will be added to the team as a contributor. + +Communication Channels +---------------- + +* Most communication about KZen cryptography happens on Telegram, feel free to send us an email with your contact details. + +* Discussion about code base improvements happens in GitHub issues and on pull requests. + +Contributor Workflow +---------------- + +The codebase is maintained using the "contributor workflow" where everyone contributes patch proposals using "pull requests". This facilitates social contribution, easy testing and peer review. + +To contribute a patch, the workflow is as follows: + +* Fork repository +* Create topic branch +* Commit patches +* Push changes to your fork +* Create pull request + +Make sure to provide a clear description in your Pull Request (PR). + +### Header + +Make sure to include the following header (by configuring your IDE) in all files: + +```rust +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ +``` diff --git a/multi-party-ecdsa/Cargo.toml b/multi-party-ecdsa/Cargo.toml new file mode 100644 index 00000000..7542b57a --- /dev/null +++ b/multi-party-ecdsa/Cargo.toml @@ -0,0 +1,108 @@ +[package] +name = "multi-party-ecdsa" +version = "0.8.2" +edition = "2018" +authors = [ + "Gary ", + "Omer " +] +keywords = [ + "ecdsa", + "multi-party-ecdsa", + "signature", + "rust", + "secret-shares", + "blockchain", + "cryptography", + "cryptocurrency" +] + +homepage = "https://github.com/KZen-networks/multi-party-ecdsa" +repository = "https://github.com/KZen-networks/multi-party-ecdsa" +license = "GPL-3.0-or-later" +categories = ["cryptography"] + +[lib] +crate-type = ["lib"] + +[features] +default = ["curv-kzen/rust-gmp-kzen"] + +[dependencies] +subtle = { version = "2" } +serde = { version = "1.0", features = ["derive"] } +zeroize = "^1.5" +curv-kzen = { version = "0.10.0", default-features = false } +centipede = { version = "0.3.1", default-features = false } +zk-paillier = { version = "0.4.4", default-features = false } +round-based = { git = "https://github.com/webb-tools/round-based-protocol", features = [] } +thiserror = "1.0.23" +derivative = "2.2.0" +sha2 = "0.9" +log = "0.4.17" + +[dependencies.paillier] +package = "kzen-paillier" +version = "0.4.3" +default-features = false + +[dev-dependencies] +criterion = "0.3" +aes-gcm = "0.9.4" +hex = "0.4" +tokio = { version = "1", default-features = false, features = ["macros"] } +futures = "0.3" +rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] } +reqwest = { version = "0.9", default-features = false } +uuid = { version = "0.8", features = ["v4"] } +serde_json = "1.0" +rand = "0.8" +surf = "2" +async-sse = "5" +anyhow = "1" +structopt = "0.3" +secp256k1 = { version = "0.20", features = ["global-context"]} + +thiserror = "1.0.23" +round-based = { git = "https://github.com/webb-tools/round-based-protocol", features = ["dev"] } +[[example]] +name = "gg18_sm_manager" + +[[example]] +name = "gg18_sign_client" + +[[example]] +name = "gg18_keygen_client" + +[[example]] +name = "common" +crate-type = ["lib"] + +[[bench]] +name = "cclst_keygen" +path = "benches/two_party_ecdsa/cclst_2019/keygen.rs" +required-features = ["cclst"] +harness = false + +[[bench]] +name = "cclst_sign" +path = "benches/two_party_ecdsa/cclst_2019/sign.rs" +required-features = ["cclst"] +harness = false + + +[[bench]] +name = "gg18" +path = "benches/multi_party_ecdsa/gg18/keygen.rs" +harness = false + +[[bench]] +name = "lindel2017_keygen" +path = "benches/two_party_ecdsa/lindell_2017/keygen.rs" +harness = false + + +[[bench]] +name = "lindel2017_sign" +path = "benches/two_party_ecdsa/lindell_2017/sign.rs" +harness = false diff --git a/multi-party-ecdsa/LICENSE b/multi-party-ecdsa/LICENSE new file mode 100644 index 00000000..94a9ed02 --- /dev/null +++ b/multi-party-ecdsa/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/multi-party-ecdsa/README.md b/multi-party-ecdsa/README.md new file mode 100644 index 00000000..6b4fa464 --- /dev/null +++ b/multi-party-ecdsa/README.md @@ -0,0 +1,156 @@ +# Multi-party ECDSA + +[![Build Status](https://travis-ci.com/ZenGo-X/multi-party-ecdsa.svg?branch=master)](https://travis-ci.com/zengo-x/multi-party-ecdsa) +[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) + +This project is a Rust implementation of {t,n}-threshold ECDSA (elliptic curve digital signature algorithm). + +Threshold ECDSA includes two protocols: + +- Key Generation for creating secret shares. +- Signing for using the secret shares to generate a signature. + +ECDSA is used extensively for crypto-currencies such as Bitcoin, Ethereum (secp256k1 curve), NEO (NIST P-256 curve) and much more. +This library can be used to create MultiSig and ThresholdSig crypto wallet. For a full background on threshold signatures please read our Binance academy article [Threshold Signatures Explained](https://academy.binance.com/en/articles/threshold-signatures-explained). + +## Library Introduction +The library was built with four core design principles in mind: +1. Multi-protocol support +2. Built for cryptography engineers +3. Foolproof +4. Black box use of cryptographic primitives + +To learn about the core principles as well as on the [audit](https://github.com/KZen-networks/multi-party-ecdsa/tree/master/audits) process and security of the library, please read our [Intro to multiparty ecdsa library](https://zengo.com/introducing-multi-party-ecdsa-library/) blog post. + +## Use It + + +The library implements four different protocols for threshold ECDSA. The protocols presents different tradeoffs in terms of parameters, security assumptions and efficiency. + +| Protocol | High Level code | +| -------------------------------------------- | -------------------------------------------- | +| Lindell 17 [1] | [Gotham-city](https://github.com/KZen-networks/gotham-city) (accepted to [CIW19](https://ifca.ai/fc19/ciw/program.html)) is a two party bitcoin wallet, including benchmarks. [KMS](https://github.com/KZen-networks/kms-secp256k1) is a Rust wrapper library that implements a general purpose two party key management system. [thresh-sig-js](https://github.com/KZen-networks/thresh-sig-js) is a Javascript SDK | +| Gennaro, Goldfeder 19 [2] ([video](https://www.youtube.com/watch?v=PdfDZIwuZm0)) | [tss-ecdsa-cli](https://github.com/cryptochill/tss-ecdsa-cli) is a wrapper CLI for full threshold access structure, including network and threshold HD keys ([BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)). See [Demo](https://github.com/KZen-networks/multi-party-ecdsa#run-demo) in this library to get better low level understanding| +|Castagnos et. al. 19 [3]| Currently enabled as a feature in this library. To Enable, build with `--features=cclst`. to Test, use `cargo test --features=cclst -- --test-threads=1` | +| Gennaro, Goldfeder 20 [4] | A full threshold protocol that supports identifying malicious parties. If signing fails - a list of malicious parties is returned. The protocol requires only a broadcast channel (all messages are broadcasted)| + +## Run GG20 Demo + +In the following steps we will generate 2-of-3 threshold signing key and sign a message with 2 parties. + +### Setup + +1. You need [Rust](https://rustup.rs/) and [GMP library](https://gmplib.org) (optionally) to be installed on your computer. +2. - Run `cargo build --release --examples` + - Don't have GMP installed? Use this command instead: + ```bash + cargo build --release --examples --no-default-features --features curv-kzen/num-bigint + ``` + But keep in mind that it will be less efficient. + + Either of commands will produce binaries into `./target/release/examples/` folder. +3. `cd ./target/release/examples/` + +### Start an SM server + +`./gg20_sm_manager` + +That will start an HTTP server on `http://127.0.0.1:8000`. Other parties will use that server in order to communicate with +each other. Note that communication channels are neither encrypted nor authenticated. In production, you must encrypt and +authenticate parties messages. + +### Run Keygen + +Open 3 terminal tabs for each party. Run: + +1. `./gg20_keygen -t 1 -n 3 -i 1 --output local-share1.json` +2. `./gg20_keygen -t 1 -n 3 -i 2 --output local-share2.json` +3. `./gg20_keygen -t 1 -n 3 -i 3 --output local-share3.json` + +Each command corresponds to one party. Once keygen is completed, you'll have 3 new files: +`local-share1.json`, `local-share2.json`, `local-share3.json` corresponding to local secret +share of each party. + +### Run Signing + +Since we use 2-of-3 scheme (`t=1 n=3`), any two parties can sign a message. Run: + +1. `./gg20_signing -p 1,2 -d "hello" -l local-share1.json` +2. `./gg20_signing -p 1,2 -d "hello" -l local-share2.json` + +Each party will produce a resulting signature. `-p 1,2` specifies indexes of parties +who attends in signing (each party has an associated index given at keygen, see argument +`-i`), `-l file.json` sets a path to a file with secret local share, and `-d "hello"` +is a message being signed. + +### Running Demo on different computers + +While previous steps show how to run keygen & signing on local computer, you actually can +run each party on dedicated machine. To do this, you should ensure that parties can reach +SM Server, and specify its address via command line argument, eg: + +`./gg20_keygen --address http://10.0.1.9:8000/ ...` + +## Run GG18 Demo + +The following steps are for setup, key generation with `n` parties and signing with `t+1` parties. + +### Setup + +1. We use shared state machine architecture (see [white city](https://github.com/KZen-networks/white-city)). The parameters `parties` and `threshold` can be configured by changing the file: `param`. a keygen will run with `parties` parties and signing will run with any subset of `threshold + 1` parties. `param` file should be located in the same path of the client software. + +2. Install [Rust](https://rustup.rs/). Run `cargo build --release --examples` (it will build into `/target/release/examples/`) + +3. Run the shared state machine: `./gg18_sm_manager`. By default, it's configured to be in `127.0.0.1:8000`, this can be changed in `Rocket.toml` file. The `Rocket.toml` file should be in the same folder you run `sm_manager` from. + +### KeyGen + +run `gg18_keygen_client` as follows: `./gg18_keygen_client http://127.0.0.1:8000 keys.store`. Replace IP and port with the ones configured in setup. Once `n` parties join the application will run till finish. At the end each party will get a local keys file `keys.store` (change filename in command line). This contains secret and public data of the party after keygen. The file therefore should remain private. + +### Sign + +Run `./gg18_sign_client`. The application should be in the same folder as the `keys.store` file (or custom filename generated in keygen). the application takes three arguments: `IP:port` as in keygen, `filename` and message to be signed: `./gg18_sign_client http://127.0.0.1:8001 keys.store "KZen Networks"`. The same message should be used by all signers. Once `t+1` parties join the protocol will run and will output to screen signature (R,s). + +The `./gg18_sign_client` executable initially tries to unhex its input message (the third parameter). Before running ensure two things: + +1. If you want to pass a binary message to be signed - hex it. +2. If you want to pass a textual message in a non-hex form, make sure it can't be unhexed. +Simply put, the safest way to use the signing binary is to just always hex your messages before passing them to the `./gg18_sign_client` executable. + +#### Example +To sign the message `hello world`, first calculate its hexadecimal representation. This yields the `68656c6c6f20776f726c64`. +Then, run: +```bash +./gg18_sign_client http://127.0.0.1:8000 keys.store "68656c6c6f20776f726c64" +``` + +### GG18 demo + +Run `./run.sh` (located in `/demo` folder) in the main folder. Move `params` file to the same folder as the executables (usually `/target/release/examples`). The script will spawn a shared state machine, clients in the number of parties and signing requests for the `threshold + 1` first parties. + +`gg18_sm_manager` rocket server runs in _production_ mode by default. You may modify the `./run.sh` to config it to run in different environments. For example, to run rocket server in _development_: +``` +ROCKET_ENV=development ./target/release/examples/sm_manager +``` + +## Contributions & Development Process + +The contribution workflow is described in [CONTRIBUTING.md](CONTRIBUTING.md), in addition **the [Rust utilities wiki](https://github.com/KZen-networks/rust-utils/wiki) contains information on workflow and environment set-up**. + +## License + +Multi-party ECDSA is released under the terms of the GPL-3.0 license. See [LICENSE](LICENSE) for more information. + +## Contact + +Feel free to [reach out](mailto:github@kzencorp.com) or join ZenGo X [Telegram](https://t.me/joinchat/ET1mddGXRoyCxZ-7) for discussions on code and research. + +## References + +[1] + +[2] + +[3] + +[4] diff --git a/multi-party-ecdsa/audits/REPORT_final_2019-10-22.pdf b/multi-party-ecdsa/audits/REPORT_final_2019-10-22.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9e7c2f8d6630245271d832515a519c7764c8ec1e GIT binary patch literal 306423 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

&J9Ius^W<_Sez5|PY5r-Jt^420Eo8jJ;tQ$HPo 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

n!jXQz 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 z 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 literal 0 HcmV?d00001 diff --git a/multi-party-ecdsa/demo/run.sh b/multi-party-ecdsa/demo/run.sh new file mode 100755 index 00000000..b608be1e --- /dev/null +++ b/multi-party-ecdsa/demo/run.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +cargo +nightly build --examples --release + +file_as_string=`cat params.json` + +n=`echo "$file_as_string" | cut -d "\"" -f 4 ` +t=`echo "$file_as_string" | cut -d "\"" -f 8 ` + +echo "Multi-party ECDSA parties:$n threshold:$t" +#clean +sleep 1 + +rm keys?.store +killall gg18_sm_manager gg18_keygen_client gg18_sign_client 2> /dev/null + +./target/release/examples/gg18_sm_manager & + +sleep 2 +echo "keygen part" + +for i in $(seq 1 $n) +do + echo "key gen for client $i out of $n" + ./target/release/examples/gg18_keygen_client http://127.0.0.1:8001 keys$i.store & + sleep 3 +done + + + +sleep 5 +echo "sign" + +for i in $(seq 1 $((t+1))); +do + echo "signing for client $i out of $((t+1))" + ./target/release/examples/gg18_sign_client http://127.0.0.1:8001 keys$i.store "KZen Networks" & + sleep 3 +done + +killall gg18_sm_manager 2> /dev/null diff --git a/multi-party-ecdsa/docs/diagrams/keygen1.dot b/multi-party-ecdsa/docs/diagrams/keygen1.dot new file mode 100644 index 00000000..cea99d97 --- /dev/null +++ b/multi-party-ecdsa/docs/diagrams/keygen1.dot @@ -0,0 +1,23 @@ +// Lindell 2party ECDSA keygen party 1 +digraph { + start [shape=doublecircle] + stop [shape=doublecircle] + A + B + C + D + E + start -> A [label="create commitments, ID"] + "check1" [shape=diamond] + "check2" [shape=diamond] + A -> "check1" [label="verify_and_decommit, ID"] + "check1" -> B [label="yes,ID"] + "check1" -> stop [label=no] + B -> C [label="generate_keypair_and_encrypted_share,ID"] + C -> "check2" [label="generate_proof_correct_key, ID"] + "check2" -> D [label="yes,ID"] + "check2" -> stop [label=no] + D -> E [label="generate_range_proof,ID"] + E -> stop [label="write_to_db,ID"] + +} diff --git a/multi-party-ecdsa/docs/diagrams/keygen2.dot b/multi-party-ecdsa/docs/diagrams/keygen2.dot new file mode 100644 index 00000000..ee3a4951 --- /dev/null +++ b/multi-party-ecdsa/docs/diagrams/keygen2.dot @@ -0,0 +1,27 @@ +// Lindell 2party ECDSA keygen party 2 +digraph { + start [shape=doublecircle] + stop [shape=doublecircle] + A + B + C + D + E + start -> A [label="begin session"] + "check1" [shape=diamond] + "check2" [shape=diamond] + "check3" [shape=diamond] + A -> B [label="create, ID"] + B -> "check1" [label="verify_commitments_and_dlog_proof, ID"] + "check1" -> C [label="yes,ID"] + "check1" -> stop [label=no] + C -> D [label="generate_correct_key_challenge,ID"] + D -> "check2" [label="verify_correct_key, ID"] + "check2" -> E [label="yes,ID"] + "check2" -> stop [label=no] + E -> "check3" [label="verify_range_proof, ID"] + "check3" -> F [label="yes,ID"] + "check3" -> stop [label=no] + F -> stop [label="write_to_local_storage,ID"] + +} diff --git a/multi-party-ecdsa/docs/gg19.pdf b/multi-party-ecdsa/docs/gg19.pdf new file mode 100644 index 0000000000000000000000000000000000000000..413ba89317202d7f74e139e622df429c5de2b463 GIT binary patch literal 189350 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�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 diff --git a/multi-party-ecdsa/demo/run.sh b/multi-party-ecdsa/demo/run.sh deleted file mode 100755 index b608be1e..00000000 --- a/multi-party-ecdsa/demo/run.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -cargo +nightly build --examples --release - -file_as_string=`cat params.json` - -n=`echo "$file_as_string" | cut -d "\"" -f 4 ` -t=`echo "$file_as_string" | cut -d "\"" -f 8 ` - -echo "Multi-party ECDSA parties:$n threshold:$t" -#clean -sleep 1 - -rm keys?.store -killall gg18_sm_manager gg18_keygen_client gg18_sign_client 2> /dev/null - -./target/release/examples/gg18_sm_manager & - -sleep 2 -echo "keygen part" - -for i in $(seq 1 $n) -do - echo "key gen for client $i out of $n" - ./target/release/examples/gg18_keygen_client http://127.0.0.1:8001 keys$i.store & - sleep 3 -done - - - -sleep 5 -echo "sign" - -for i in $(seq 1 $((t+1))); -do - echo "signing for client $i out of $((t+1))" - ./target/release/examples/gg18_sign_client http://127.0.0.1:8001 keys$i.store "KZen Networks" & - sleep 3 -done - -killall gg18_sm_manager 2> /dev/null From 958159f11b7555b50b36a1f7fa90f659db418399 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 08:47:36 +0800 Subject: [PATCH 26/41] Remove setup script. --- multi-party-ecdsa/kzen-dev-setup.sh | 9 --------- 1 file changed, 9 deletions(-) delete mode 100755 multi-party-ecdsa/kzen-dev-setup.sh diff --git a/multi-party-ecdsa/kzen-dev-setup.sh b/multi-party-ecdsa/kzen-dev-setup.sh deleted file mode 100755 index f926700c..00000000 --- a/multi-party-ecdsa/kzen-dev-setup.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -echo "KZen dev Setup" -echo "Installing Git hooks..." -git clone https://github.com/KZen-networks/scripts.git -chmod -R +x scripts/git/hooks/rust/ -cp scripts/git/hooks/rust/* .git/hooks/ -rm -rf scripts -echo "Done." \ No newline at end of file From 574d97b5eae61e8ed3b5178eaa92959e6f8c96d4 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 08:47:51 +0800 Subject: [PATCH 27/41] Remove .rustfmt.toml, provided by the workspace now. --- multi-party-ecdsa/.rustfmt.toml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 multi-party-ecdsa/.rustfmt.toml diff --git a/multi-party-ecdsa/.rustfmt.toml b/multi-party-ecdsa/.rustfmt.toml deleted file mode 100644 index 32a9786f..00000000 --- a/multi-party-ecdsa/.rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -edition = "2018" From 45b6e947a4ac2d294b91e27cea27f485f2129ce1 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 08:49:33 +0800 Subject: [PATCH 28/41] Update edition in multi-party-ecdsa. --- multi-party-ecdsa/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi-party-ecdsa/Cargo.toml b/multi-party-ecdsa/Cargo.toml index 0264c01e..705cc5de 100644 --- a/multi-party-ecdsa/Cargo.toml +++ b/multi-party-ecdsa/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "multi-party-ecdsa" version = "0.8.2" -edition = "2018" +edition = "2021" authors = [ "Gary ", "Omer " From 98cb08682e2521496163a55d20d4196cd228d77a Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 08:50:21 +0800 Subject: [PATCH 29/41] Update edition in fs-dkr. --- fs-dkr/Cargo.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fs-dkr/Cargo.toml b/fs-dkr/Cargo.toml index 113dedcc..6bec9133 100644 --- a/fs-dkr/Cargo.toml +++ b/fs-dkr/Cargo.toml @@ -6,9 +6,7 @@ authors = [ "Tudor Cebere ", "Drew Stone ", ] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +edition = "2021" [dependencies.paillier] version = "0.4.3" From 31cf40901679fbbc45cbf343d732f2f82950af65 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 08:51:19 +0800 Subject: [PATCH 30/41] Formatting. --- multi-party-ecdsa/examples/common.rs | 43 +-- multi-party-ecdsa/examples/gg20_keygen.rs | 14 +- multi-party-ecdsa/examples/gg20_signing.rs | 45 ++- multi-party-ecdsa/examples/gg20_sm_client.rs | 54 ++- multi-party-ecdsa/examples/gg20_sm_manager.rs | 59 ++-- .../multi_party_ecdsa/gg_2020/blame.rs | 96 ++++-- .../multi_party_ecdsa/gg_2020/party_i.rs | 193 ++++++----- .../gg_2020/state_machine/keygen.rs | 238 ++++++++----- .../gg_2020/state_machine/keygen/rounds.rs | 81 +++-- .../gg_2020/state_machine/sign.rs | 326 +++++++++++------- .../gg_2020/state_machine/sign/fmt.rs | 6 +- .../gg_2020/state_machine/sign/rounds.rs | 205 ++++++----- .../gg_2020/state_machine/traits.rs | 3 +- .../multi_party_ecdsa/gg_2020/test.rs | 243 ++++++++----- multi-party-ecdsa/src/utilities/mta/mod.rs | 72 ++-- .../src/utilities/mta/range_proofs.rs | 214 ++++++------ multi-party-ecdsa/src/utilities/mta/test.rs | 11 +- multi-party-ecdsa/src/utilities/zk_pdl/mod.rs | 57 +-- .../src/utilities/zk_pdl/test.rs | 40 +-- .../src/utilities/zk_pdl_with_slack/mod.rs | 64 ++-- .../src/utilities/zk_pdl_with_slack/test.rs | 35 +- 21 files changed, 1223 insertions(+), 876 deletions(-) diff --git a/multi-party-ecdsa/examples/common.rs b/multi-party-ecdsa/examples/common.rs index 1fbacbc3..d21a3f92 100644 --- a/multi-party-ecdsa/examples/common.rs +++ b/multi-party-ecdsa/examples/common.rs @@ -2,8 +2,10 @@ use std::{env, thread, time, time::Duration}; -use aes_gcm::aead::{Aead, NewAead}; -use aes_gcm::{Aes256Gcm, Nonce}; +use aes_gcm::{ + aead::{Aead, NewAead}, + Aes256Gcm, Nonce, +}; use rand::{rngs::OsRng, RngCore}; use curv::{ @@ -58,14 +60,10 @@ pub fn aes_encrypt(key: &[u8], plaintext: &[u8]) -> AEAD { OsRng.fill_bytes(&mut nonce); let nonce = Nonce::from_slice(&nonce); - let ciphertext = cipher - .encrypt(nonce, plaintext) - .expect("encryption failure!"); + let ciphertext = + cipher.encrypt(nonce, plaintext).expect("encryption failure!"); - AEAD { - ciphertext: ciphertext, - tag: nonce.to_vec(), - } + AEAD { ciphertext, tag: nonce.to_vec() } } #[allow(dead_code)] @@ -88,13 +86,10 @@ where let retries = 3; let retry_delay = time::Duration::from_millis(250); for _i in 1..retries { - let res = client - .post(&format!("{}/{}", addr, path)) - .json(&body) - .send(); + let res = client.post(&format!("{}/{}", addr, path)).json(&body).send(); if let Ok(mut res) = res { - return Some(res.text().unwrap()); + return Some(res.text().unwrap()) } thread::sleep(retry_delay); } @@ -148,11 +143,15 @@ pub fn poll_for_broadcasts( // add delay to allow the server to process request: thread::sleep(delay); let res_body = postb(client, "get", index.clone()).unwrap(); - let answer: Result = serde_json::from_str(&res_body).unwrap(); + let answer: Result = + serde_json::from_str(&res_body).unwrap(); if let Ok(answer) = answer { ans_vec.push(answer.value); - println!("[{:?}] party {:?} => party {:?}", round, i, party_num); - break; + println!( + "[{:?}] party {:?} => party {:?}", + round, i, party_num + ); + break } } } @@ -177,11 +176,15 @@ pub fn poll_for_p2p( // add delay to allow the server to process request: thread::sleep(delay); let res_body = postb(client, "get", index.clone()).unwrap(); - let answer: Result = serde_json::from_str(&res_body).unwrap(); + let answer: Result = + serde_json::from_str(&res_body).unwrap(); if let Ok(answer) = answer { ans_vec.push(answer.value); - println!("[{:?}] party {:?} => party {:?}", round, i, party_num); - break; + println!( + "[{:?}] party {:?} => party {:?}", + round, i, party_num + ); + break } } } diff --git a/multi-party-ecdsa/examples/gg20_keygen.rs b/multi-party-ecdsa/examples/gg20_keygen.rs index 2de51dac..f89361b4 100644 --- a/multi-party-ecdsa/examples/gg20_keygen.rs +++ b/multi-party-ecdsa/examples/gg20_keygen.rs @@ -44,12 +44,14 @@ async fn main() -> Result<()> { tokio::pin!(incoming); tokio::pin!(outgoing); - let keygen = Keygen::new(args.index, args.threshold, args.number_of_parties)?; - let output = AsyncProtocol::new(keygen, incoming, outgoing) - .run() - .await - .map_err(|e| anyhow!("protocol execution terminated with error: {}", e))?; - let output = serde_json::to_vec_pretty(&output).context("serialize output")?; + let keygen = + Keygen::new(args.index, args.threshold, args.number_of_parties)?; + let output = + AsyncProtocol::new(keygen, incoming, outgoing).run().await.map_err( + |e| anyhow!("protocol execution terminated with error: {}", e), + )?; + let output = + serde_json::to_vec_pretty(&output).context("serialize output")?; tokio::io::copy(&mut output.as_slice(), &mut output_file) .await .context("save output to file")?; diff --git a/multi-party-ecdsa/examples/gg20_signing.rs b/multi-party-ecdsa/examples/gg20_signing.rs index 59e3c23b..ba8b6f97 100644 --- a/multi-party-ecdsa/examples/gg20_signing.rs +++ b/multi-party-ecdsa/examples/gg20_signing.rs @@ -4,8 +4,7 @@ use anyhow::{anyhow, Context, Result}; use futures::{SinkExt, StreamExt, TryStreamExt}; use structopt::StructOpt; -use curv::arithmetic::Converter; -use curv::BigInt; +use curv::{arithmetic::Converter, BigInt}; use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::sign::{ OfflineStage, SignManual, @@ -37,27 +36,31 @@ async fn main() -> Result<()> { let local_share = tokio::fs::read(args.local_share) .await .context("cannot read local share")?; - let local_share = serde_json::from_slice(&local_share).context("parse local share")?; + let local_share = + serde_json::from_slice(&local_share).context("parse local share")?; let number_of_parties = args.parties.len(); - let (i, incoming, outgoing) = - join_computation(args.address.clone(), &format!("{}-offline", args.room)) - .await - .context("join offline computation")?; + let (i, incoming, outgoing) = join_computation( + args.address.clone(), + &format!("{}-offline", args.room), + ) + .await + .context("join offline computation")?; let incoming = incoming.fuse(); tokio::pin!(incoming); tokio::pin!(outgoing); let signing = OfflineStage::new(i, args.parties, local_share)?; - let completed_offline_stage = AsyncProtocol::new(signing, incoming, outgoing) - .run() - .await - .map_err(|e| anyhow!("protocol execution terminated with error: {}", e))?; + let completed_offline_stage = + AsyncProtocol::new(signing, incoming, outgoing).run().await.map_err( + |e| anyhow!("protocol execution terminated with error: {}", e), + )?; - let (i, incoming, outgoing) = join_computation(args.address, &format!("{}-online", args.room)) - .await - .context("join online computation")?; + let (i, incoming, outgoing) = + join_computation(args.address, &format!("{}-online", args.room)) + .await + .context("join online computation")?; tokio::pin!(incoming); tokio::pin!(outgoing); @@ -68,11 +71,7 @@ async fn main() -> Result<()> { )?; outgoing - .send(Msg { - sender: i, - receiver: None, - body: partial_signature, - }) + .send(Msg { sender: i, receiver: None, body: partial_signature }) .await?; let partial_signatures: Vec<_> = incoming @@ -80,10 +79,10 @@ async fn main() -> Result<()> { .map_ok(|msg| msg.body) .try_collect() .await?; - let signature = signing - .complete(&partial_signatures) - .context("online stage failed")?; - let signature = serde_json::to_string(&signature).context("serialize signature")?; + let signature = + signing.complete(&partial_signatures).context("online stage failed")?; + let signature = + serde_json::to_string(&signature).context("serialize signature")?; println!("{}", signature); Ok(()) diff --git a/multi-party-ecdsa/examples/gg20_sm_client.rs b/multi-party-ecdsa/examples/gg20_sm_client.rs index f28c44dd..c42b6e9c 100644 --- a/multi-party-ecdsa/examples/gg20_sm_client.rs +++ b/multi-party-ecdsa/examples/gg20_sm_client.rs @@ -18,16 +18,15 @@ pub async fn join_computation( where M: Serialize + DeserializeOwned, { - let client = SmClient::new(address, room_id).context("construct SmClient")?; + let client = + SmClient::new(address, room_id).context("construct SmClient")?; // Construct channel of incoming messages - let incoming = client - .subscribe() - .await - .context("subscribe")? - .and_then(|msg| async move { + let incoming = client.subscribe().await.context("subscribe")?.and_then( + |msg| async move { serde_json::from_str::>(&msg).context("deserialize message") - }); + }, + ); // Obtain party index let index = client.issue_index().await.context("issue an index")?; @@ -35,19 +34,19 @@ where // Ignore incoming messages addressed to someone else let incoming = incoming.try_filter(move |msg| { futures::future::ready( - msg.sender != index && (msg.receiver.is_none() || msg.receiver == Some(index)), + msg.sender != index && + (msg.receiver.is_none() || msg.receiver == Some(index)), ) }); // Construct channel of outgoing messages - let outgoing = futures::sink::unfold(client, |client, message: Msg| async move { - let serialized = serde_json::to_string(&message).context("serialize message")?; - client - .broadcast(&serialized) - .await - .context("broadcast message")?; - Ok::<_, anyhow::Error>(client) - }); + let outgoing = + futures::sink::unfold(client, |client, message: Msg| async move { + let serialized = + serde_json::to_string(&message).context("serialize message")?; + client.broadcast(&serialized).await.context("broadcast message")?; + Ok::<_, anyhow::Error>(client) + }); Ok((index, incoming, outgoing)) } @@ -61,9 +60,7 @@ impl SmClient { let config = surf::Config::new() .set_base_url(address.join(&format!("rooms/{}/", room_id))?) .set_timeout(None); - Ok(Self { - http_client: config.try_into()?, - }) + Ok(Self { http_client: config.try_into()? }) } pub async fn issue_index(&self) -> Result { @@ -85,7 +82,9 @@ impl SmClient { Ok(()) } - pub async fn subscribe(&self) -> Result>> { + pub async fn subscribe( + &self, + ) -> Result>> { let response = self .http_client .get("subscribe") @@ -101,7 +100,7 @@ impl SmClient { Ok(_) => { // ignore other types of events None - } + }, Err(e) => Some(Err(e.into_inner())), } })) @@ -137,23 +136,22 @@ enum Cmd { #[allow(dead_code)] async fn main() -> Result<()> { let args: Cli = Cli::from_args(); - let client = SmClient::new(args.address, &args.room).context("create SmClient")?; + let client = + SmClient::new(args.address, &args.room).context("create SmClient")?; match args.cmd { - Cmd::Broadcast { message } => client - .broadcast(&message) - .await - .context("broadcast message")?, + Cmd::Broadcast { message } => + client.broadcast(&message).await.context("broadcast message")?, Cmd::IssueIdx => { let index = client.issue_index().await.context("issue index")?; println!("Index: {}", index); - } + }, Cmd::Subscribe => { let messages = client.subscribe().await.context("subsribe")?; tokio::pin!(messages); while let Some(message) = messages.next().await { println!("{:?}", message); } - } + }, } Ok(()) } diff --git a/multi-party-ecdsa/examples/gg20_sm_manager.rs b/multi-party-ecdsa/examples/gg20_sm_manager.rs index 087f37ca..3282f2b0 100644 --- a/multi-party-ecdsa/examples/gg20_sm_manager.rs +++ b/multi-party-ecdsa/examples/gg20_sm_manager.rs @@ -1,16 +1,20 @@ -use std::collections::hash_map::{Entry, HashMap}; -use std::sync::{ - atomic::{AtomicU16, Ordering}, - Arc, +use std::{ + collections::hash_map::{Entry, HashMap}, + sync::{ + atomic::{AtomicU16, Ordering}, + Arc, + }, }; use futures::Stream; -use rocket::data::ToByteUnit; -use rocket::http::Status; -use rocket::request::{FromRequest, Outcome, Request}; -use rocket::response::stream::{stream, Event, EventStream}; -use rocket::serde::json::Json; -use rocket::State; +use rocket::{ + data::ToByteUnit, + http::Status, + request::{FromRequest, Outcome, Request}, + response::stream::{stream, Event, EventStream}, + serde::json::Json, + State, +}; use serde::{Deserialize, Serialize}; use tokio::sync::{Notify, RwLock}; @@ -63,9 +67,7 @@ struct Room { impl Db { pub fn empty() -> Self { - Self { - rooms: RwLock::new(HashMap::new()), - } + Self { rooms: RwLock::new(HashMap::new()) } } pub async fn get_room_or_create_empty(&self, room_id: &str) -> Arc { @@ -73,20 +75,22 @@ impl Db { if let Some(room) = rooms.get(room_id) { // If no one is watching this room - we need to clean it up first if !room.is_abandoned() { - return room.clone(); + return room.clone() } } drop(rooms); let mut rooms = self.rooms.write().await; match rooms.entry(room_id.to_owned()) { - Entry::Occupied(entry) if !entry.get().is_abandoned() => entry.get().clone(), + Entry::Occupied(entry) if !entry.get().is_abandoned() => + entry.get().clone(), Entry::Occupied(entry) => { let room = Arc::new(Room::empty()); *entry.into_mut() = room.clone(); room - } - Entry::Vacant(entry) => entry.insert(Arc::new(Room::empty())).clone(), + }, + Entry::Vacant(entry) => + entry.insert(Arc::new(Room::empty())).clone(), } } } @@ -107,7 +111,10 @@ impl Room { self.message_appeared.notify_waiters(); } - pub fn subscribe(self: Arc, last_seen_msg: Option) -> Subscription { + pub fn subscribe( + self: Arc, + last_seen_msg: Option, + ) -> Subscription { self.subscribers.fetch_add(1, Ordering::SeqCst); Subscription { room: self, @@ -136,7 +143,7 @@ impl Subscription { if let Some(msg) = history.get(usize::from(self.next_event)) { let event_id = self.next_event; self.next_event = event_id + 1; - return (event_id, msg.clone()); + return (event_id, msg.clone()) } let notification = self.room.message_appeared.notified(); drop(history); @@ -158,16 +165,20 @@ struct LastEventId(Option); impl<'r> FromRequest<'r> for LastEventId { type Error = &'static str; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request( + request: &'r Request<'_>, + ) -> Outcome { let header = request .headers() .get_one("Last-Event-ID") .map(|id| id.parse::()); match header { - Some(Ok(last_seen_msg)) => Outcome::Success(LastEventId(Some(last_seen_msg))), - Some(Err(_parse_err)) => { - Outcome::Failure((Status::BadRequest, "last seen msg id is not valid")) - } + Some(Ok(last_seen_msg)) => + Outcome::Success(LastEventId(Some(last_seen_msg))), + Some(Err(_parse_err)) => Outcome::Failure(( + Status::BadRequest, + "last seen msg id is not valid", + )), None => Outcome::Success(LastEventId(None)), } } diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs index 4ed04443..71db7b37 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs @@ -14,18 +14,22 @@ @license GPL-3.0+ */ -use crate::protocols::multi_party_ecdsa::gg_2020::ErrorType; -use crate::utilities::mta::{MessageA, MessageB}; -use curv::cryptographic_primitives::proofs::sigma_ec_ddh::ECDDHProof; -use curv::cryptographic_primitives::proofs::sigma_ec_ddh::ECDDHStatement; -use curv::cryptographic_primitives::proofs::sigma_ec_ddh::ECDDHWitness; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; -use paillier::traits::EncryptWithChosenRandomness; -use paillier::traits::Open; -use paillier::DecryptionKey; -use paillier::Paillier; -use paillier::{EncryptionKey, Randomness, RawCiphertext, RawPlaintext}; +use crate::{ + protocols::multi_party_ecdsa::gg_2020::ErrorType, + utilities::mta::{MessageA, MessageB}, +}; +use curv::{ + cryptographic_primitives::proofs::sigma_ec_ddh::{ + ECDDHProof, ECDDHStatement, ECDDHWitness, + }, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, +}; +use paillier::{ + traits::{EncryptWithChosenRandomness, Open}, + DecryptionKey, EncryptionKey, Paillier, Randomness, RawCiphertext, + RawPlaintext, +}; use serde::{Deserialize, Serialize}; use sha2::Sha256; @@ -55,14 +59,16 @@ pub struct GlobalStatePhase5 { } // TODO: check all parties submitted inputs -// TODO: if not - abort gracefully with list of parties that did not produce inputs +// TODO: if not - abort gracefully with list of parties that did not produce +// inputs impl GlobalStatePhase5 { pub fn local_state_to_global_state( encryption_key_vec: &[EncryptionKey], - delta_vec: &[Scalar], //to test against delta_vec - g_gamma_vec: &[Point], // to test against the opened commitment for g_gamma - m_a_vec: &[MessageA], // to test against broadcast message A - m_b_mat: Vec>, // to test against broadcast message B + delta_vec: &[Scalar], //to test against delta_vec + g_gamma_vec: &[Point], /* to test against the opened + * commitment for g_gamma */ + m_a_vec: &[MessageA], // to test against broadcast message A + m_b_mat: Vec>, // to test against broadcast message B local_state_vec: &[LocalStatePhase5], ) -> Self { let len = local_state_vec.len(); @@ -98,7 +104,9 @@ impl GlobalStatePhase5 { }) .collect::>>(); - // let encryption_key_vec = (0..len).map(|i| local_state_vec[i].encryption_key.clone() ).collect::>(); + // let encryption_key_vec = (0..len).map(|i| + // local_state_vec[i].encryption_key.clone() + // ).collect::>(); GlobalStatePhase5 { k_vec, k_randomness_vec, @@ -142,26 +150,29 @@ impl GlobalStatePhase5 { (0..len - 1) .map(|j| { let ind = if j < i { j } else { j + 1 }; - let (message_b, beta) = MessageB::b_with_predefined_randomness( - &self.gamma_vec[ind], - &self.encryption_key_vec[i], - message_a.clone(), - &self.beta_randomness_vec[i][j], - &self.beta_tag_vec[i][j], - &[], - ) - .unwrap(); + let (message_b, beta) = + MessageB::b_with_predefined_randomness( + &self.gamma_vec[ind], + &self.encryption_key_vec[i], + message_a.clone(), + &self.beta_randomness_vec[i][j], + &self.beta_tag_vec[i][j], + &[], + ) + .unwrap(); // check message_b if message_b.c != self.m_b_mat[i][j].c { bad_signers_vec.push(ind) } - let k_i_gamma_j = &self.k_vec[i] * &self.gamma_vec[ind]; + let k_i_gamma_j = + &self.k_vec[i] * &self.gamma_vec[ind]; let alpha = k_i_gamma_j - β (alpha, beta) }) - .collect::, Scalar)>>() + .collect::, Scalar)>>( + ) } else { vec![] } @@ -174,8 +185,9 @@ impl GlobalStatePhase5 { // [P4, P4, P4, P3, ...] // [..., ...] // [Pn, Pn, Pn, Pn, ...] - // We have n columns, one for each party for all the times the party played alice. - // The Pi's indicate the counter party that played bob + // We have n columns, one for each party for all the times the party + // played alice. The Pi's indicate the counter party that played + // bob // we only proceed to check the blame if everyone opened values that are // consistent with publicly known commitments and ciphertexts @@ -234,7 +246,8 @@ pub struct LocalStatePhase6 { pub proof_of_eq_dlog: ECDDHProof, } -// It is assumed the second message of MtAwc (ciphertext from b to a) is broadcasted in the original protocol +// It is assumed the second message of MtAwc (ciphertext from b to a) is +// broadcasted in the original protocol #[derive(Clone, Debug, Serialize, Deserialize)] pub struct GlobalStatePhase6 { pub k_vec: Vec>, @@ -250,7 +263,10 @@ pub struct GlobalStatePhase6 { } impl GlobalStatePhase6 { - pub fn extract_paillier_randomness(ciphertext: &BigInt, dk: &DecryptionKey) -> BigInt { + pub fn extract_paillier_randomness( + ciphertext: &BigInt, + dk: &DecryptionKey, + ) -> BigInt { let raw_c = RawCiphertext::from(ciphertext.clone()); let (_plaintext, randomness) = Paillier::open(dk, raw_c); randomness.0 @@ -272,12 +288,13 @@ impl GlobalStatePhase6 { } // TODO: check all parties submitted inputs - // TODO: if not - abort gracefully with list of parties that did not produce inputs + // TODO: if not - abort gracefully with list of parties that did not produce + // inputs pub fn local_state_to_global_state( encryption_key_vec: &[EncryptionKey], S_vec: &[Point], g_w_vec: &[Point], - m_a_vec: &[MessageA], // to test against broadcast message A + m_a_vec: &[MessageA], // to test against broadcast message A m_b_mat: Vec>, // to test against broadcast message B local_state_vec: &[LocalStatePhase6], ) -> Self { @@ -379,7 +396,8 @@ impl GlobalStatePhase6 { .map(|i| { let g_wi_ki = &self.g_w_vec[i] * &self.k_vec[i]; let sum = self.miu_vec[i].iter().fold(g_wi_ki, |acc, x| { - acc + (Point::generator() * &Scalar::::from(x)) + acc + (Point::generator() * + &Scalar::::from(x)) }); sum }) @@ -390,7 +408,8 @@ impl GlobalStatePhase6 { for j in 0..len - 1 { let ind1 = if j < i { j } else { j + 1 }; let ind2 = if j < i { i - 1 } else { i }; - g_sigma_i_vec[i] = &g_sigma_i_vec[i] + &g_ni_mat[ind1][ind2]; + g_sigma_i_vec[i] = + &g_sigma_i_vec[i] + &g_ni_mat[ind1][ind2]; } } @@ -439,7 +458,8 @@ impl GlobalStatePhase7 { for i in 0..len { let R_si = &self.R * &self.s_vec[i]; - let R_dash_m = &self.R_dash_vec[i] * &Scalar::::from(&self.m); + let R_dash_m = + &self.R_dash_vec[i] * &Scalar::::from(&self.m); let Si_r = &self.S_vec[i] * &self.r; let right = R_dash_m + Si_r; let left = R_si; diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs index 0dcfe266..56cc8151 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs @@ -18,29 +18,41 @@ use std::fmt::Debug; -use centipede::juggling::proof_system::{Helgamalsegmented, Witness}; -use centipede::juggling::segmentation::Msegmentation; -use curv::arithmetic::traits::*; -use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; -use curv::cryptographic_primitives::commitments::traits::Commitment; -use curv::cryptographic_primitives::proofs::sigma_correct_homomorphic_elgamal_enc::*; -use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Curve, Point, Scalar}; -use curv::BigInt; +use centipede::juggling::{ + proof_system::{Helgamalsegmented, Witness}, + segmentation::Msegmentation, +}; +use curv::{ + arithmetic::traits::*, + cryptographic_primitives::{ + commitments::{hash_commitment::HashCommitment, traits::Commitment}, + proofs::{ + sigma_correct_homomorphic_elgamal_enc::*, sigma_dlog::DLogProof, + }, + }, + elliptic::curves::{secp256_k1::Secp256k1, Curve, Point, Scalar}, + BigInt, +}; use sha2::Sha256; use super::VerifiableSS; use crate::Error::{self, InvalidSig, Phase5BadSum, Phase6Error}; use paillier::{ - Decrypt, DecryptionKey, EncryptionKey, KeyGeneration, Paillier, RawCiphertext, RawPlaintext, + Decrypt, DecryptionKey, EncryptionKey, KeyGeneration, Paillier, + RawCiphertext, RawPlaintext, }; use serde::{Deserialize, Serialize}; -use zk_paillier::zkproofs::NiCorrectKeyProof; -use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; +use zk_paillier::zkproofs::{ + CompositeDLogProof, DLogStatement, NiCorrectKeyProof, +}; -use crate::protocols::multi_party_ecdsa::gg_2020::ErrorType; -use crate::utilities::zk_pdl_with_slack::{PDLwSlackProof, PDLwSlackStatement, PDLwSlackWitness}; +use crate::{ + protocols::multi_party_ecdsa::gg_2020::ErrorType, + utilities::zk_pdl_with_slack::{ + PDLwSlackProof, PDLwSlackStatement, PDLwSlackWitness, + }, +}; use curv::cryptographic_primitives::proofs::sigma_valid_pedersen::PedersenProof; use std::convert::TryInto; @@ -250,10 +262,8 @@ impl Keys { composite_dlog_proof_base_h1, composite_dlog_proof_base_h2, }; - let decom1 = KeyGenDecommitMessage1 { - blind_factor, - y_i: self.y_i.clone(), - }; + let decom1 = + KeyGenDecommitMessage1 { blind_factor, y_i: self.y_i.clone() }; (bcm1, decom1) } @@ -262,12 +272,16 @@ impl Keys { params: &Parameters, decom_vec: &[KeyGenDecommitMessage1], bc1_vec: &[KeyGenBroadcastMessage1], - ) -> Result<(VerifiableSS, Vec>, usize), ErrorType> { + ) -> Result< + (VerifiableSS, Vec>, usize), + ErrorType, + > { let mut bad_actors_vec = Vec::new(); // test length: assert_eq!(decom_vec.len(), usize::from(params.share_count)); assert_eq!(bc1_vec.len(), usize::from(params.share_count)); - // test paillier correct key, h1,h2 correct generation and test decommitments + // test paillier correct key, h1,h2 correct generation and test + // decommitments let correct_key_correct_decom_all = (0..bc1_vec.len()) .map(|i| { let dlog_statement_base_h2 = DLogStatement { @@ -358,8 +372,11 @@ impl Keys { data: Vec::new(), }; - let (vss_scheme, secret_shares) = - VerifiableSS::share(params.threshold, params.share_count, &self.u_i); + let (vss_scheme, secret_shares) = VerifiableSS::share( + params.threshold, + params.share_count, + &self.u_i, + ); if correct_key_correct_decom_all { Ok((vss_scheme, secret_shares.to_vec(), self.party_index)) } else { @@ -374,7 +391,8 @@ impl Keys { secret_shares_vec: &[Scalar], vss_scheme_vec: &[VerifiableSS], index: usize, - ) -> Result<(SharedKeys, DLogProof), ErrorType> { + ) -> Result<(SharedKeys, DLogProof), ErrorType> + { let mut bad_actors_vec = Vec::new(); assert_eq!(y_vec.len(), usize::from(params.share_count)); assert_eq!(secret_shares_vec.len(), usize::from(params.share_count)); @@ -383,9 +401,12 @@ impl Keys { let correct_ss_verify = (0..y_vec.len()) .map(|i| { let res = vss_scheme_vec[i] - .validate_share(&secret_shares_vec[i], index.try_into().unwrap()) - .is_ok() - && vss_scheme_vec[i].commitments[0] == y_vec[i]; + .validate_share( + &secret_shares_vec[i], + index.try_into().unwrap(), + ) + .is_ok() && + vss_scheme_vec[i].commitments[0] == y_vec[i]; if !res { bad_actors_vec.push(i); false @@ -422,8 +443,11 @@ impl Keys { let (head, tail) = vss_scheme_vec.split_at(1); let mut global_coefficients = head[0].commitments.clone(); for vss in tail { - for (i, coefficient_commitment) in vss.commitments.iter().enumerate() { - global_coefficients[i] = &global_coefficients[i] + coefficient_commitment; + for (i, coefficient_commitment) in + vss.commitments.iter().enumerate() + { + global_coefficients[i] = + &global_coefficients[i] + coefficient_commitment; } } @@ -465,7 +489,8 @@ impl Keys { let xi_dlog_verify = (0..y_vec.len()) .map(|i| { let ver_res = DLogProof::verify(&dlog_proofs_vec[i]).is_ok(); - let verify_against_vss = xi_commitments[i] == dlog_proofs_vec[i].pk; + let verify_against_vss = + xi_commitments[i] == dlog_proofs_vec[i].pk; if !ver_res || !verify_against_vss { bad_actors_vec.push(i); false @@ -491,11 +516,7 @@ impl Keys { impl PartyPrivate { pub fn set_private(key: Keys, shared_key: SharedKeys) -> Self { - Self { - u_i: key.u_i, - x_i: shared_key.x_i, - dk: key.dk, - } + Self { u_i: key.u_i, x_i: shared_key.x_i, dk: key.dk } } pub fn y_i(&self) -> Point { @@ -507,7 +528,11 @@ impl PartyPrivate { Paillier::decrypt(&self.dk, &RawCiphertext::from(ciphertext)) } - pub fn refresh_private_key(&self, factor: &Scalar, index: usize) -> Keys { + pub fn refresh_private_key( + &self, + factor: &Scalar, + index: usize, + ) -> Keys { let u: Scalar = &self.u_i + factor; let y = Point::generator() * &u; let (ek, dk) = Paillier::keypair().keys(); @@ -529,7 +554,11 @@ impl PartyPrivate { } // we recommend using safe primes if the code is used in production - pub fn refresh_private_key_safe_prime(&self, factor: &Scalar, index: usize) -> Keys { + pub fn refresh_private_key_safe_prime( + &self, + factor: &Scalar, + index: usize, + ) -> Keys { let u: Scalar = &self.u_i + factor; let y = Point::generator() * &u; let (ek, dk) = Paillier::keypair_safe_primes().keys(); @@ -558,7 +587,13 @@ impl PartyPrivate { pub_ke_y: &Point, g: &Point, ) -> (Witness, Helgamalsegmented) { - Msegmentation::to_encrypted_segments(&self.u_i, &segment_size, num_of_segments, pub_ke_y, g) + Msegmentation::to_encrypted_segments( + &self.u_i, + &segment_size, + num_of_segments, + pub_ke_y, + g, + ) } pub fn update_private_key( @@ -612,16 +647,12 @@ impl SignKeys { let gamma_i = Scalar::::random(); let g_gamma_i = g * &gamma_i; let k_i = Scalar::::random(); - Self { - w_i, - g_w_i, - k_i, - gamma_i, - g_gamma_i, - } + Self { w_i, g_w_i, k_i, gamma_i, g_gamma_i } } - pub fn phase1_broadcast(&self) -> (SignBroadcastPhase1, SignDecommitPhase1) { + pub fn phase1_broadcast( + &self, + ) -> (SignBroadcastPhase1, SignDecommitPhase1) { let blind_factor = BigInt::sample(SECURITY); let g = Point::generator(); let g_gamma_i = g * &self.gamma_i; @@ -670,11 +701,8 @@ impl SignKeys { pub fn phase3_compute_t_i( sigma_i: &Scalar, - ) -> ( - Point, - Scalar, - PedersenProof, - ) { + ) -> (Point, Scalar, PedersenProof) + { let g_sigma_i = Point::generator() * sigma_i; let l = Scalar::::random(); let h_l = Point::::base_point2() * &l; @@ -683,7 +711,9 @@ impl SignKeys { (T, l, T_zk_proof) } - pub fn phase3_reconstruct_delta(delta_vec: &[Scalar]) -> Scalar { + pub fn phase3_reconstruct_delta( + delta_vec: &[Scalar], + ) -> Scalar { let sum = delta_vec .iter() .fold(Scalar::::zero(), |acc, x| acc + x); @@ -729,7 +759,8 @@ impl SignKeys { if test_b_vec_and_com { Ok({ - let gamma_sum = tail.fold(head.g_gamma_i.clone(), |acc, x| acc + &x.g_gamma_i); + let gamma_sum = tail + .fold(head.g_gamma_i.clone(), |acc, x| acc + &x.g_gamma_i); // R gamma_sum * delta_inv }) @@ -760,10 +791,8 @@ impl LocalSignature { N_tilde: dlog_statement.N.clone(), }; - let pdl_w_slack_witness = PDLwSlackWitness { - x: k_i.clone(), - r: k_enc_randomness.clone(), - }; + let pdl_w_slack_witness = + PDLwSlackWitness { x: k_i.clone(), r: k_enc_randomness.clone() }; PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement) } @@ -796,7 +825,8 @@ impl LocalSignature { h2: dlog_statement[s[ind]].ni.clone(), N_tilde: dlog_statement[s[ind]].N.clone(), }; - let ver_res = pdl_w_slack_proof_vec[j].verify(&pdl_w_slack_statement); + let ver_res = + pdl_w_slack_proof_vec[j].verify(&pdl_w_slack_statement); if ver_res.is_err() { bad_actors_vec.push(i); false @@ -806,7 +836,7 @@ impl LocalSignature { }) .all(|x| x); if proofs_verification { - return Ok(()); + return Ok(()) } } @@ -818,11 +848,15 @@ impl LocalSignature { Err(err_type) } - pub fn phase5_check_R_dash_sum(R_dash_vec: &[Point]) -> Result<(), Error> { + pub fn phase5_check_R_dash_sum( + R_dash_vec: &[Point], + ) -> Result<(), Error> { let sum = R_dash_vec .iter() .fold(Point::generator().to_point(), |acc, x| acc + x); - match sum - &Point::generator().to_point() == Point::generator().to_point() { + match sum - &Point::generator().to_point() == + Point::generator().to_point() + { true => Ok(()), false => Err(Phase5BadSum), } @@ -842,10 +876,7 @@ impl LocalSignature { D: T.clone(), E: S.clone(), }; - let witness = HomoElGamalWitness { - x: l.clone(), - r: sigma.clone(), - }; + let witness = HomoElGamalWitness { x: l.clone(), r: sigma.clone() }; let proof = HomoELGamalProof::prove(&witness, &delta); (S, proof) @@ -882,7 +913,7 @@ impl LocalSignature { data: Vec::new(), }; Err(err_type) - } + }, } } @@ -890,9 +921,8 @@ impl LocalSignature { pubkey_y: &Point, S_vec: &[Point], ) -> Result<(), Error> { - let sum_plus_g = S_vec - .iter() - .fold(Point::generator().to_point(), |acc, x| acc + x); + let sum_plus_g = + S_vec.iter().fold(Point::generator().to_point(), |acc, x| acc + x); let sum = sum_plus_g - &Point::generator().to_point(); match &sum == pubkey_y { @@ -910,21 +940,16 @@ impl LocalSignature { ) -> Self { let m_fe = Scalar::::from(message); let r = Scalar::::from( - &R.x_coord() - .unwrap() - .mod_floor(Scalar::::group_order()), + &R.x_coord().unwrap().mod_floor(Scalar::::group_order()), ); let s_i = m_fe * k_i + &r * sigma_i; - Self { - r, - R: R.clone(), - s_i, - m: message.clone(), - y: pubkey.clone(), - } + Self { r, R: R.clone(), s_i, m: message.clone(), y: pubkey.clone() } } - pub fn output_signature(&self, s_vec: &[Scalar]) -> Result { + pub fn output_signature( + &self, + s_vec: &[Scalar], + ) -> Result { let mut s = s_vec.iter().fold(self.s_i.clone(), |acc, x| acc + x); let s_bn = s.to_bigint(); @@ -964,7 +989,11 @@ impl LocalSignature { } } -pub fn verify(sig: &SignatureRecid, y: &Point, message: &BigInt) -> Result<(), Error> { +pub fn verify( + sig: &SignatureRecid, + y: &Point, + message: &BigInt, +) -> Result<(), Error> { let b = sig.s.invert().unwrap(); let a = Scalar::::from(message); let u1 = a * &b; @@ -975,8 +1004,8 @@ pub fn verify(sig: &SignatureRecid, y: &Point, message: &BigInt) -> R let yu2 = y * &u2; // can be faster using shamir trick - if sig.r - == Scalar::::from( + if sig.r == + Scalar::::from( &(gu1 + yu2) .x_coord() .unwrap() diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs index 3e2861d5..40ab9274 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs @@ -1,16 +1,18 @@ //! High-level keygen protocol implementation -use std::fmt; -use std::mem::replace; -use std::time::Duration; - -use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; -use curv::elliptic::curves::secp256_k1::Secp256k1; -use round_based::containers::{ - push::{Push, PushExt}, - *, +use std::{fmt, mem::replace, time::Duration}; + +use curv::{ + cryptographic_primitives::proofs::sigma_dlog::DLogProof, + elliptic::curves::secp256_k1::Secp256k1, +}; +use round_based::{ + containers::{ + push::{Push, PushExt}, + *, + }, + IsCritical, Msg, StateMachine, }; -use round_based::{IsCritical, Msg, StateMachine}; use serde::{Deserialize, Serialize}; use sha2::Sha256; use thiserror::Error; @@ -25,13 +27,15 @@ use rounds::{Round0, Round1, Round2, Round3, Round4}; /// Keygen protocol state machine /// -/// Successfully completed keygen protocol produces [LocalKey] that can be used in further -/// [signing](super::sign) protocol. +/// Successfully completed keygen protocol produces [LocalKey] that can be used +/// in further [signing](super::sign) protocol. pub struct Keygen { round: R, - msgs1: Option>>, - msgs2: Option>>, + msgs1: + Option>>, + msgs2: + Option>>, msgs3: Option, Vec)>>>, msgs4: Option>>>, @@ -44,9 +48,9 @@ pub struct Keygen { impl Keygen { /// Constructs a party of keygen protocol /// - /// Takes party index `i` (in range `[1; n]`), threshold value `t`, and total number of - /// parties `n`. Party index identifies this party in the protocol, so it must be guaranteed - /// to be unique. + /// Takes party index `i` (in range `[1; n]`), threshold value `t`, and + /// total number of parties `n`. Party index identifies this party in + /// the protocol, so it must be guaranteed to be unique. /// /// Returns error if: /// * `n` is less than 2, returns [Error::TooFewParties] @@ -54,13 +58,13 @@ impl Keygen { /// * `i` is not in range `[1; n]`, returns [Error::InvalidPartyIndex] pub fn new(i: u16, t: u16, n: u16) -> Result { if n < 2 { - return Err(Error::TooFewParties); + return Err(Error::TooFewParties) } if t == 0 || t >= n { - return Err(Error::InvalidThreshold); + return Err(Error::InvalidThreshold) } if i == 0 || i > n { - return Err(Error::InvalidPartyIndex); + return Err(Error::InvalidPartyIndex) } let mut state = Self { round: R::Round0(Round0 { party_i: i, t, n }), @@ -83,16 +87,21 @@ impl Keygen { where F: FnMut(T) -> M + 'a, { - (&mut self.msgs_queue).gmap(move |m: Msg| m.map_body(|m| ProtocolMessage(f(m)))) + (&mut self.msgs_queue) + .gmap(move |m: Msg| m.map_body(|m| ProtocolMessage(f(m)))) } - /// Proceeds round state if it received enough messages and if it's cheap to compute or - /// `may_block == true` + /// Proceeds round state if it received enough messages and if it's cheap to + /// compute or `may_block == true` fn proceed_round(&mut self, may_block: bool) -> Result<()> { - let store1_wants_more = self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store3_wants_more = self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store4_wants_more = self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store1_wants_more = + self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = + self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store3_wants_more = + self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store4_wants_more = + self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); let next_state: R; let try_again: bool = match replace(&mut self.round, R::Gone) { @@ -102,13 +111,17 @@ impl Keygen { .map(R::Round1) .map_err(Error::ProceedRound)?; true - } + }, s @ R::Round0(_) => { next_state = s; false - } - R::Round1(round) if !store1_wants_more && (!round.is_expensive() || may_block) => { - let store = self.msgs1.take().ok_or(InternalError::StoreGone)?; + }, + R::Round1(round) + if !store1_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.msgs1.take().ok_or(InternalError::StoreGone)?; let msgs = store .finish() .map_err(InternalError::RetrieveRoundMessages)?; @@ -117,13 +130,17 @@ impl Keygen { .map(R::Round2) .map_err(Error::ProceedRound)?; true - } + }, s @ R::Round1(_) => { next_state = s; false - } - R::Round2(round) if !store2_wants_more && (!round.is_expensive() || may_block) => { - let store = self.msgs2.take().ok_or(InternalError::StoreGone)?; + }, + R::Round2(round) + if !store2_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.msgs2.take().ok_or(InternalError::StoreGone)?; let msgs = store .finish() .map_err(InternalError::RetrieveRoundMessages)?; @@ -132,13 +149,17 @@ impl Keygen { .map(R::Round3) .map_err(Error::ProceedRound)?; true - } + }, s @ R::Round2(_) => { next_state = s; false - } - R::Round3(round) if !store3_wants_more && (!round.is_expensive() || may_block) => { - let store = self.msgs3.take().ok_or(InternalError::StoreGone)?; + }, + R::Round3(round) + if !store3_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.msgs3.take().ok_or(InternalError::StoreGone)?; let msgs = store .finish() .map_err(InternalError::RetrieveRoundMessages)?; @@ -147,13 +168,17 @@ impl Keygen { .map(R::Round4) .map_err(Error::ProceedRound)?; true - } + }, s @ R::Round3(_) => { next_state = s; false - } - R::Round4(round) if !store4_wants_more && (!round.is_expensive() || may_block) => { - let store = self.msgs4.take().ok_or(InternalError::StoreGone)?; + }, + R::Round4(round) + if !store4_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.msgs4.take().ok_or(InternalError::StoreGone)?; let msgs = store .finish() .map_err(InternalError::RetrieveRoundMessages)?; @@ -162,15 +187,15 @@ impl Keygen { .map(R::Final) .map_err(Error::ProceedRound)?; true - } + }, s @ R::Round4(_) => { next_state = s; false - } + }, s @ R::Final(_) | s @ R::Gone => { next_state = s; false - } + }, }; self.round = next_state; @@ -192,13 +217,12 @@ impl StateMachine for Keygen { match msg.body { ProtocolMessage(M::Round1(m)) => { - let store = self - .msgs1 - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { + let store = self.msgs1.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { current_round, msg_round: 1, - })?; + }, + )?; store .push_msg(Msg { sender: msg.sender, @@ -207,15 +231,14 @@ impl StateMachine for Keygen { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - } + }, ProtocolMessage(M::Round2(m)) => { - let store = self - .msgs2 - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { + let store = self.msgs2.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { current_round, msg_round: 2, - })?; + }, + )?; store .push_msg(Msg { sender: msg.sender, @@ -224,15 +247,14 @@ impl StateMachine for Keygen { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - } + }, ProtocolMessage(M::Round3(m)) => { - let store = self - .msgs3 - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { + let store = self.msgs3.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { current_round, msg_round: 3, - })?; + }, + )?; store .push_msg(Msg { sender: msg.sender, @@ -241,15 +263,14 @@ impl StateMachine for Keygen { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - } + }, ProtocolMessage(M::Round4(m)) => { - let store = self - .msgs4 - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { + let store = self.msgs4.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { current_round, msg_round: 4, - })?; + }, + )?; store .push_msg(Msg { sender: msg.sender, @@ -258,7 +279,7 @@ impl StateMachine for Keygen { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - } + }, } } @@ -267,10 +288,14 @@ impl StateMachine for Keygen { } fn wants_to_proceed(&self) -> bool { - let store1_wants_more = self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store3_wants_more = self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store4_wants_more = self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store1_wants_more = + self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = + self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store3_wants_more = + self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store4_wants_more = + self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); match &self.round { R::Round0(_) => true, @@ -338,10 +363,14 @@ impl StateMachine for Keygen { impl super::traits::RoundBlame for Keygen { /// Returns number of unwilling parties and a vector of their party indexes. fn round_blame(&self) -> (u16, Vec) { - let store1_blame = self.msgs1.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store2_blame = self.msgs2.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store3_blame = self.msgs3.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store4_blame = self.msgs4.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store1_blame = + self.msgs1.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store2_blame = + self.msgs2.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store3_blame = + self.msgs3.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store4_blame = + self.msgs4.as_ref().map(|s| s.blame()).unwrap_or_default(); let default = (0, vec![]); match &self.round { @@ -367,19 +396,35 @@ impl fmt::Debug for Keygen { R::Gone => "[Gone]", }; let msgs1 = match self.msgs1.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), None => "[None]".into(), }; let msgs2 = match self.msgs2.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), None => "[None]".into(), }; let msgs3 = match self.msgs3.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), None => "[None]".into(), }; let msgs4 = match self.msgs4.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), None => "[None]".into(), }; write!( @@ -411,7 +456,8 @@ enum R { /// Protocol message which parties send on wire /// -/// Hides actual messages structure so it could be changed without breaking semver policy. +/// Hides actual messages structure so it could be changed without breaking +/// semver policy. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProtocolMessage(M); @@ -459,7 +505,8 @@ pub enum Error { /// Received message didn't pass pre-validation #[error("received message didn't pass pre-validation: {0}")] HandleMessage(#[source] StoreErr), - /// Received message which we didn't expect to receive now (e.g. message from previous round) + /// Received message which we didn't expect to receive now (e.g. message + /// from previous round) #[error( "didn't expect to receive message from round {msg_round} (being at round {current_round})" )] @@ -478,18 +525,20 @@ impl IsCritical for Error { fn is_critical(&self) -> bool { match self { Error::ProceedRound(e) => e.is_critical(), - // These errors are not critical, because they are handled by the protocol - // and don't indicate a bug in the library. + // These errors are not critical, because they are handled by the + // protocol and don't indicate a bug in the library. Error::HandleMessage(e) => !matches!( e, - StoreErr::MsgOverwrite | StoreErr::NotForMe | StoreErr::WantsMoreMessages + StoreErr::MsgOverwrite | + StoreErr::NotForMe | + StoreErr::WantsMoreMessages ), Error::ReceivedOutOfOrderMessage { .. } => false, - Error::DoublePickOutput - | Error::TooFewParties - | Error::InvalidThreshold - | Error::InvalidPartyIndex - | Error::InternalError(_) => true, + Error::DoublePickOutput | + Error::TooFewParties | + Error::InvalidThreshold | + Error::InvalidPartyIndex | + Error::InternalError(_) => true, } } } @@ -504,8 +553,9 @@ mod private { #[derive(Debug)] #[non_exhaustive] pub enum InternalError { - /// [Messages store](super::MessageStore) reported that it received all messages it wanted to receive, - /// but refused to return message container + /// [Messages store](super::MessageStore) reported that it received all + /// messages it wanted to receive, but refused to return + /// message container RetrieveRoundMessages(super::StoreErr), #[doc(hidden)] StoreGone, diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs index 74cb39cd..bb6ce65b 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs @@ -1,24 +1,31 @@ -use curv::arithmetic::Converter; -use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Curve, Point, Scalar}; -use curv::BigInt; +use curv::{ + arithmetic::Converter, + cryptographic_primitives::proofs::sigma_dlog::DLogProof, + elliptic::curves::{secp256_k1::Secp256k1, Curve, Point, Scalar}, + BigInt, +}; use sha2::Sha256; use serde::{Deserialize, Serialize}; use thiserror::Error; -use paillier::Paillier; -use paillier::{Decrypt, Encrypt}; -use paillier::{EncryptionKey, RawCiphertext, RawPlaintext}; -use round_based::containers::push::Push; -use round_based::containers::{self, BroadcastMsgs, MessageStore, P2PMsgs, P2PMsgsStore, Store}; -use round_based::{IsCritical, Msg}; +use paillier::{ + Decrypt, Encrypt, EncryptionKey, Paillier, RawCiphertext, RawPlaintext, +}; +use round_based::{ + containers::{ + self, push::Push, BroadcastMsgs, MessageStore, P2PMsgs, P2PMsgsStore, + Store, + }, + IsCritical, Msg, +}; use zk_paillier::zkproofs::DLogStatement; -use crate::protocols::multi_party_ecdsa::gg_2020::{VerifiableSS, party_i::{ - KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Keys, -},}; -use crate::protocols::multi_party_ecdsa::gg_2020::{self, ErrorType}; +use crate::protocols::multi_party_ecdsa::gg_2020::{ + self, + party_i::{KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Keys}, + ErrorType, VerifiableSS, +}; pub struct Round0 { pub party_i: u16, @@ -32,8 +39,9 @@ impl Round0 { O: Push>, { let party_keys = Keys::create(self.party_i as usize); - let (bc1, decom1) = - party_keys.phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2(); + let (bc1, decom1) = party_keys + .phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2( + ); output.push(Msg { sender: self.party_i, @@ -90,7 +98,10 @@ impl Round1 { pub fn is_expensive(&self) -> bool { false } - pub fn expects_messages(i: u16, n: u16) -> Store> { + pub fn expects_messages( + i: u16, + n: u16, + ) -> Store> { containers::BroadcastMsgsStore::new(i, n) } } @@ -131,12 +142,14 @@ impl Round2 { for (i, share) in vss_result.1.iter().enumerate() { if i + 1 == usize::from(self.party_i) { - continue; + continue } let enc_key_for_recipient = &self.received_comm[i].e; - let encrypted_share = - Paillier::encrypt(enc_key_for_recipient, RawPlaintext::from(share.to_bigint())); + let encrypted_share = Paillier::encrypt( + enc_key_for_recipient, + RawPlaintext::from(share.to_bigint()), + ); output.push(Msg { sender: self.party_i, receiver: Some(i as u16 + 1), @@ -161,7 +174,10 @@ impl Round2 { pub fn is_expensive(&self) -> bool { true } - pub fn expects_messages(i: u16, n: u16) -> Store> { + pub fn expects_messages( + i: u16, + n: u16, + ) -> Store> { containers::BroadcastMsgsStore::new(i, n) } } @@ -199,7 +215,8 @@ impl Round3 { for (i, (vss, encrypted_share)) in encrypted_input { let v = BigInt::from_bytes(&encrypted_share); let c = RawCiphertext::from(v); - let raw_share: RawPlaintext<'_> = Paillier::decrypt(&self.keys.dk, c); + let raw_share: RawPlaintext<'_> = + Paillier::decrypt(&self.keys.dk, c); let share = Scalar::from_bigint(&raw_share.0); let _ = decrypted_input.push_msg(Msg { sender: i, @@ -247,7 +264,10 @@ impl Round3 { pub fn is_expensive(&self) -> bool { true } - pub fn expects_messages(i: u16, n: u16) -> Store, Vec)>> { + pub fn expects_messages( + i: u16, + n: u16, + ) -> Store, Vec)>> { containers::P2PMsgsStore::new(i, n) } } @@ -274,7 +294,8 @@ impl Round4 { threshold: self.t, share_count: self.n, }; - let dlog_proofs = input.into_vec_including_me(self.own_dlog_proof.clone()); + let dlog_proofs = + input.into_vec_including_me(self.own_dlog_proof.clone()); Keys::verify_dlog_proofs_check_against_vss( ¶ms, @@ -320,12 +341,16 @@ impl Round4 { pub fn is_expensive(&self) -> bool { true } - pub fn expects_messages(i: u16, n: u16) -> Store>> { + pub fn expects_messages( + i: u16, + n: u16, + ) -> Store>> { containers::BroadcastMsgsStore::new(i, n) } } -/// Local secret obtained by party after [keygen](super::Keygen) protocol is completed +/// Local secret obtained by party after [keygen](super::Keygen) protocol is +/// completed #[derive(Serialize, Deserialize, Clone, Debug)] pub struct LocalKey { pub paillier_dk: paillier::DecryptionKey, @@ -353,8 +378,8 @@ type Result = std::result::Result; /// Proceeding protocol error /// -/// Subset of [keygen errors](enum@super::Error) that can occur at protocol proceeding (i.e. after -/// every message was received and pre-validated). +/// Subset of [keygen errors](enum@super::Error) that can occur at protocol +/// proceeding (i.e. after every message was received and pre-validated). #[derive(Debug, Error)] pub enum ProceedError { #[error("round 2: verify commitments: {0:?}")] diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign.rs index e44b80d0..03d9285c 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign.rs @@ -1,31 +1,35 @@ //! # High-level threshold signing protocol implementation //! -//! Key feature of GG20 protocol is one-round online signing, meaning that every party needs to -//! broadcast just a single message to sign a data. However, it still requires completing an offline -//! computation for fixed set of parties +//! Key feature of GG20 protocol is one-round online signing, meaning that every +//! party needs to broadcast just a single message to sign a data. However, it +//! still requires completing an offline computation for fixed set of parties //! //! ## How to get things work //! -//! First of all, parties need to carry out distributed key generation protocol (see [keygen module]). -//! After DKG is successfully completed, it outputs [LocalKey] — a party local secret share. -//! Then you fix a set of parties who will participate in threshold signing, and they run -//! [OfflineStage] protocol. `OfflineStage` implements [StateMachine] and can be executed in the same -//! way as [Keygen]. `OfflineStage` outputs a [CompletedOfflineStage]. [SignManual] takes a -//! `CompletedOfflineStage` and allows you to perform one-round signing. It doesn't implement -//! `StateMachine`, but rather provides methods to construct messages and final signature manually -//! (refer to [SignManual] documentation to see how to use it). +//! First of all, parties need to carry out distributed key generation protocol +//! (see [keygen module]). After DKG is successfully completed, it outputs +//! [LocalKey] — a party local secret share. Then you fix a set of parties who +//! will participate in threshold signing, and they run [OfflineStage] protocol. +//! `OfflineStage` implements [StateMachine] and can be executed in the same way +//! as [Keygen]. `OfflineStage` outputs a [CompletedOfflineStage]. [SignManual] +//! takes a `CompletedOfflineStage` and allows you to perform one-round signing. +//! It doesn't implement `StateMachine`, but rather provides methods to +//! construct messages and final signature manually (refer to [SignManual] +//! documentation to see how to use it). //! //! [keygen module]: super::keygen //! [Keygen]: super::keygen::Keygen //! [LocalKey]: super::keygen::LocalKey //! [StateMachine]: round_based::StateMachine -use std::convert::TryFrom; -use std::mem::replace; -use std::time::Duration; +use std::{convert::TryFrom, mem::replace, time::Duration}; -use round_based::containers::{push::Push, BroadcastMsgs, MessageStore, P2PMsgs, Store, StoreErr}; -use round_based::{IsCritical, Msg, StateMachine}; +use round_based::{ + containers::{ + push::Push, BroadcastMsgs, MessageStore, P2PMsgs, Store, StoreErr, + }, + IsCritical, Msg, StateMachine, +}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -33,8 +37,10 @@ use crate::utilities::mta::MessageA; use crate::protocols::multi_party_ecdsa::gg_2020 as gg20; use curv::elliptic::curves::secp256_k1::Secp256k1; -use gg20::party_i::{SignBroadcastPhase1, SignDecommitPhase1, SignatureRecid}; -use gg20::state_machine::keygen::LocalKey; +use gg20::{ + party_i::{SignBroadcastPhase1, SignDecommitPhase1, SignatureRecid}, + state_machine::keygen::LocalKey, +}; mod fmt; pub mod rounds; @@ -42,12 +48,14 @@ pub mod rounds; use crate::utilities::zk_pdl_with_slack::PDLwSlackProof; use curv::BigInt; use rounds::*; -pub use rounds::{CompletedOfflineStage, Error as ProceedError, PartialSignature}; +pub use rounds::{ + CompletedOfflineStage, Error as ProceedError, PartialSignature, +}; /// Offline Stage of GG20 signing /// -/// Successfully carried out Offline Stage will produce [CompletedOfflineStage] that can -/// be used for one-round signing multiple times. +/// Successfully carried out Offline Stage will produce [CompletedOfflineStage] +/// that can be used for one-round signing multiple times. pub struct OfflineStage { round: OfflineR, @@ -67,25 +75,30 @@ pub struct OfflineStage { impl OfflineStage { /// Construct a party of offline stage of threshold signing protocol /// - /// Once offline stage is finished, parties can do one-round threshold signing (i.e. they only - /// need to exchange a single set of messages). + /// Once offline stage is finished, parties can do one-round threshold + /// signing (i.e. they only need to exchange a single set of messages). /// - /// Takes party index `i` (in range `[1; n]`), list `s_l` of parties' indexes from keygen protocol - /// (`s_l[i]` must be an index of party `i` that was used by this party in keygen protocol), and - /// party local secret share `local_key`. + /// Takes party index `i` (in range `[1; n]`), list `s_l` of parties' + /// indexes from keygen protocol (`s_l[i]` must be an index of party `i` + /// that was used by this party in keygen protocol), and party local + /// secret share `local_key`. /// /// Returns error if given arguments are contradicting. - pub fn new(i: u16, s_l: Vec, local_key: LocalKey) -> Result { + pub fn new( + i: u16, + s_l: Vec, + local_key: LocalKey, + ) -> Result { if s_l.len() < 2 { - return Err(Error::TooFewParties); + return Err(Error::TooFewParties) } if i == 0 || usize::from(i) > s_l.len() { - return Err(Error::InvalidPartyIndex); + return Err(Error::InvalidPartyIndex) } let keygen_n = local_key.n; if s_l.iter().any(|&i| i == 0 || i > keygen_n) { - return Err(Error::InvalidSl); + return Err(Error::InvalidSl) } { // Check if s_l has duplicates @@ -95,11 +108,12 @@ impl OfflineStage { s_l_sorted_deduped.dedup(); if s_l_sorted != s_l_sorted_deduped { - return Err(Error::InvalidSl); + return Err(Error::InvalidSl) } } - let n = u16::try_from(s_l.len()).map_err(|_| Error::TooManyParties { n: s_l.len() })?; + let n = u16::try_from(s_l.len()) + .map_err(|_| Error::TooManyParties { n: s_l.len() })?; Ok(Self { round: OfflineR::R0(Round0 { i, s_l, local_key }), @@ -124,12 +138,18 @@ impl OfflineStage { // } fn proceed_round(&mut self, may_block: bool) -> Result<()> { - let store1_wants_more = self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store3_wants_more = self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store4_wants_more = self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store5_wants_more = self.msgs5.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store6_wants_more = self.msgs6.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store1_wants_more = + self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = + self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store3_wants_more = + self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store4_wants_more = + self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store5_wants_more = + self.msgs5.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store6_wants_more = + self.msgs6.as_ref().map(|s| s.wants_more()).unwrap_or(false); let next_state: OfflineR; let try_again: bool = match replace(&mut self.round, OfflineR::Gone) { @@ -139,13 +159,17 @@ impl OfflineStage { .map(OfflineR::R1) .map_err(Error::ProceedRound)?; true - } + }, s @ OfflineR::R0(_) => { next_state = s; false - } - OfflineR::R1(round) if !store1_wants_more && (!round.is_expensive() || may_block) => { - let store = self.msgs1.take().ok_or(InternalError::StoreGone)?; + }, + OfflineR::R1(round) + if !store1_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.msgs1.take().ok_or(InternalError::StoreGone)?; let msgs = store .finish() .map_err(InternalError::RetrieveMessagesFromStore)?; @@ -154,13 +178,17 @@ impl OfflineStage { .map(OfflineR::R2) .map_err(Error::ProceedRound)?; true - } + }, s @ OfflineR::R1(_) => { next_state = s; false - } - OfflineR::R2(round) if !store2_wants_more && (!round.is_expensive() || may_block) => { - let store = self.msgs2.take().ok_or(InternalError::StoreGone)?; + }, + OfflineR::R2(round) + if !store2_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.msgs2.take().ok_or(InternalError::StoreGone)?; let msgs = store .finish() .map_err(InternalError::RetrieveMessagesFromStore)?; @@ -169,13 +197,17 @@ impl OfflineStage { .map(OfflineR::R3) .map_err(Error::ProceedRound)?; true - } + }, s @ OfflineR::R2(_) => { next_state = s; false - } - OfflineR::R3(round) if !store3_wants_more && (!round.is_expensive() || may_block) => { - let store = self.msgs3.take().ok_or(InternalError::StoreGone)?; + }, + OfflineR::R3(round) + if !store3_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.msgs3.take().ok_or(InternalError::StoreGone)?; let msgs = store .finish() .map_err(InternalError::RetrieveMessagesFromStore)?; @@ -184,13 +216,17 @@ impl OfflineStage { .map(OfflineR::R4) .map_err(Error::ProceedRound)?; true - } + }, s @ OfflineR::R3(_) => { next_state = s; false - } - OfflineR::R4(round) if !store4_wants_more && (!round.is_expensive() || may_block) => { - let store = self.msgs4.take().ok_or(InternalError::StoreGone)?; + }, + OfflineR::R4(round) + if !store4_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.msgs4.take().ok_or(InternalError::StoreGone)?; let msgs = store .finish() .map_err(InternalError::RetrieveMessagesFromStore)?; @@ -199,13 +235,17 @@ impl OfflineStage { .map(OfflineR::R5) .map_err(Error::ProceedRound)?; false - } + }, s @ OfflineR::R4(_) => { next_state = s; false - } - OfflineR::R5(round) if !store5_wants_more && (!round.is_expensive() || may_block) => { - let store = self.msgs5.take().ok_or(InternalError::StoreGone)?; + }, + OfflineR::R5(round) + if !store5_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.msgs5.take().ok_or(InternalError::StoreGone)?; let msgs = store .finish() .map_err(InternalError::RetrieveMessagesFromStore)?; @@ -214,13 +254,17 @@ impl OfflineStage { .map(OfflineR::R6) .map_err(Error::ProceedRound)?; false - } + }, s @ OfflineR::R5(_) => { next_state = s; false - } - OfflineR::R6(round) if !store6_wants_more && (!round.is_expensive() || may_block) => { - let store = self.msgs6.take().ok_or(InternalError::StoreGone)?; + }, + OfflineR::R6(round) + if !store6_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.msgs6.take().ok_or(InternalError::StoreGone)?; let msgs = store .finish() .map_err(InternalError::RetrieveMessagesFromStore)?; @@ -229,15 +273,15 @@ impl OfflineStage { .map(OfflineR::Finished) .map_err(Error::ProceedRound)?; false - } + }, s @ OfflineR::R6(_) => { next_state = s; false - } + }, s @ OfflineR::Finished(_) | s @ OfflineR::Gone => { next_state = s; false - } + }, }; self.round = next_state; @@ -254,18 +298,20 @@ impl StateMachine for OfflineStage { type Err = Error; type Output = CompletedOfflineStage; - fn handle_incoming(&mut self, msg: Msg) -> Result<(), Self::Err> { + fn handle_incoming( + &mut self, + msg: Msg, + ) -> Result<(), Self::Err> { let current_round = self.current_round(); match msg.body { OfflineProtocolMessage(OfflineM::M1(m)) => { - let store = self - .msgs1 - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { + let store = self.msgs1.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { current_round, msg_round: 1, - })?; + }, + )?; store .push_msg(Msg { sender: msg.sender, @@ -273,15 +319,14 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - } + }, OfflineProtocolMessage(OfflineM::M2(m)) => { - let store = self - .msgs2 - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { + let store = self.msgs2.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { current_round, msg_round: 2, - })?; + }, + )?; store .push_msg(Msg { sender: msg.sender, @@ -289,15 +334,14 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - } + }, OfflineProtocolMessage(OfflineM::M3(m)) => { - let store = self - .msgs3 - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { + let store = self.msgs3.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { current_round, msg_round: 2, - })?; + }, + )?; store .push_msg(Msg { sender: msg.sender, @@ -305,15 +349,14 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - } + }, OfflineProtocolMessage(OfflineM::M4(m)) => { - let store = self - .msgs4 - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { + let store = self.msgs4.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { current_round, msg_round: 2, - })?; + }, + )?; store .push_msg(Msg { sender: msg.sender, @@ -321,15 +364,14 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - } + }, OfflineProtocolMessage(OfflineM::M5(m)) => { - let store = self - .msgs5 - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { + let store = self.msgs5.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { current_round, msg_round: 2, - })?; + }, + )?; store .push_msg(Msg { sender: msg.sender, @@ -337,15 +379,14 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - } + }, OfflineProtocolMessage(OfflineM::M6(m)) => { - let store = self - .msgs6 - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { + let store = self.msgs6.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { current_round, msg_round: 2, - })?; + }, + )?; store .push_msg(Msg { sender: msg.sender, @@ -353,7 +394,7 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - } + }, } self.proceed_round(false) } @@ -363,12 +404,18 @@ impl StateMachine for OfflineStage { } fn wants_to_proceed(&self) -> bool { - let store1_wants_more = self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store3_wants_more = self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store4_wants_more = self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store5_wants_more = self.msgs5.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store6_wants_more = self.msgs6.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store1_wants_more = + self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = + self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store3_wants_more = + self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store4_wants_more = + self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store5_wants_more = + self.msgs5.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store6_wants_more = + self.msgs6.as_ref().map(|s| s.wants_more()).unwrap_or(false); match &self.round { OfflineR::R0(_) => true, @@ -438,14 +485,21 @@ impl StateMachine for OfflineStage { } impl super::traits::RoundBlame for OfflineStage { - /// RoundBlame returns number of unwilling parties and a vector of their party indexes. + /// RoundBlame returns number of unwilling parties and a vector of their + /// party indexes. fn round_blame(&self) -> (u16, Vec) { - let store1_blame = self.msgs1.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store2_blame = self.msgs2.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store3_blame = self.msgs3.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store4_blame = self.msgs4.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store5_blame = self.msgs5.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store6_blame = self.msgs6.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store1_blame = + self.msgs1.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store2_blame = + self.msgs2.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store3_blame = + self.msgs3.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store4_blame = + self.msgs4.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store5_blame = + self.msgs5.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store6_blame = + self.msgs6.as_ref().map(|s| s.blame()).unwrap_or_default(); let default = (0, vec![]); match &self.round { @@ -536,15 +590,17 @@ pub enum Error { /// Too few parties (`n < 2`) #[error("at least 2 parties are required for signing")] TooFewParties, - /// Too many parties. `n` must fit into `u16`, so only `n < u16::MAX` values are supported. + /// Too many parties. `n` must fit into `u16`, so only `n < u16::MAX` + /// values are supported. #[error("too many parties: n={n}, n must be less than 2^16")] TooManyParties { n: usize }, /// Party index `i` is not in range `[1; n]` #[error("party index is not in range [1; n]")] InvalidPartyIndex, - /// List `s_l` is invalid. Either it contains duplicates (`exist i j. i != j && s_l[i] = s_l[j]`), - /// or contains index that is not in the range `[1; keygen_n]`, `keygen_n` — number of parties - /// participated in DKG (`exist i. s_l[i] = 0 || s_l[i] > keygen_n`). + /// List `s_l` is invalid. Either it contains duplicates (`exist i j. i != + /// j && s_l[i] = s_l[j]`), or contains index that is not in the range + /// `[1; keygen_n]`, `keygen_n` — number of parties participated in DKG + /// (`exist i. s_l[i] = 0 || s_l[i] > keygen_n`). #[error("invalid s_l")] InvalidSl, @@ -552,7 +608,8 @@ pub enum Error { #[error("proceeding round: {0}")] ProceedRound(rounds::Error), - /// Received message which we didn't expect to receive now (e.g. message from previous round) + /// Received message which we didn't expect to receive now (e.g. message + /// from previous round) #[error( "didn't expect to receive message from round {msg_round} (being at round {current_round})" )] @@ -604,8 +661,8 @@ impl IsCritical for Error { /// Manual GG20 signing /// -/// After you completed [OfflineStage] and got [CompletedOfflineStage], parties can perform signing -/// simply by broadcasting a single message. +/// After you completed [OfflineStage] and got [CompletedOfflineStage], parties +/// can perform signing simply by broadcasting a single message. /// /// ## Example /// ```no_run @@ -649,12 +706,13 @@ impl SignManual { .map_err(SignError::LocalSigning) } - /// `sigs` must not include partial signature produced by local party (only partial signatures produced - /// by other parties) - pub fn complete(self, sigs: &[PartialSignature]) -> Result { - self.state - .proceed_manual(sigs) - .map_err(SignError::CompleteSigning) + /// `sigs` must not include partial signature produced by local party (only + /// partial signatures produced by other parties) + pub fn complete( + self, + sigs: &[PartialSignature], + ) -> Result { + self.state.proceed_manual(sigs).map_err(SignError::CompleteSigning) } } @@ -668,14 +726,15 @@ pub enum SignError { #[cfg(test)] mod test { - use curv::arithmetic::Converter; - use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; + use curv::{ + arithmetic::Converter, + cryptographic_primitives::hashing::{Digest, DigestExt}, + }; use round_based::dev::Simulation; use sha2::Sha256; use super::*; - use gg20::party_i::verify; - use gg20::state_machine::keygen::test::simulate_keygen; + use gg20::{party_i::verify, state_machine::keygen::test::simulate_keygen}; fn simulate_offline_stage( local_keys: Vec>, @@ -714,7 +773,8 @@ mod test { .map(|o| SignManual::new(message.clone(), o.clone())) .collect::, _>>() .unwrap(); - let (parties, local_sigs): (Vec<_>, Vec<_>) = parties.into_iter().unzip(); + let (parties, local_sigs): (Vec<_>, Vec<_>) = + parties.into_iter().unzip(); // parties.remove(0).complete(&local_sigs[1..]).unwrap(); let local_sigs_except = |i: usize| { let mut v = vec![]; diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/fmt.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/fmt.rs index c8bf31db..ba6e3e4c 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/fmt.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/fmt.rs @@ -43,9 +43,7 @@ impl From<&super::OfflineStage> for OfflineStageProgress { round4_msgs: ReceivedMessages::from_broadcast(state.msgs4.as_ref()), round5_msgs: ReceivedMessages::from_broadcast(state.msgs5.as_ref()), - msgs_queue: OutgoingMessages { - len: state.msgs_queue.0.len(), - }, + msgs_queue: OutgoingMessages { len: state.msgs_queue.0.len() }, } } } @@ -114,7 +112,7 @@ impl fmt::Debug for ReceivedMessages { container.total - container.waiting_for.len(), container.total ) - } + }, None => write!(f, "[gone]"), } } diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs index 79cf80b3..9b1d467c 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs @@ -1,30 +1,39 @@ #![allow(non_snake_case)] -use std::convert::TryFrom; -use std::iter; +use std::{convert::TryFrom, iter}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; +use curv::{ + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, +}; use sha2::Sha256; -use round_based::containers::push::Push; -use round_based::containers::{self, BroadcastMsgs, P2PMsgs, Store}; -use round_based::Msg; +use round_based::{ + containers::{self, push::Push, BroadcastMsgs, P2PMsgs, Store}, + Msg, +}; use crate::utilities::mta::{MessageA, MessageB}; -use crate::protocols::multi_party_ecdsa::gg_2020 as gg20; -use crate::utilities::zk_pdl_with_slack::PDLwSlackProof; -use curv::cryptographic_primitives::proofs::sigma_correct_homomorphic_elgamal_enc::HomoELGamalProof; -use curv::cryptographic_primitives::proofs::sigma_valid_pedersen::PedersenProof; -use gg20::party_i::{ - LocalSignature, SignBroadcastPhase1, SignDecommitPhase1, SignKeys, SignatureRecid, +use crate::{ + protocols::multi_party_ecdsa::gg_2020 as gg20, + utilities::zk_pdl_with_slack::PDLwSlackProof, +}; +use curv::cryptographic_primitives::proofs::{ + sigma_correct_homomorphic_elgamal_enc::HomoELGamalProof, + sigma_valid_pedersen::PedersenProof, +}; +use gg20::{ + party_i::{ + LocalSignature, SignBroadcastPhase1, SignDecommitPhase1, SignKeys, + SignatureRecid, + }, + state_machine::keygen::LocalKey, + ErrorType, }; -use gg20::state_machine::keygen::LocalKey; -use gg20::ErrorType; type Result = std::result::Result; @@ -51,12 +60,14 @@ pub struct HEGProof(pub HomoELGamalProof); pub struct Round0 { /// Index of this party /// - /// Must be in range `[0; n)` where `n` is number of parties involved in signing. + /// Must be in range `[0; n)` where `n` is number of parties involved in + /// signing. pub i: u16, /// List of parties' indexes from keygen protocol /// - /// I.e. `s_l[i]` must be an index of party `i` that was used by this party in keygen protocol. + /// I.e. `s_l[i]` must be an index of party `i` that was used by this party + /// in keygen protocol. // s_l.len()` equals to `n` (number of parties involved in signing) pub s_l: Vec, @@ -73,16 +84,18 @@ impl Round0 { &self.local_key.keys_linear.x_i, &self.local_key.vss_scheme.clone(), usize::from(self.s_l[usize::from(self.i - 1)]) - 1, - &self - .s_l - .iter() - .map(|&i| usize::from(i) - 1) - .collect::>(), + &self.s_l.iter().map(|&i| usize::from(i) - 1).collect::>(), ); let (bc1, decom1) = sign_keys.phase1_broadcast(); - let party_ek = self.local_key.paillier_key_vec[usize::from(self.local_key.i - 1)].clone(); - let m_a = MessageA::a(&sign_keys.k_i, &party_ek, &self.local_key.h1_h2_n_tilde_vec); + let party_ek = self.local_key.paillier_key_vec + [usize::from(self.local_key.i - 1)] + .clone(); + let m_a = MessageA::a( + &sign_keys.k_i, + &party_ek, + &self.local_key.h1_h2_n_tilde_vec, + ); output.push(Msg { sender: self.i, @@ -128,7 +141,10 @@ impl Round1 { O: Push>, { let (m_a_vec, bc_vec): (Vec<_>, Vec<_>) = input - .into_vec_including_me((self.m_a.0.clone(), self.phase1_com.clone())) + .into_vec_including_me(( + self.m_a.0.clone(), + self.phase1_com.clone(), + )) .into_iter() .unzip(); @@ -138,29 +154,26 @@ impl Round1 { let mut ni_vec = Vec::new(); let ttag = self.s_l.len(); - let l_s: Vec<_> = self - .s_l - .iter() - .cloned() - .map(|i| usize::from(i) - 1) - .collect(); + let l_s: Vec<_> = + self.s_l.iter().cloned().map(|i| usize::from(i) - 1).collect(); let i = usize::from(self.i - 1); for j in 0..ttag - 1 { let ind = if j < i { j } else { j + 1 }; - let (m_b_gamma, beta_gamma, _beta_randomness, _beta_tag) = MessageB::b( - &self.sign_keys.gamma_i, - &self.local_key.paillier_key_vec[l_s[ind]], - m_a_vec[ind].clone(), - &self.local_key.h1_h2_n_tilde_vec, - ) - .map_err(|e| { - Error::Round1(ErrorType { - error_type: e.to_string(), - bad_actors: vec![ind], - data: vec![], - }) - })?; + let (m_b_gamma, beta_gamma, _beta_randomness, _beta_tag) = + MessageB::b( + &self.sign_keys.gamma_i, + &self.local_key.paillier_key_vec[l_s[ind]], + m_a_vec[ind].clone(), + &self.local_key.h1_h2_n_tilde_vec, + ) + .map_err(|e| { + Error::Round1(ErrorType { + error_type: e.to_string(), + bad_actors: vec![ind], + data: vec![], + }) + })?; let (m_b_w, beta_wi, _, _) = MessageB::b( &self.sign_keys.w_i, @@ -185,7 +198,9 @@ impl Round1 { let party_indices = (1..=self.s_l.len()) .map(|j| u16::try_from(j).unwrap()) .filter(|&j| j != self.i); - for ((j, gamma_i), w_i) in party_indices.zip(m_b_gamma_vec).zip(m_b_w_vec) { + for ((j, gamma_i), w_i) in + party_indices.zip(m_b_gamma_vec).zip(m_b_w_vec) + { output.push(Msg { sender: self.i, receiver: Some(j), @@ -233,7 +248,11 @@ pub struct Round2 { } impl Round2 { - pub fn proceed(self, input_p2p: P2PMsgs<(GammaI, WI)>, mut output: O) -> Result + pub fn proceed( + self, + input_p2p: P2PMsgs<(GammaI, WI)>, + mut output: O, + ) -> Result where O: Push>, // TODO: unify TI and TIProof { @@ -248,12 +267,8 @@ impl Round2 { let ttag = self.s_l.len(); let index = usize::from(self.i) - 1; - let l_s: Vec<_> = self - .s_l - .iter() - .cloned() - .map(|i| usize::from(i) - 1) - .collect(); + let l_s: Vec<_> = + self.s_l.iter().cloned().map(|i| usize::from(i) - 1).collect(); let g_w_vec = SignKeys::g_w_vec( &self.local_key.pk_vec[..], &l_s[..], @@ -264,7 +279,10 @@ impl Round2 { let m_b = m_b_gamma_s[j].clone(); let alpha_ij_gamma = m_b - .verify_proofs_get_alpha(&self.local_key.paillier_dk, &self.sign_keys.k_i) + .verify_proofs_get_alpha( + &self.local_key.paillier_dk, + &self.sign_keys.k_i, + ) .map_err(|e| { Error::Round3(ErrorType { error_type: e.to_string(), @@ -274,7 +292,10 @@ impl Round2 { })?; let m_b = m_b_w_s[j].clone(); let alpha_ij_wi = m_b - .verify_proofs_get_alpha(&self.local_key.paillier_dk, &self.sign_keys.k_i) + .verify_proofs_get_alpha( + &self.local_key.paillier_dk, + &self.sign_keys.k_i, + ) .map_err(|e| { Error::Round3(ErrorType { error_type: e.to_string(), @@ -406,7 +427,10 @@ impl Round3 { }) } - pub fn expects_messages(i: u16, n: u16) -> Store> { + pub fn expects_messages( + i: u16, + n: u16, + ) -> Store> { containers::BroadcastMsgsStore::new(i, n) } @@ -441,10 +465,12 @@ impl Round4 { where O: Push)>>, { - let decom_vec: Vec<_> = decommit_round1.into_vec_including_me(self.phase1_decom.clone()); + let decom_vec: Vec<_> = + decommit_round1.into_vec_including_me(self.phase1_decom.clone()); let ttag = self.s_l.len(); - let b_proof_vec: Vec<_> = (0..ttag - 1).map(|i| &self.mb_gamma_s[i].b_proof).collect(); + let b_proof_vec: Vec<_> = + (0..ttag - 1).map(|i| &self.mb_gamma_s[i].b_proof).collect(); let R = SignKeys::phase4( &self.delta_inv, &b_proof_vec[..], @@ -458,12 +484,8 @@ impl Round4 { // each party sends first message to all other parties let mut phase5_proofs_vec = Vec::new(); - let l_s: Vec<_> = self - .s_l - .iter() - .cloned() - .map(|i| usize::from(i) - 1) - .collect(); + let l_s: Vec<_> = + self.s_l.iter().cloned().map(|i| usize::from(i) - 1).collect(); let index = usize::from(self.i - 1); for j in 0..ttag - 1 { let ind = if j < index { j } else { j + 1 }; @@ -502,7 +524,10 @@ impl Round4 { }) } - pub fn expects_messages(i: u16, n: u16) -> Store> { + pub fn expects_messages( + i: u16, + n: u16, + ) -> Store> { containers::BroadcastMsgsStore::new(i, n) } @@ -541,12 +566,8 @@ impl Round5 { .map(|(r_dash, pdl_proof)| (r_dash.0, pdl_proof)) .unzip(); - let l_s: Vec<_> = self - .s_l - .iter() - .cloned() - .map(|i| usize::from(i) - 1) - .collect(); + let l_s: Vec<_> = + self.s_l.iter().cloned().map(|i| usize::from(i) - 1).collect(); let ttag = self.s_l.len(); for i in 0..ttag { LocalSignature::phase5_verify_pdl( @@ -569,12 +590,13 @@ impl Round5 { }) })?; - let (S_i, homo_elgamal_proof) = LocalSignature::phase6_compute_S_i_and_proof_of_consistency( - &self.R, - &self.t_i, - &self.sigma_i, - &self.l_i, - ); + let (S_i, homo_elgamal_proof) = + LocalSignature::phase6_compute_S_i_and_proof_of_consistency( + &self.R, + &self.t_i, + &self.sigma_i, + &self.l_i, + ); output.push(Msg { sender: self.i, @@ -597,7 +619,10 @@ impl Round5 { }) } - pub fn expects_messages(i: u16, n: u16) -> Store)>> { + pub fn expects_messages( + i: u16, + n: u16, + ) -> Store)>> { containers::BroadcastMsgsStore::new(i, n) } @@ -620,7 +645,10 @@ impl Round6 { input: BroadcastMsgs<(SI, HEGProof)>, ) -> Result { let (S_i_vec, hegp_vec): (Vec<_>, Vec<_>) = input - .into_vec_including_me((SI(self.S_i), HEGProof(self.homo_elgamal_proof))) + .into_vec_including_me(( + SI(self.S_i), + HEGProof(self.homo_elgamal_proof), + )) .into_iter() .map(|(s_i, hegp_i)| (s_i.0, hegp_i.0)) .unzip(); @@ -635,13 +663,19 @@ impl Round6 { &self.protocol_output.t_vec, ) .map_err(Error::Round6VerifyProof)?; - LocalSignature::phase6_check_S_i_sum(&self.protocol_output.local_key.y_sum_s, &S_i_vec) - .map_err(Error::Round6CheckSig)?; + LocalSignature::phase6_check_S_i_sum( + &self.protocol_output.local_key.y_sum_s, + &S_i_vec, + ) + .map_err(Error::Round6CheckSig)?; Ok(self.protocol_output) } - pub fn expects_messages(i: u16, n: u16) -> Store> { + pub fn expects_messages( + i: u16, + n: u16, + ) -> Store> { containers::BroadcastMsgsStore::new(i, n) } @@ -691,11 +725,12 @@ impl Round7 { Ok((Self { local_signature }, partial)) } - pub fn proceed_manual(self, sigs: &[PartialSignature]) -> Result { + pub fn proceed_manual( + self, + sigs: &[PartialSignature], + ) -> Result { let sigs = sigs.iter().map(|s_i| s_i.0.clone()).collect::>(); - self.local_signature - .output_signature(&sigs) - .map_err(Error::Round7) + self.local_signature.output_signature(&sigs).map_err(Error::Round7) } } diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/traits.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/traits.rs index 2377a8ce..af24de1c 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/traits.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/traits.rs @@ -1,7 +1,8 @@ pub trait RoundBlame { /// Retrieves a list of uncorporative parties /// - /// Returns a numbers of messages yet to recieve and list of parties to send messages for the current round + /// Returns a numbers of messages yet to recieve and list of parties to send + /// messages for the current round fn round_blame(&self) -> (u16, Vec); } diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs index 26eeb333..deb11b7b 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs @@ -16,23 +16,32 @@ use crate::protocols::multi_party_ecdsa::gg_2020::VerifiableSS; @license GPL-3.0+ */ -use crate::protocols::multi_party_ecdsa::gg_2020::blame::{ - GlobalStatePhase5, GlobalStatePhase6, GlobalStatePhase7, LocalStatePhase5, LocalStatePhase6, +use crate::{ + protocols::multi_party_ecdsa::gg_2020::{ + blame::{ + GlobalStatePhase5, GlobalStatePhase6, GlobalStatePhase7, + LocalStatePhase5, LocalStatePhase6, + }, + party_i::{ + KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Keys, + LocalSignature, Parameters, SharedKeys, SignKeys, SignatureRecid, + }, + }, + utilities::mta::{MessageA, MessageB}, }; -use crate::protocols::multi_party_ecdsa::gg_2020::party_i::SignatureRecid; -use crate::protocols::multi_party_ecdsa::gg_2020::party_i::{ - KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Keys, LocalSignature, Parameters, SharedKeys, - SignKeys, -}; -use crate::utilities::mta::{MessageA, MessageB}; use curv::arithmetic::traits::Converter; -use crate::protocols::multi_party_ecdsa::gg_2020::ErrorType; -use crate::utilities::zk_pdl_with_slack::PDLwSlackProof; -use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; -use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; -use curv::cryptographic_primitives::proofs::sigma_valid_pedersen::PedersenProof; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use crate::{ + protocols::multi_party_ecdsa::gg_2020::ErrorType, + utilities::zk_pdl_with_slack::PDLwSlackProof, +}; +use curv::{ + cryptographic_primitives::{ + hashing::{Digest, DigestExt}, + proofs::{sigma_dlog::DLogProof, sigma_valid_pedersen::PedersenProof}, + }, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, +}; use paillier::*; use sha2::Sha256; use zk_paillier::zkproofs::DLogStatement; @@ -162,10 +171,7 @@ fn keygen_t_n_parties( ), ErrorType, > { - let params = Parameters { - threshold: t, - share_count: n, - }; + let params = Parameters { threshold: t, share_count: n }; let (t, n) = (t as usize, n as usize); let party_keys_vec = (0..n).map(Keys::create).collect::>(); @@ -174,10 +180,8 @@ fn keygen_t_n_parties( .map(|k| k.phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2()) .unzip(); - let e_vec = bc1_vec - .iter() - .map(|bc1| bc1.e.clone()) - .collect::>(); + let e_vec = + bc1_vec.iter().map(|bc1| bc1.e.clone()).collect::>(); let h1_h2_N_tilde_vec = bc1_vec .iter() .map(|bc1| bc1.dlog_statement.clone()) @@ -235,7 +239,7 @@ fn keygen_t_n_parties( (&index_vec[i] + 1).into(), ); if res.is_err() { - return Err(res.err().unwrap()); + return Err(res.err().unwrap()) } let (shared_keys, dlog_proof) = res.unwrap(); shared_keys_vec.push(shared_keys); @@ -254,27 +258,28 @@ fn keygen_t_n_parties( ); if dlog_verification.is_err() { - return Err(dlog_verification.err().unwrap()); + return Err(dlog_verification.err().unwrap()) } //test let xi_vec = (0..=t) .map(|i| shared_keys_vec[i].x_i.clone()) .collect::>>(); - let x = vss_scheme_for_test[0] - .clone() - .reconstruct(&index_vec[0..=t], &xi_vec); + let x = + vss_scheme_for_test[0].clone().reconstruct(&index_vec[0..=t], &xi_vec); let sum_u_i = party_keys_vec .iter() .fold(Scalar::::zero(), |acc, x| acc + &x.u_i); assert_eq!(x, sum_u_i); Ok(( - party_keys_vec, // Paillier keys, keypair, N, h1, h2 - shared_keys_vec, // Private shares for this MPC keypair - pk_vec, // dlog proof for x_i - y_sum, // public key for this MPC keypair. - vss_scheme_for_test[0].clone(), // This contains the commitments for each initial share and the shares itself - e_vec, // paillier encryption keys. Why separate ? + party_keys_vec, // Paillier keys, keypair, N, h1, h2 + shared_keys_vec, // Private shares for this MPC keypair + pk_vec, // dlog proof for x_i + y_sum, // public key for this MPC keypair. + vss_scheme_for_test[0].clone(), /* This contains the commitments for + * each initial share and the shares + * itself */ + e_vec, // paillier encryption keys. Why separate ? h1_h2_N_tilde_vec, )) } @@ -288,8 +293,15 @@ fn sign( corrupted_parties: &[usize], ) -> Result { // full key gen emulation - let (party_keys_vec, shared_keys_vec, pk_vec, y, vss_scheme, ek_vec, dlog_statement_vec) = - keygen_t_n_parties(t, n).unwrap(); + let ( + party_keys_vec, + shared_keys_vec, + pk_vec, + y, + vss_scheme, + ek_vec, + dlog_statement_vec, + ) = keygen_t_n_parties(t, n).unwrap(); // transform the t,n share to t,t+1 share. Get the public keys for the same. let g_w_vec = SignKeys::g_w_vec(&pk_vec, &s[..], &vss_scheme); @@ -297,26 +309,29 @@ fn sign( let private_vec = (0..shared_keys_vec.len()) .map(|i| shared_keys_vec[i].x_i.clone()) .collect::>(); - // make sure that we have t t); let ttag = ttag as usize; assert_eq!(s.len(), ttag); - // each party creates a signing key. This happens in parallel IRL. In this test we - // create a vector of signing keys, one for each party. + // each party creates a signing key. This happens in parallel IRL. In this + // test we create a vector of signing keys, one for each party. // throughout i will index parties let sign_keys_vec = (0..ttag) .map(|i| SignKeys::create(&private_vec[s[i]], &vss_scheme, s[i], &s)) .collect::>(); - // each party computes [Ci,Di] = com(g^gamma_i) and broadcast the commitments + // each party computes [Ci,Di] = com(g^gamma_i) and broadcast the + // commitments let (bc1_vec, decommit_vec1): (Vec<_>, Vec<_>) = sign_keys_vec.iter().map(|k| k.phase1_broadcast()).unzip(); // each signer's dlog statement. in reality, parties prove statements - // using only other parties' h1,h2,N_tilde. here we also use own parameters for simplicity + // using only other parties' h1,h2,N_tilde. here we also use own parameters + // for simplicity let signers_dlog_statements = (0..ttag) .map(|i| dlog_statement_vec[s[i]].clone()) .collect::>(); @@ -324,15 +339,23 @@ fn sign( // each party i BROADCASTS encryption of k_i under her Paillier key // m_a_vec = [ma_0;ma_1;,...] // we assume here that party sends the same encryption to all other parties. - // It should be changed to different encryption (randomness) to each counter party + // It should be changed to different encryption (randomness) to each counter + // party let m_a_vec: Vec<_> = sign_keys_vec .iter() .enumerate() - .map(|(i, k)| MessageA::a(&k.k_i, &party_keys_vec[s[i]].ek, &signers_dlog_statements)) + .map(|(i, k)| { + MessageA::a( + &k.k_i, + &party_keys_vec[s[i]].ek, + &signers_dlog_statements, + ) + }) .collect(); - // #each party i sends responses to m_a_vec she received (one response with input gamma_i and one with w_i) - // #m_b_gamma_vec_all is a matrix where column i is a vector of message_b's that were sent to party i + // #each party i sends responses to m_a_vec she received (one response with + // input gamma_i and one with w_i) #m_b_gamma_vec_all is a matrix where + // column i is a vector of message_b's that were sent to party i // aggregation of the n messages of all parties let mut m_b_gamma_vec_all = Vec::new(); @@ -353,13 +376,14 @@ fn sign( for j in 0..ttag - 1 { let ind = if j < i { j } else { j + 1 }; - let (m_b_gamma, beta_gamma, beta_randomness, beta_tag) = MessageB::b( - &sign_keys_vec[ind].gamma_i, - &ek_vec[s[i]], - m_a_vec[i].0.clone(), - &signers_dlog_statements, - ) - .expect("Alice's range proofs in MtA failed"); + let (m_b_gamma, beta_gamma, beta_randomness, beta_tag) = + MessageB::b( + &sign_keys_vec[ind].gamma_i, + &ek_vec[s[i]], + m_a_vec[i].0.clone(), + &signers_dlog_statements, + ) + .expect("Alice's range proofs in MtA failed"); let (m_b_w, beta_wi, _, _) = MessageB::b( &sign_keys_vec[ind].w_i, &ek_vec[s[i]], @@ -383,8 +407,9 @@ fn sign( ni_vec_all.push(ni_vec.clone()); } - // Here we complete the MwA protocols by taking the mb matrices and starting with the first column, - // generating the appropriate message. The first column is the answers of party 1 to mb sent from other parties. + // Here we complete the MwA protocols by taking the mb matrices and starting + // with the first column, generating the appropriate message. The first + // column is the answers of party 1 to mb sent from other parties. // The second column is the answers that party 2 is sending and so on. // TODO: simulate as IRL @@ -407,15 +432,24 @@ fn sign( // TODO: identify these errors let alpha_ij_gamma = m_b - .verify_proofs_get_alpha(&party_keys_vec[s[i]].dk, &sign_keys_vec[i].k_i) + .verify_proofs_get_alpha( + &party_keys_vec[s[i]].dk, + &sign_keys_vec[i].k_i, + ) .expect("wrong dlog or m_b"); let m_b = m_b_w_vec_i[j].clone(); let alpha_ij_wi = m_b - .verify_proofs_get_alpha(&party_keys_vec[s[i]].dk, &sign_keys_vec[i].k_i) + .verify_proofs_get_alpha( + &party_keys_vec[s[i]].dk, + &sign_keys_vec[i].k_i, + ) .expect("wrong dlog or m_b"); - // since we actually run two MtAwc each party needs to make sure that the values B are the same as the public values - // here for b=w_i the parties already know W_i = g^w_i for each party so this check is done here. for b = gamma_i the check will be later when g^gamma_i will become public + // since we actually run two MtAwc each party needs to make sure + // that the values B are the same as the public values + // here for b=w_i the parties already know W_i = g^w_i for each + // party so this check is done here. for b = gamma_i the check will + // be later when g^gamma_i will become public // currently we take the W_i from the other parties signing keys // TODO: use pk_vec (first change from x_i to w_i) for this check. assert_eq!(m_b.b_proof.pk, sign_keys_vec[ind].g_w_i); @@ -452,9 +486,11 @@ fn sign( }) .collect::>>(); - let mut delta = sign_keys_vec[i].phase2_delta_i(&alpha_vec_all[i], &beta_vec); + let mut delta = + sign_keys_vec[i].phase2_delta_i(&alpha_vec_all[i], &beta_vec); - let mut sigma = sign_keys_vec[i].phase2_sigma_i(&miu_vec_all[i], &ni_vec); + let mut sigma = + sign_keys_vec[i].phase2_sigma_i(&miu_vec_all[i], &ni_vec); // test wrong delta corruption if corrupt_step == 5 && corrupted_parties.iter().any(|&x| x == i) { delta = &delta + δ @@ -484,8 +520,8 @@ fn sign( assert_eq!(T_vec[i], T_proof_vec[i].com.clone()); PedersenProof::verify(&T_proof_vec[i]).expect("error T proof"); } - // de-commit to g^gamma_i from phase1, test comm correctness, and that it is the same value used in MtA. - // Return R + // de-commit to g^gamma_i from phase1, test comm correctness, and that it is + // the same value used in MtA. Return R let R_vec = (0..ttag) .map(|i| { @@ -494,8 +530,14 @@ fn sign( let b_proof_vec = (0..ttag - 1) .map(|j| &m_b_gamma_vec[j].b_proof) .collect::>>(); - SignKeys::phase4(&delta_inv, &b_proof_vec, decommit_vec1.clone(), &bc1_vec, i) - .expect("") //TODO: propagate the error + SignKeys::phase4( + &delta_inv, + &b_proof_vec, + decommit_vec1.clone(), + &bc1_vec, + i, + ) + .expect("") //TODO: propagate the error }) .collect::>>(); @@ -506,7 +548,8 @@ fn sign( .collect::>>(); // each party sends first message to all other parties - let mut phase5_proofs_vec: Vec> = vec![Vec::new(); ttag]; + let mut phase5_proofs_vec: Vec> = + vec![Vec::new(); ttag]; for i in 0..ttag { for j in 0..ttag - 1 { let ind = if j < i { j } else { j + 1 }; @@ -536,17 +579,18 @@ fn sign( i, ); if phase5_verify_zk.is_err() { - return Err(phase5_verify_zk.err().unwrap()); + return Err(phase5_verify_zk.err().unwrap()) } } //each party must run the test let phase5_check = LocalSignature::phase5_check_R_dash_sum(&R_dash_vec); if phase5_check.is_err() { - // initiate phase 5 blame protocol to learn which parties acted maliciously. - // each party generates local state and share with other parties. - // assuming sync communication - if a message was failed to arrive from a party - - // this party should automatically be blamed + // initiate phase 5 blame protocol to learn which parties acted + // maliciously. each party generates local state and share with + // other parties. assuming sync communication - if a message was + // failed to arrive from a party - this party should + // automatically be blamed let mut local_state_vec = Vec::new(); for i in 0..ttag { // compose beta tag vector: @@ -556,7 +600,8 @@ fn sign( let ind1 = if j < i { j } else { j + 1 }; let ind2 = if j < i { i - 1 } else { i }; beta_tag_vec_to_test.push(beta_tag_vec_all[ind1][ind2].clone()); - beta_randomness_vec_to_test.push(beta_randomness_vec_all[ind1][ind2].clone()); + beta_randomness_vec_to_test + .push(beta_randomness_vec_all[ind1][ind2].clone()); } let local_state = LocalStatePhase5 { @@ -596,24 +641,31 @@ fn sign( let mut S_vec = Vec::new(); let mut homo_elgamal_proof_vec = Vec::new(); for i in 0..ttag { - let (S_i, homo_elgamal_proof) = LocalSignature::phase6_compute_S_i_and_proof_of_consistency( - &R_vec[i], - &T_vec[i], - &sigma_vec[i], - &l_vec[i], - ); + let (S_i, homo_elgamal_proof) = + LocalSignature::phase6_compute_S_i_and_proof_of_consistency( + &R_vec[i], + &T_vec[i], + &sigma_vec[i], + &l_vec[i], + ); S_vec.push(S_i); homo_elgamal_proof_vec.push(homo_elgamal_proof); } - LocalSignature::phase6_verify_proof(&S_vec, &homo_elgamal_proof_vec, &R_vec, &T_vec)?; + LocalSignature::phase6_verify_proof( + &S_vec, + &homo_elgamal_proof_vec, + &R_vec, + &T_vec, + )?; let phase6_check = LocalSignature::phase6_check_S_i_sum(&y, &S_vec); if phase6_check.is_err() { - // initiate phase 6 blame protocol to learn which parties acted maliciously. - // each party generates local state and share with other parties. - // assuming sync communication - if a message was failed to arrive from a party - - // this party should automatically be blamed + // initiate phase 6 blame protocol to learn which parties acted + // maliciously. each party generates local state and share with + // other parties. assuming sync communication - if a message was + // failed to arrive from a party - this party should + // automatically be blamed let mut local_state_vec = Vec::new(); for i in 0..ttag { @@ -625,7 +677,11 @@ fn sign( ); miu_randomness_vec.push(rand); } - let proof = GlobalStatePhase6::ecddh_proof(&sigma_vec[i], &R_vec[i], &S_vec[i]); + let proof = GlobalStatePhase6::ecddh_proof( + &sigma_vec[i], + &R_vec[i], + &S_vec[i], + ); let local_state = LocalStatePhase6 { k: sign_keys_vec[i].k_i.clone(), k_randomness: m_a_vec[i].1.clone(), @@ -708,7 +764,12 @@ fn sign( Ok(sig) } -fn check_sig(r: &Scalar, s: &Scalar, msg: &BigInt, pk: &Point) { +fn check_sig( + r: &Scalar, + s: &Scalar, + msg: &BigInt, + pk: &Point, +) { use secp256k1::{Message, PublicKey, Signature, SECP256K1}; let raw_msg = BigInt::to_bytes(msg); @@ -751,14 +812,17 @@ fn test_serialize_deserialize() { use serde_json; let k = Keys::create(0); - let (commit, decommit) = k.phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2(); + let (commit, decommit) = + k.phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2(); let encoded = serde_json::to_string(&commit).unwrap(); - let decoded: KeyGenBroadcastMessage1 = serde_json::from_str(&encoded).unwrap(); + let decoded: KeyGenBroadcastMessage1 = + serde_json::from_str(&encoded).unwrap(); assert_eq!(commit.com, decoded.com); let encoded = serde_json::to_string(&decommit).unwrap(); - let decoded: KeyGenDecommitMessage1 = serde_json::from_str(&encoded).unwrap(); + let decoded: KeyGenDecommitMessage1 = + serde_json::from_str(&encoded).unwrap(); assert_eq!(decommit.y_i, decoded.y_i); } #[test] @@ -769,7 +833,8 @@ fn test_small_paillier() { let (ek, dk) = Paillier::keypair_with_modulus_size(2046).keys(); k.dk = dk; k.ek = ek; - let (commit, decommit) = k.phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2(); + let (commit, decommit) = + k.phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2(); assert!(k .phase1_verify_com_phase3_verify_correct_key_verify_dlog_phase2_distribute( &Parameters { diff --git a/multi-party-ecdsa/src/utilities/mta/mod.rs b/multi-party-ecdsa/src/utilities/mta/mod.rs index 43009367..5185e936 100644 --- a/multi-party-ecdsa/src/utilities/mta/mod.rs +++ b/multi-party-ecdsa/src/utilities/mta/mod.rs @@ -16,25 +16,32 @@ /// MtA is described in https://eprint.iacr.org/2019/114.pdf section 3 use curv::arithmetic::traits::Samplable; -use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; -use paillier::traits::EncryptWithChosenRandomness; -use paillier::{Add, Decrypt, Mul}; -use paillier::{DecryptionKey, EncryptionKey, Paillier, Randomness, RawCiphertext, RawPlaintext}; +use curv::{ + cryptographic_primitives::proofs::sigma_dlog::DLogProof, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, +}; +use paillier::{ + traits::EncryptWithChosenRandomness, Add, Decrypt, DecryptionKey, + EncryptionKey, Mul, Paillier, Randomness, RawCiphertext, RawPlaintext, +}; use zk_paillier::zkproofs::DLogStatement; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use crate::protocols::multi_party_ecdsa::gg_2020::party_i::PartyPrivate; -use crate::utilities::mta::range_proofs::AliceProof; -use crate::Error::{self, InvalidKey}; +use crate::{ + protocols::multi_party_ecdsa::gg_2020::party_i::PartyPrivate, + utilities::mta::range_proofs::AliceProof, + Error::{self, InvalidKey}, +}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct MessageA { pub c: BigInt, // paillier encryption - pub range_proofs: Vec, // proofs (using other parties' h1,h2,N_tilde) that the plaintext is small + pub range_proofs: Vec, /* proofs (using other parties' + * h1,h2,N_tilde) that the plaintext + * is small */ } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -45,17 +52,24 @@ pub struct MessageB { } impl MessageA { - /// Creates a new `messageA` using Alice's Paillier encryption key and `dlog_statements` + /// Creates a new `messageA` using Alice's Paillier encryption key and + /// `dlog_statements` /// - other parties' `h1,h2,N_tilde`s for range proofs. - /// If range proofs are not needed (one example is identification of aborts where we - /// only want to reconstruct a ciphertext), `dlog_statements` can be an empty slice. + /// If range proofs are not needed (one example is identification of aborts + /// where we only want to reconstruct a ciphertext), `dlog_statements` + /// can be an empty slice. pub fn a( a: &Scalar, alice_ek: &EncryptionKey, dlog_statements: &[DLogStatement], ) -> (Self, BigInt) { let randomness = BigInt::sample_below(&alice_ek.n); - let m_a = MessageA::a_with_predefined_randomness(a, alice_ek, &randomness, dlog_statements); + let m_a = MessageA::a_with_predefined_randomness( + a, + alice_ek, + &randomness, + dlog_statements, + ); (m_a, randomness) } @@ -76,14 +90,17 @@ impl MessageA { let alice_range_proofs = dlog_statements .iter() .map(|dlog_statement| { - AliceProof::generate(&a.to_bigint(), &c_a, alice_ek, dlog_statement, randomness) + AliceProof::generate( + &a.to_bigint(), + &c_a, + alice_ek, + dlog_statement, + randomness, + ) }) .collect::>(); - Self { - c: c_a, - range_proofs: alice_range_proofs, - } + Self { c: c_a, range_proofs: alice_range_proofs } } } @@ -117,18 +134,20 @@ impl MessageB { dlog_statements: &[DLogStatement], ) -> Result<(Self, Scalar), Error> { if m_a.range_proofs.len() != dlog_statements.len() { - return Err(InvalidKey); + return Err(InvalidKey) } // verify proofs if !m_a .range_proofs .iter() .zip(dlog_statements) - .map(|(proof, dlog_statement)| proof.verify(&m_a.c, alice_ek, dlog_statement)) + .map(|(proof, dlog_statement)| { + proof.verify(&m_a.c, alice_ek, dlog_statement) + }) .all(|x| x) { log::info!("MP-ECDSA : Proof Mismatch"); - return Err(InvalidKey); + return Err(InvalidKey) }; let beta_tag_fe = Scalar::::from(beta_tag); let c_beta_tag = Paillier::encrypt_with_chosen_randomness( @@ -163,7 +182,8 @@ impl MessageB { dk: &DecryptionKey, a: &Scalar, ) -> Result<(Scalar, BigInt), Error> { - let alice_share = Paillier::decrypt(dk, &RawCiphertext::from(self.c.clone())); + let alice_share = + Paillier::decrypt(dk, &RawCiphertext::from(self.c.clone())); let g = Point::generator(); let alpha = Scalar::::from(alice_share.0.as_ref()); let g_alpha = g * α @@ -192,9 +212,9 @@ impl MessageB { let g_alpha = g * α let ba_btag = &self.b_proof.pk * a + &self.beta_tag_proof.pk; - if DLogProof::verify(&self.b_proof).is_ok() - && DLogProof::verify(&self.beta_tag_proof).is_ok() - && ba_btag == g_alpha + if DLogProof::verify(&self.b_proof).is_ok() && + DLogProof::verify(&self.beta_tag_proof).is_ok() && + ba_btag == g_alpha { Ok(alpha) } else { diff --git a/multi-party-ecdsa/src/utilities/mta/range_proofs.rs b/multi-party-ecdsa/src/utilities/mta/range_proofs.rs index 4610a57f..f0d41cc2 100644 --- a/multi-party-ecdsa/src/utilities/mta/range_proofs.rs +++ b/multi-party-ecdsa/src/utilities/mta/range_proofs.rs @@ -6,13 +6,17 @@ //! Zero knowledge range proofs for MtA protocol are implemented here. //! Formal description can be found in Appendix A of https://eprint.iacr.org/2019/114.pdf //! There are some deviations from the original specification: -//! 1) In Bob's proofs `gamma` is sampled from `[0;q^2 * N]` and `tau` from `[0;q^3 * N_tilde]`. -//! 2) A non-interactive version is implemented, with challenge `e` computed via Fiat-Shamir. - -use curv::arithmetic::traits::*; -use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; +//! 1) In Bob's proofs `gamma` is sampled from `[0;q^2 * N]` and `tau` from +//! `[0;q^3 * N_tilde]`. +//! 2) A non-interactive version is implemented, with challenge `e` computed via +//! Fiat-Shamir. + +use curv::{ + arithmetic::traits::*, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, +}; use sha2::Sha256; use paillier::{EncryptionKey, Randomness}; @@ -49,21 +53,16 @@ impl AliceZkpRound1 { let beta = BigInt::from_paillier_key(alice_ek); let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); let ro = BigInt::sample_below(&(q * N_tilde)); - let z = (BigInt::mod_pow(h1, a, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; - let u = ((alpha.borrow() * &alice_ek.n + 1) - * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) - % &alice_ek.nn; - let w = - (BigInt::mod_pow(h1, &alpha, N_tilde) * BigInt::mod_pow(h2, &gamma, N_tilde)) % N_tilde; - Self { - alpha, - beta, - gamma, - ro, - z, - u, - w, - } + let z = (BigInt::mod_pow(h1, a, N_tilde) * + BigInt::mod_pow(h2, &ro, N_tilde)) % + N_tilde; + let u = ((alpha.borrow() * &alice_ek.n + 1) * + BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % + &alice_ek.nn; + let w = (BigInt::mod_pow(h1, &alpha, N_tilde) * + BigInt::mod_pow(h2, &gamma, N_tilde)) % + N_tilde; + Self { alpha, beta, gamma, ro, z, u, w } } } @@ -83,7 +82,8 @@ impl AliceZkpRound2 { r: &BigInt, ) -> Self { Self { - s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, + s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) % + &alice_ek.n, s1: (e * a) + round1.alpha.borrow(), s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), } @@ -116,23 +116,27 @@ impl AliceProof { let Gen = alice_ek.n.borrow() + 1; if self.s1 > Scalar::::group_order().pow(3) { - return false; + return false } - let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); + let z_e_inv = BigInt::mod_inv( + &BigInt::mod_pow(&self.z, &self.e, N_tilde), + N_tilde, + ); let z_e_inv = match z_e_inv { // z must be invertible, yet the check is done here None => return false, Some(c) => c, }; - let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) - * BigInt::mod_pow(h2, &self.s2, N_tilde) - * z_e_inv) - % N_tilde; + let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) * + BigInt::mod_pow(h2, &self.s2, N_tilde) * + z_e_inv) % + N_tilde; let gs1 = (self.s1.borrow() * N + 1) % NN; - let cipher_e_inv = BigInt::mod_inv(&BigInt::mod_pow(cipher, &self.e, NN), NN); + let cipher_e_inv = + BigInt::mod_inv(&BigInt::mod_pow(cipher, &self.e, NN), NN); let cipher_e_inv = match cipher_e_inv { None => return false, Some(c) => c, @@ -149,13 +153,13 @@ impl AliceProof { .chain_bigint(&w) .result_bigint(); if e != self.e { - return false; + return false } true } - /// Create the proof using Alice's Paillier private keys and public ZKP setup. - /// Requires randomness used for encrypting Alice's secret a. + /// Create the proof using Alice's Paillier private keys and public ZKP + /// setup. Requires randomness used for encrypting Alice's secret a. /// It is assumed that secp256k1 curve is used. pub fn generate( a: &BigInt, @@ -235,32 +239,23 @@ impl BobZkpRound1 { let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); let sigma = BigInt::sample_below(&(q * N_tilde)); let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; - let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) - * BigInt::mod_pow(h2, &ro_prim, N_tilde)) - % N_tilde; - let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * BigInt::mod_pow(h2, &sigma, N_tilde)) - % N_tilde; - let w = - (BigInt::mod_pow(h1, &gamma, N_tilde) * BigInt::mod_pow(h2, &tau, N_tilde)) % N_tilde; - let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) - * (gamma.borrow() * &alice_ek.n + 1) - * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) - % &alice_ek.nn; - Self { - alpha, - beta, - gamma, - ro, - ro_prim, - sigma, - tau, - z, - z_prim, - t, - w, - v, - } + let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * + BigInt::mod_pow(h2, &ro, N_tilde)) % + N_tilde; + let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) * + BigInt::mod_pow(h2, &ro_prim, N_tilde)) % + N_tilde; + let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * + BigInt::mod_pow(h2, &sigma, N_tilde)) % + N_tilde; + let w = (BigInt::mod_pow(h1, &gamma, N_tilde) * + BigInt::mod_pow(h2, &tau, N_tilde)) % + N_tilde; + let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) * + (gamma.borrow() * &alice_ek.n + 1) * + BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % + &alice_ek.nn; + Self { alpha, beta, gamma, ro, ro_prim, sigma, tau, z, z_prim, t, w, v } } } @@ -277,7 +272,8 @@ impl BobZkpRound2 { /// `e` - the challenge in interactive ZKP, the hash in non-interactive ZKP /// `b` - Bob's secret /// `beta_prim` - randomly chosen in `MtA` by Bob - /// `r` - randomness used by Bob on Alice's public Paillier key to encrypt `beta_prim` in `MtA` + /// `r` - randomness used by Bob on Alice's public Paillier key to encrypt + /// `beta_prim` in `MtA` fn from( alice_ek: &EncryptionKey, round1: &BobZkpRound1, @@ -288,7 +284,9 @@ impl BobZkpRound2 { ) -> Self { let b_bn = b.to_bigint(); Self { - s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, + s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * + round1.beta.borrow()) % + &alice_ek.n, s1: (e * b_bn) + round1.alpha.borrow(), s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), t1: (e * beta_prim) + round1.gamma.borrow(), @@ -333,43 +331,50 @@ impl BobProof { let h2 = &dlog_statement.ni; if self.s1 > Scalar::::group_order().pow(3) { - return false; + return false } - let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); + let z_e_inv = BigInt::mod_inv( + &BigInt::mod_pow(&self.z, &self.e, N_tilde), + N_tilde, + ); let z_e_inv = match z_e_inv { // z must be invertible, yet the check is done here None => return false, Some(c) => c, }; - let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) - * BigInt::mod_pow(h2, &self.s2, N_tilde) - * z_e_inv) - % N_tilde; + let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) * + BigInt::mod_pow(h2, &self.s2, N_tilde) * + z_e_inv) % + N_tilde; - let mta_e_inv = BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); + let mta_e_inv = + BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); let mta_e_inv = match mta_e_inv { None => return false, Some(c) => c, }; - let v = (BigInt::mod_pow(a_enc, &self.s1, NN) - * BigInt::mod_pow(&self.s, N, NN) - * (self.t1.borrow() * N + 1) - * mta_e_inv) - % NN; + let v = (BigInt::mod_pow(a_enc, &self.s1, NN) * + BigInt::mod_pow(&self.s, N, NN) * + (self.t1.borrow() * N + 1) * + mta_e_inv) % + NN; - let t_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.t, &self.e, N_tilde), N_tilde); + let t_e_inv = BigInt::mod_inv( + &BigInt::mod_pow(&self.t, &self.e, N_tilde), + N_tilde, + ); let t_e_inv = match t_e_inv { None => return false, Some(c) => c, }; - let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) - * BigInt::mod_pow(h2, &self.t2, N_tilde) - * t_e_inv) - % N_tilde; + let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) * + BigInt::mod_pow(h2, &self.t2, N_tilde) * + t_e_inv) % + N_tilde; let Gen = alice_ek.n.borrow() + 1; let mut values_to_hash = vec![ @@ -397,7 +402,7 @@ impl BobProof { .into_iter() .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) .result_bigint() - } + }, None => values_to_hash .into_iter() .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) @@ -405,7 +410,7 @@ impl BobProof { }; if e != self.e { - return false; + return false } true @@ -510,12 +515,9 @@ impl BobProofExt { mta_avc_out, alice_ek, dlog_statement, - Some(&BobCheck { - u: self.u.clone(), - X: X.clone(), - }), + Some(&BobCheck { u: self.u.clone(), X: X.clone() }), ) { - return false; + return false } // fiddle with EC points @@ -527,7 +529,7 @@ impl BobProofExt { }; if x1 != x2 { - return false; + return false } true @@ -546,7 +548,7 @@ impl SampleFromMultiplicativeGroup for BigInt { loop { let r = Self::sample_below(N); if r.gcd(N) == One { - return r; + return r } } } @@ -559,8 +561,10 @@ impl SampleFromMultiplicativeGroup for BigInt { #[cfg(test)] pub(crate) mod tests { use super::*; - use paillier::traits::{Encrypt, EncryptWithChosenRandomness, KeyGeneration}; - use paillier::{Add, DecryptionKey, Mul, Paillier, RawCiphertext, RawPlaintext}; + use paillier::{ + traits::{Encrypt, EncryptWithChosenRandomness, KeyGeneration}, + Add, DecryptionKey, Mul, Paillier, RawCiphertext, RawPlaintext, + }; fn generate( a_encrypted: &BigInt, @@ -583,13 +587,11 @@ pub(crate) mod tests { true, ); - BobProofExt { - proof: bob_proof, - u: u.unwrap(), - } + BobProofExt { proof: bob_proof, u: u.unwrap() } } - pub(crate) fn generate_init() -> (DLogStatement, EncryptionKey, DecryptionKey) { + pub(crate) fn generate_init( + ) -> (DLogStatement, EncryptionKey, DecryptionKey) { let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); let one = BigInt::one(); let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); @@ -604,11 +606,7 @@ pub(crate) mod tests { let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); let (ek, dk) = Paillier::keypair().keys(); - let dlog_statement = DLogStatement { - g: h1, - ni: h2, - N: ek_tilde.n, - }; + let dlog_statement = DLogStatement { g: h1, ni: h2, N: ek_tilde.n }; (dlog_statement, ek, dk) } @@ -628,7 +626,8 @@ pub(crate) mod tests { .clone() .into_owned(); - let alice_proof = AliceProof::generate(&a, &cipher, &ek, &dlog_statement, &r); + let alice_proof = + AliceProof::generate(&a, &cipher, &ek, &dlog_statement, &r); assert!(alice_proof.verify(&cipher, &ek, &dlog_statement)); } @@ -644,10 +643,11 @@ pub(crate) mod tests { (0..5).for_each(|_| { // Simulate Alice let a = Scalar::::random().to_bigint(); - let encrypted_a = Paillier::encrypt(alice_public_key, RawPlaintext::from(a)) - .0 - .clone() - .into_owned(); + let encrypted_a = + Paillier::encrypt(alice_public_key, RawPlaintext::from(a)) + .0 + .clone() + .into_owned(); // Bob follows MtA let b = Scalar::::random(); @@ -665,7 +665,11 @@ pub(crate) mod tests { &r, ); - let mta_out = Paillier::add(alice_public_key, b_times_enc_a, enc_beta_prim); + let mta_out = Paillier::add( + alice_public_key, + b_times_enc_a, + enc_beta_prim, + ); let (bob_proof, _) = BobProof::generate( &encrypted_a, diff --git a/multi-party-ecdsa/src/utilities/mta/test.rs b/multi-party-ecdsa/src/utilities/mta/test.rs index 0602ea9a..c2e6d635 100644 --- a/multi-party-ecdsa/src/utilities/mta/test.rs +++ b/multi-party-ecdsa/src/utilities/mta/test.rs @@ -1,5 +1,6 @@ -use crate::utilities::mta::range_proofs::tests::generate_init; -use crate::utilities::mta::{MessageA, MessageB}; +use crate::utilities::mta::{ + range_proofs::tests::generate_init, MessageA, MessageB, +}; use curv::elliptic::curves::{secp256_k1::Secp256k1, Scalar}; #[test] @@ -7,8 +8,10 @@ fn test_mta() { let alice_input = Scalar::::random(); let (dlog_statement, ek_alice, dk_alice) = generate_init(); let bob_input = Scalar::::random(); - let (m_a, _) = MessageA::a(&alice_input, &ek_alice, &[dlog_statement.clone()]); - let (m_b, beta, _, _) = MessageB::b(&bob_input, &ek_alice, m_a, &[dlog_statement]).unwrap(); + let (m_a, _) = + MessageA::a(&alice_input, &ek_alice, &[dlog_statement.clone()]); + let (m_b, beta, _, _) = + MessageB::b(&bob_input, &ek_alice, m_a, &[dlog_statement]).unwrap(); let alpha = m_b .verify_proofs_get_alpha(&dk_alice, &alice_input) .expect("wrong dlog or m_b"); diff --git a/multi-party-ecdsa/src/utilities/zk_pdl/mod.rs b/multi-party-ecdsa/src/utilities/zk_pdl/mod.rs index 2d703527..cde03ff7 100644 --- a/multi-party-ecdsa/src/utilities/zk_pdl/mod.rs +++ b/multi-party-ecdsa/src/utilities/zk_pdl/mod.rs @@ -22,19 +22,22 @@ use std::ops::Shl; -use curv::arithmetic::traits::*; -use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; -use curv::cryptographic_primitives::commitments::traits::Commitment; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; -use paillier::Paillier; -use paillier::{Add, Decrypt, Encrypt, Mul}; -use paillier::{DecryptionKey, EncryptionKey, RawCiphertext, RawPlaintext}; +use curv::{ + arithmetic::traits::*, + cryptographic_primitives::commitments::{ + hash_commitment::HashCommitment, traits::Commitment, + }, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, +}; +use paillier::{ + Add, Decrypt, DecryptionKey, Encrypt, EncryptionKey, Mul, Paillier, + RawCiphertext, RawPlaintext, +}; use serde::{Deserialize, Serialize}; use sha2::Sha256; use thiserror::Error; -use zk_paillier::zkproofs::IncorrectProof; -use zk_paillier::zkproofs::RangeProofNi; +use zk_paillier::zkproofs::{IncorrectProof, RangeProofNi}; #[derive(Error, Debug)] pub enum ZkPdlError { @@ -109,14 +112,17 @@ pub struct Prover {} pub struct Verifier {} impl Verifier { - pub fn message1(statement: &PDLStatement) -> (PDLVerifierFirstMessage, PDLVerifierState) { + pub fn message1( + statement: &PDLStatement, + ) -> (PDLVerifierFirstMessage, PDLVerifierState) { let a_fe = Scalar::::random(); let a = a_fe.to_bigint(); let q = Scalar::::group_order(); let q_sq = q.pow(2); let b = BigInt::sample_below(&q_sq); let b_fe = Scalar::::from(&b); - let b_enc = Paillier::encrypt(&statement.ek, RawPlaintext::from(b.clone())); + let b_enc = + Paillier::encrypt(&statement.ek, RawPlaintext::from(b.clone())); let ac = Paillier::mul( &statement.ek, RawCiphertext::from(statement.ciphertext.clone()), @@ -158,7 +164,8 @@ impl Verifier { blindness: state.blindness.clone(), }; let range_proof_is_ok = - verify_range_proof(statement, &prover_first_messasge.range_proof).is_ok(); + verify_range_proof(statement, &prover_first_messasge.range_proof) + .is_ok(); state.c_hat = prover_first_messasge.c_hat.clone(); if range_proof_is_ok { Ok(decommit_message) @@ -177,8 +184,8 @@ impl Verifier { &prover_second_message.decommit.blindness, ); - if prover_first_message.c_hat == c_hat_test - && prover_second_message.decommit.q_hat == state.q_tag + if prover_first_message.c_hat == c_hat_test && + prover_second_message.decommit.q_hat == state.q_tag { Ok(()) } else { @@ -197,7 +204,8 @@ impl Prover { let alpha = Paillier::decrypt(&witness.dk, &RawCiphertext::from(c_tag)); let alpha_fe = Scalar::::from(alpha.0.as_ref()); let q_hat = &statement.G * alpha_fe; - let blindness = BigInt::sample_below(Scalar::::group_order()); + let blindness = + BigInt::sample_below(Scalar::::group_order()); let c_hat = HashCommitment::::create_commitment_with_user_defined_randomness( &BigInt::from_bytes(q_hat.to_bytes(true).as_ref()), &blindness, @@ -219,8 +227,8 @@ impl Prover { witness: &PDLWitness, state: &PDLProverState, ) -> Result { - let ab_concat = &verifier_second_message.a - + verifier_second_message + let ab_concat = &verifier_second_message.a + + verifier_second_message .b .clone() .shl(verifier_second_message.a.bit_length()); // b|a (in the paper it is a|b) @@ -231,17 +239,20 @@ impl Prover { ); let ax1 = &verifier_second_message.a * witness.x.to_bigint(); let alpha_test = ax1 + &verifier_second_message.b; - if alpha_test == state.alpha && verifier_first_message.c_tag_tag == c_tag_tag_test { - Ok(PDLProverSecondMessage { - decommit: state.decommit.clone(), - }) + if alpha_test == state.alpha && + verifier_first_message.c_tag_tag == c_tag_tag_test + { + Ok(PDLProverSecondMessage { decommit: state.decommit.clone() }) } else { Err(ZkPdlError::Message2) } } } -fn generate_range_proof(statement: &PDLStatement, witness: &PDLWitness) -> RangeProofNi { +fn generate_range_proof( + statement: &PDLStatement, + witness: &PDLWitness, +) -> RangeProofNi { RangeProofNi::prove( &statement.ek, Scalar::::group_order(), diff --git a/multi-party-ecdsa/src/utilities/zk_pdl/test.rs b/multi-party-ecdsa/src/utilities/zk_pdl/test.rs index 3f4e6994..928ca7d6 100644 --- a/multi-party-ecdsa/src/utilities/zk_pdl/test.rs +++ b/multi-party-ecdsa/src/utilities/zk_pdl/test.rs @@ -1,12 +1,15 @@ #![allow(non_snake_case)] -use curv::arithmetic::traits::*; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; -use paillier::core::Randomness; -use paillier::traits::{EncryptWithChosenRandomness, KeyGeneration}; -use paillier::Paillier; -use paillier::RawPlaintext; +use curv::{ + arithmetic::traits::*, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, +}; +use paillier::{ + core::Randomness, + traits::{EncryptWithChosenRandomness, KeyGeneration}, + Paillier, RawPlaintext, +}; use crate::utilities::zk_pdl::{PDLStatement, PDLWitness, Prover, Verifier}; @@ -29,23 +32,17 @@ fn test_zk_pdl() { ) .0 .into_owned(); - let statement = PDLStatement { - ciphertext: c, - ek, - Q, - G: Point::generator().to_point(), - }; - let witness = PDLWitness { - x, - r: randomness.0, - dk, - }; + let statement = + PDLStatement { ciphertext: c, ek, Q, G: Point::generator().to_point() }; + let witness = PDLWitness { x, r: randomness.0, dk }; // - let (verifier_message1, mut verifier_state) = Verifier::message1(&statement); + let (verifier_message1, mut verifier_state) = + Verifier::message1(&statement); let (prover_message1, prover_state) = Prover::message1(&witness, &statement, &verifier_message1); let verifier_message2 = - Verifier::message2(&prover_message1, &statement, &mut verifier_state).expect(""); + Verifier::message2(&prover_message1, &statement, &mut verifier_state) + .expect(""); let prover_message2 = Prover::message2( &verifier_message1, &verifier_message2, @@ -53,6 +50,7 @@ fn test_zk_pdl() { &prover_state, ) .expect(""); - let result = Verifier::finalize(&prover_message1, &prover_message2, &verifier_state); + let result = + Verifier::finalize(&prover_message1, &prover_message2, &verifier_state); assert!(result.is_ok()); } diff --git a/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs index c9d2c70c..552757b4 100644 --- a/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs +++ b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs @@ -20,12 +20,15 @@ //! //! Statement: (c, pk, Q, G) //! witness (x, r) such that Q = xG, c = Enc(pk, x, r) -//! note that because of the range proof, the proof has a slack in the range: x in [-q^3, q^3] - -use curv::arithmetic::traits::*; -use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; +//! note that because of the range proof, the proof has a slack in the range: x +//! in [-q^3, q^3] + +use curv::{ + arithmetic::traits::*, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, +}; use paillier::EncryptionKey; use serde::{Deserialize, Serialize}; use sha2::Sha256; @@ -65,7 +68,10 @@ pub struct PDLwSlackProof { } impl PDLwSlackProof { - pub fn prove(witness: &PDLwSlackWitness, statement: &PDLwSlackStatement) -> Self { + pub fn prove( + witness: &PDLwSlackWitness, + statement: &PDLwSlackStatement, + ) -> Self { let q3 = Scalar::::group_order().pow(3); let q_N_tilde = Scalar::::group_order() * &statement.N_tilde; let q3_N_tilde = &q3 * &statement.N_tilde; @@ -100,8 +106,12 @@ impl PDLwSlackProof { ); let e = Sha256::new() - .chain_bigint(&BigInt::from_bytes(statement.G.to_bytes(true).as_ref())) - .chain_bigint(&BigInt::from_bytes(statement.Q.to_bytes(true).as_ref())) + .chain_bigint(&BigInt::from_bytes( + statement.G.to_bytes(true).as_ref(), + )) + .chain_bigint(&BigInt::from_bytes( + statement.Q.to_bytes(true).as_ref(), + )) .chain_bigint(&statement.ciphertext) .chain_bigint(&z) .chain_bigint(&BigInt::from_bytes(u1.to_bytes(true).as_ref())) @@ -110,24 +120,29 @@ impl PDLwSlackProof { .result_bigint(); let s1 = &e * witness.x.to_bigint() + alpha; - let s2 = commitment_unknown_order(&witness.r, &beta, &statement.ek.n, &e, &BigInt::one()); + let s2 = commitment_unknown_order( + &witness.r, + &beta, + &statement.ek.n, + &e, + &BigInt::one(), + ); let s3 = &e * rho + gamma; - PDLwSlackProof { - z, - u1, - u2, - u3, - s1, - s2, - s3, - } + PDLwSlackProof { z, u1, u2, u3, s1, s2, s3 } } - pub fn verify(&self, statement: &PDLwSlackStatement) -> Result<(), ZkPdlWithSlackError> { + pub fn verify( + &self, + statement: &PDLwSlackStatement, + ) -> Result<(), ZkPdlWithSlackError> { let e = Sha256::new() - .chain_bigint(&BigInt::from_bytes(statement.G.to_bytes(true).as_ref())) - .chain_bigint(&BigInt::from_bytes(statement.Q.to_bytes(true).as_ref())) + .chain_bigint(&BigInt::from_bytes( + statement.G.to_bytes(true).as_ref(), + )) + .chain_bigint(&BigInt::from_bytes( + statement.Q.to_bytes(true).as_ref(), + )) .chain_bigint(&statement.ciphertext) .chain_bigint(&self.z) .chain_bigint(&BigInt::from_bytes(self.u1.to_bytes(true).as_ref())) @@ -136,8 +151,9 @@ impl PDLwSlackProof { .result_bigint(); let g_s1 = statement.G.clone() * &Scalar::::from(&self.s1); - let e_fe_neg: Scalar = - Scalar::::from(&(Scalar::::group_order() - &e)); + let e_fe_neg: Scalar = Scalar::::from( + &(Scalar::::group_order() - &e), + ); let y_minus_e = &statement.Q * &e_fe_neg; let u1_test = g_s1 + y_minus_e; diff --git a/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs index fabcda4b..314288fa 100644 --- a/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs +++ b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs @@ -1,11 +1,14 @@ #![allow(non_snake_case)] use crate::utilities::zk_pdl_with_slack::*; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; -use paillier::core::Randomness; -use paillier::traits::{EncryptWithChosenRandomness, KeyGeneration}; -use paillier::Paillier; -use paillier::RawPlaintext; +use curv::{ + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, +}; +use paillier::{ + core::Randomness, + traits::{EncryptWithChosenRandomness, KeyGeneration}, + Paillier, RawPlaintext, +}; use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; #[test] @@ -21,11 +24,8 @@ fn test_zk_pdl_with_slack() { let xhi = BigInt::sample_below(&S); let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = DLogStatement { - N: ek_tilde.n.clone(), - g: h1.clone(), - ni: h2.clone(), - }; + let statement = + DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); @@ -59,7 +59,8 @@ fn test_zk_pdl_with_slack() { let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; - let proof = PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); + let proof = + PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); // verify h1,h2, N_tilde let setup_result = composite_dlog_proof.verify(&statement); assert!(setup_result.is_ok()); @@ -81,11 +82,8 @@ fn test_zk_pdl_with_slack_soundness() { let xhi = BigInt::sample_below(&S); let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = DLogStatement { - N: ek_tilde.n.clone(), - g: h1.clone(), - ni: h2.clone(), - }; + let statement = + DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); @@ -120,7 +118,8 @@ fn test_zk_pdl_with_slack_soundness() { let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; - let proof = PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); + let proof = + PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); // verify h1,h2, N_tilde let setup_result = composite_dlog_proof.verify(&statement); assert!(setup_result.is_ok()); From 422a9c0acc93a985200a80b7776465a660d8b9cb Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 09:00:17 +0800 Subject: [PATCH 31/41] Use more workspace dependencies. --- Cargo.toml | 35 ++++++++++++++++++++++------------- fs-dkr/Cargo.toml | 29 +++++++++++++---------------- multi-party-ecdsa/Cargo.toml | 15 ++++++--------- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c36df13f..5355b238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,33 +13,42 @@ members = [ "fs-dkr", ] +[workspace.dependencies] +curv-kzen = { version = "0.10", default-features = false } +centipede = { version = "0.3.1", default-features = false } +zk-paillier = { version = "0.4.4", default-features = false } +sha2 = "0.9" +serde = { version = "1.0", features = ["derive"] } +zeroize = "1" + [workspace.dependencies.multi-party-ecdsa] version = "0.8" path = "multi-party-ecdsa" default-features = false +[workspace.dependencies.paillier] +version = "0.4.3" +package = "kzen-paillier" +default-features = false + [dependencies] multi-party-ecdsa.workspace = true -bincode = "1.3.3" -serde = { version = "1.0", features = ["derive"] } -zeroize = "1" -curv-kzen = { version = "0.10", default-features = false } -centipede = { version = "0.3.1", default-features = false } -zk-paillier = { version = "0.4.4", default-features = false } +curv-kzen.workspace = true +centipede.workspace = true +zk-paillier.workspace = true +sha2.workspace = true +serde.workspace = true +zeroize.workspace = true +paillier.workspace = true fs-dkr = { path = "fs-dkr", default-features = false } round-based = { git = "https://github.com/webb-tools/round-based-protocol", version = "0.1.4", features = ["dev"] } +bincode = "1.3.3" thiserror = "1.0.23" -sha2 = "0.9" digest = "0.9" generic-array = "0.14" rand_chacha = "0.3.1" rand = "0.8.5" -getrandom = {version = "0.2", optional = true} - -[dependencies.paillier] -version = "0.4.3" -package = "kzen-paillier" -default-features = false +getrandom = { version = "0.2", optional = true } [dev-dependencies] criterion = "0.3" diff --git a/fs-dkr/Cargo.toml b/fs-dkr/Cargo.toml index 6bec9133..99e07f04 100644 --- a/fs-dkr/Cargo.toml +++ b/fs-dkr/Cargo.toml @@ -8,10 +8,17 @@ authors = [ ] edition = "2021" -[dependencies.paillier] -version = "0.4.3" -package = "kzen-paillier" -default-features = false +[dependencies] +curv-kzen.workspace = true +zk-paillier.workspace = true +sha2.workspace = true +paillier.workspace = true + +serde = { version = "1.0", features = ["derive"] } +serde_derive = "1.0" +zeroize = "1" +round-based = { git = "https://github.com/webb-tools/round-based-protocol", version = "0.1.4", features = ["dev"] } +thiserror = "1.0.26" [dependencies.multi-party-ecdsa] workspace = true @@ -22,17 +29,7 @@ version = "1" default-features = false features = ["atomic", "alloc"] -[dependencies] -zk-paillier = { version = "0.4.4", default-features = false } -curv = { package = "curv-kzen", version = "0.10", default-features = false } -serde = { version = "1.0", features = ["derive"] } -serde_derive = "1.0" -zeroize = "1" -round-based = { git = "https://github.com/webb-tools/round-based-protocol", version = "0.1.4", features = ["dev"] } -thiserror = "1.0.26" -sha2 = "0.9" - [features] default = ["rust-gmp-kzen"] -rust-gmp-kzen = ["curv/rust-gmp-kzen"] -num-bigint = ["curv/num-bigint"] \ No newline at end of file +rust-gmp-kzen = ["curv-kzen/rust-gmp-kzen"] +num-bigint = ["curv-kzen/num-bigint"] \ No newline at end of file diff --git a/multi-party-ecdsa/Cargo.toml b/multi-party-ecdsa/Cargo.toml index 705cc5de..aeaffca7 100644 --- a/multi-party-ecdsa/Cargo.toml +++ b/multi-party-ecdsa/Cargo.toml @@ -29,23 +29,20 @@ crate-type = ["lib"] default = ["curv-kzen/rust-gmp-kzen"] [dependencies] +curv-kzen.workspace = true +centipede.workspace = true +zk-paillier.workspace = true +sha2.workspace = true +paillier.workspace = true + subtle = { version = "2" } serde = { version = "1.0", features = ["derive"] } zeroize = "^1.5" -curv-kzen = { version = "0.10.0", default-features = false } -centipede = { version = "0.3.1", default-features = false } -zk-paillier = { version = "0.4.4", default-features = false } round-based = { git = "https://github.com/webb-tools/round-based-protocol", features = [] } thiserror = "1.0.23" derivative = "2.2.0" -sha2 = "0.9" log = "0.4.17" -[dependencies.paillier] -package = "kzen-paillier" -version = "0.4.3" -default-features = false - [dev-dependencies] criterion = "0.3" aes-gcm = "0.9.4" From 971b447b4929861cbaf3d3c470adac98b4d8e6db Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 09:05:06 +0800 Subject: [PATCH 32/41] Tidy workspace dependencies. --- Cargo.lock | 1 - Cargo.toml | 6 ++++-- fs-dkr/Cargo.toml | 10 ++++------ multi-party-ecdsa/Cargo.toml | 8 ++++---- .../src/protocols/multi_party_ecdsa/gg_2020/test.rs | 4 ++-- multi-party-ecdsa/src/utilities/mta/mod.rs | 6 +++--- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47fbc667..77d22dee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,7 +1341,6 @@ dependencies = [ "multi-party-ecdsa", "round-based", "serde", - "serde_derive", "sha2 0.9.9", "thiserror", "zeroize", diff --git a/Cargo.toml b/Cargo.toml index 5355b238..cd880869 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ zk-paillier = { version = "0.4.4", default-features = false } sha2 = "0.9" serde = { version = "1.0", features = ["derive"] } zeroize = "1" +round-based = { git = "https://github.com/webb-tools/round-based-protocol", version = "0.1.4", features = ["dev"] } +thiserror = "1.0.23" [workspace.dependencies.multi-party-ecdsa] version = "0.8" @@ -40,10 +42,10 @@ sha2.workspace = true serde.workspace = true zeroize.workspace = true paillier.workspace = true +round-based.workspace = true +thiserror.workspace = true fs-dkr = { path = "fs-dkr", default-features = false } -round-based = { git = "https://github.com/webb-tools/round-based-protocol", version = "0.1.4", features = ["dev"] } bincode = "1.3.3" -thiserror = "1.0.23" digest = "0.9" generic-array = "0.14" rand_chacha = "0.3.1" diff --git a/fs-dkr/Cargo.toml b/fs-dkr/Cargo.toml index 99e07f04..88fd1e0c 100644 --- a/fs-dkr/Cargo.toml +++ b/fs-dkr/Cargo.toml @@ -13,12 +13,10 @@ curv-kzen.workspace = true zk-paillier.workspace = true sha2.workspace = true paillier.workspace = true - -serde = { version = "1.0", features = ["derive"] } -serde_derive = "1.0" -zeroize = "1" -round-based = { git = "https://github.com/webb-tools/round-based-protocol", version = "0.1.4", features = ["dev"] } -thiserror = "1.0.26" +round-based.workspace = true +serde.workspace = true +zeroize.workspace = true +thiserror.workspace = true [dependencies.multi-party-ecdsa] workspace = true diff --git a/multi-party-ecdsa/Cargo.toml b/multi-party-ecdsa/Cargo.toml index aeaffca7..275a5cae 100644 --- a/multi-party-ecdsa/Cargo.toml +++ b/multi-party-ecdsa/Cargo.toml @@ -34,12 +34,12 @@ centipede.workspace = true zk-paillier.workspace = true sha2.workspace = true paillier.workspace = true +round-based.workspace = true +thiserror.workspace = true +serde.workspace = true +zeroize.workspace = true subtle = { version = "2" } -serde = { version = "1.0", features = ["derive"] } -zeroize = "^1.5" -round-based = { git = "https://github.com/webb-tools/round-based-protocol", features = [] } -thiserror = "1.0.23" derivative = "2.2.0" log = "0.4.17" diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs index deb11b7b..2d516f74 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs @@ -277,8 +277,8 @@ fn keygen_t_n_parties( pk_vec, // dlog proof for x_i y_sum, // public key for this MPC keypair. vss_scheme_for_test[0].clone(), /* This contains the commitments for - * each initial share and the shares - * itself */ + * each initial share and the shares + * itself */ e_vec, // paillier encryption keys. Why separate ? h1_h2_N_tilde_vec, )) diff --git a/multi-party-ecdsa/src/utilities/mta/mod.rs b/multi-party-ecdsa/src/utilities/mta/mod.rs index 5185e936..e153e1d8 100644 --- a/multi-party-ecdsa/src/utilities/mta/mod.rs +++ b/multi-party-ecdsa/src/utilities/mta/mod.rs @@ -38,10 +38,10 @@ use crate::{ #[derive(Clone, Debug, Serialize, Deserialize)] pub struct MessageA { - pub c: BigInt, // paillier encryption + pub c: BigInt, // paillier encryption pub range_proofs: Vec, /* proofs (using other parties' - * h1,h2,N_tilde) that the plaintext - * is small */ + * h1,h2,N_tilde) that the plaintext + * is small */ } #[derive(Clone, Debug, Serialize, Deserialize)] From 93dc199cdbd65ed557c00ee814f6bdcf75929462 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 09:14:05 +0800 Subject: [PATCH 33/41] Move multi party ecdsa module. Use backwards compatible re-export from old module path. --- .../src/{protocols/multi_party_ecdsa => }/gg_2020/blame.rs | 0 .../src/{protocols/multi_party_ecdsa => }/gg_2020/mod.rs | 0 .../src/{protocols/multi_party_ecdsa => }/gg_2020/party_i.rs | 0 .../multi_party_ecdsa => }/gg_2020/state_machine/keygen.rs | 0 .../gg_2020/state_machine/keygen/rounds.rs | 0 .../multi_party_ecdsa => }/gg_2020/state_machine/mod.rs | 0 .../multi_party_ecdsa => }/gg_2020/state_machine/sign.rs | 0 .../multi_party_ecdsa => }/gg_2020/state_machine/sign/fmt.rs | 0 .../multi_party_ecdsa => }/gg_2020/state_machine/sign/rounds.rs | 0 .../multi_party_ecdsa => }/gg_2020/state_machine/traits.rs | 0 .../src/{protocols/multi_party_ecdsa => }/gg_2020/test.rs | 0 multi-party-ecdsa/src/lib.rs | 1 + multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs | 2 +- 13 files changed, 2 insertions(+), 1 deletion(-) rename multi-party-ecdsa/src/{protocols/multi_party_ecdsa => }/gg_2020/blame.rs (100%) rename multi-party-ecdsa/src/{protocols/multi_party_ecdsa => }/gg_2020/mod.rs (100%) rename multi-party-ecdsa/src/{protocols/multi_party_ecdsa => }/gg_2020/party_i.rs (100%) rename multi-party-ecdsa/src/{protocols/multi_party_ecdsa => }/gg_2020/state_machine/keygen.rs (100%) rename multi-party-ecdsa/src/{protocols/multi_party_ecdsa => }/gg_2020/state_machine/keygen/rounds.rs (100%) rename multi-party-ecdsa/src/{protocols/multi_party_ecdsa => }/gg_2020/state_machine/mod.rs (100%) rename multi-party-ecdsa/src/{protocols/multi_party_ecdsa => }/gg_2020/state_machine/sign.rs (100%) rename multi-party-ecdsa/src/{protocols/multi_party_ecdsa => }/gg_2020/state_machine/sign/fmt.rs (100%) rename multi-party-ecdsa/src/{protocols/multi_party_ecdsa => }/gg_2020/state_machine/sign/rounds.rs (100%) rename multi-party-ecdsa/src/{protocols/multi_party_ecdsa => }/gg_2020/state_machine/traits.rs (100%) rename multi-party-ecdsa/src/{protocols/multi_party_ecdsa => }/gg_2020/test.rs (100%) diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs b/multi-party-ecdsa/src/gg_2020/blame.rs similarity index 100% rename from multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs rename to multi-party-ecdsa/src/gg_2020/blame.rs diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/mod.rs b/multi-party-ecdsa/src/gg_2020/mod.rs similarity index 100% rename from multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/mod.rs rename to multi-party-ecdsa/src/gg_2020/mod.rs diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs b/multi-party-ecdsa/src/gg_2020/party_i.rs similarity index 100% rename from multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs rename to multi-party-ecdsa/src/gg_2020/party_i.rs diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs b/multi-party-ecdsa/src/gg_2020/state_machine/keygen.rs similarity index 100% rename from multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs rename to multi-party-ecdsa/src/gg_2020/state_machine/keygen.rs diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs b/multi-party-ecdsa/src/gg_2020/state_machine/keygen/rounds.rs similarity index 100% rename from multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs rename to multi-party-ecdsa/src/gg_2020/state_machine/keygen/rounds.rs diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/mod.rs b/multi-party-ecdsa/src/gg_2020/state_machine/mod.rs similarity index 100% rename from multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/mod.rs rename to multi-party-ecdsa/src/gg_2020/state_machine/mod.rs diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign.rs b/multi-party-ecdsa/src/gg_2020/state_machine/sign.rs similarity index 100% rename from multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign.rs rename to multi-party-ecdsa/src/gg_2020/state_machine/sign.rs diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/fmt.rs b/multi-party-ecdsa/src/gg_2020/state_machine/sign/fmt.rs similarity index 100% rename from multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/fmt.rs rename to multi-party-ecdsa/src/gg_2020/state_machine/sign/fmt.rs diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs b/multi-party-ecdsa/src/gg_2020/state_machine/sign/rounds.rs similarity index 100% rename from multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs rename to multi-party-ecdsa/src/gg_2020/state_machine/sign/rounds.rs diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/traits.rs b/multi-party-ecdsa/src/gg_2020/state_machine/traits.rs similarity index 100% rename from multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/traits.rs rename to multi-party-ecdsa/src/gg_2020/state_machine/traits.rs diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs b/multi-party-ecdsa/src/gg_2020/test.rs similarity index 100% rename from multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs rename to multi-party-ecdsa/src/gg_2020/test.rs diff --git a/multi-party-ecdsa/src/lib.rs b/multi-party-ecdsa/src/lib.rs index 2b6be622..525697e8 100644 --- a/multi-party-ecdsa/src/lib.rs +++ b/multi-party-ecdsa/src/lib.rs @@ -18,6 +18,7 @@ #![allow(clippy::too_many_arguments)] #![allow(clippy::type_complexity)] +pub mod gg_2020; pub mod protocols; pub mod utilities; use std::fmt; diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs index af2cbc54..a1fcfdd0 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs @@ -14,4 +14,4 @@ @license GPL-3.0+ */ -pub mod gg_2020; +pub use crate::gg_2020; From 88dbbfe0b129dfd8ddf599b6cc2739ff089dcd7f Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 09:15:53 +0800 Subject: [PATCH 34/41] Add deprecated notice to protocols module. --- multi-party-ecdsa/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/multi-party-ecdsa/src/lib.rs b/multi-party-ecdsa/src/lib.rs index 525697e8..1825415e 100644 --- a/multi-party-ecdsa/src/lib.rs +++ b/multi-party-ecdsa/src/lib.rs @@ -19,6 +19,7 @@ #![allow(clippy::type_complexity)] pub mod gg_2020; +#[deprecated(note = "Use top-level protocol namespace (eg: gg_2020)")] pub mod protocols; pub mod utilities; use std::fmt; From afe45714d409cf19f86f1acff4441810e234a3c5 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 09:16:56 +0800 Subject: [PATCH 35/41] Tidy module path. --- multi-party-ecdsa/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi-party-ecdsa/src/lib.rs b/multi-party-ecdsa/src/lib.rs index 1825415e..cd88363e 100644 --- a/multi-party-ecdsa/src/lib.rs +++ b/multi-party-ecdsa/src/lib.rs @@ -24,7 +24,7 @@ pub mod protocols; pub mod utilities; use std::fmt; -pub use protocols::multi_party_ecdsa::gg_2020::state_machine::traits::MessageRoundID; +pub use gg_2020::state_machine::traits::MessageRoundID; #[derive(Copy, PartialEq, Eq, Clone, Debug)] pub enum Error { From 3f5359ee2c4a393e56c4144d7eb7be7ae2318287 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 09:18:14 +0800 Subject: [PATCH 36/41] Remove unused box_patterns flag. --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 191410cf..49f84ce4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,6 @@ */ #![allow(non_snake_case)] -#![feature(box_patterns)] use serde::{Deserialize, Serialize}; From e96cb660b03101c71c7f64ae4fbe00db166feea4 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 09:22:13 +0800 Subject: [PATCH 37/41] Migrate to stable toolchain. --- .github/workflows/checks.yml | 8 +- .github/workflows/tests.yml | 2 +- fs-dkr/src/add_party_message.rs | 50 +++-- fs-dkr/src/error.rs | 11 +- fs-dkr/src/range_proofs.rs | 136 ++++++++------ fs-dkr/src/refresh_message.rs | 44 ++--- fs-dkr/src/ring_pedersen_proof.rs | 20 +- fs-dkr/src/test.rs | 35 ++-- fs-dkr/src/zk_pdl_with_slack.rs | 11 +- multi-party-ecdsa/examples/common.rs | 21 ++- multi-party-ecdsa/examples/gg20_keygen.rs | 10 +- multi-party-ecdsa/examples/gg20_signing.rs | 20 +- multi-party-ecdsa/examples/gg20_sm_client.rs | 25 ++- multi-party-ecdsa/examples/gg20_sm_manager.rs | 25 ++- multi-party-ecdsa/src/gg_2020/blame.rs | 4 +- multi-party-ecdsa/src/gg_2020/party_i.rs | 71 ++++--- .../src/gg_2020/state_machine/keygen.rs | 68 +++---- .../gg_2020/state_machine/keygen/rounds.rs | 2 +- .../src/gg_2020/state_machine/sign.rs | 78 ++++---- .../src/gg_2020/state_machine/sign/fmt.rs | 6 +- .../src/gg_2020/state_machine/sign/rounds.rs | 42 ++++- multi-party-ecdsa/src/gg_2020/test.rs | 22 ++- multi-party-ecdsa/src/utilities/mta/mod.rs | 15 +- .../src/utilities/mta/range_proofs.rs | 151 +++++++++------ multi-party-ecdsa/src/utilities/zk_pdl/mod.rs | 16 +- .../src/utilities/zk_pdl/test.rs | 14 +- .../src/utilities/zk_pdl_with_slack/mod.rs | 10 +- .../src/utilities/zk_pdl_with_slack/test.rs | 14 +- rust-toolchain.toml | 3 +- rustfmt.toml | 22 +-- src/party_i.rs | 77 +++++--- src/presign/mod.rs | 5 +- src/presign/rounds.rs | 173 ++++++++++++------ src/presign/state_machine.rs | 142 ++++++++------ src/refresh/rounds.rs | 14 +- src/refresh/state_machine.rs | 106 +++++++---- src/sign/rounds.rs | 113 ++++++------ src/sign/state_machine.rs | 81 +++++--- src/utilities/aff_g/mod.rs | 15 +- src/utilities/dec_q/mod.rs | 19 +- src/utilities/enc/mod.rs | 47 +++-- src/utilities/log_star/mod.rs | 47 +++-- src/utilities/mta/mod.rs | 15 +- src/utilities/mta/range_proofs.rs | 151 +++++++++------ src/utilities/mul/mod.rs | 22 ++- src/utilities/mul_star/mod.rs | 40 ++-- src/utilities/sha2.rs | 8 +- src/utilities/zk_pdl/mod.rs | 16 +- src/utilities/zk_pdl/test.rs | 14 +- src/utilities/zk_pdl_with_slack/mod.rs | 10 +- src/utilities/zk_pdl_with_slack/test.rs | 14 +- 51 files changed, 1320 insertions(+), 765 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 85398e06..d9b88c3a 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -50,10 +50,10 @@ jobs: access_token: ${{ github.token }} - uses: actions/checkout@v2 - - name: Install nightly toolchain + - name: Install toolchain uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2023-10-01 + toolchain: stable - name: Rust Cache uses: Swatinem/rust-cache@v1.3.0 @@ -76,10 +76,10 @@ jobs: access_token: ${{ github.token }} - uses: actions/checkout@v2 - - name: Install nightly toolchain + - name: Install toolchain uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2023-10-01 + toolchain: stable components: rustfmt, clippy - name: Rust Cache diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2c6413a2..84c3c042 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,7 @@ jobs: - name: Install Toolchain uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2023-10-01 + toolchain: stable components: rustfmt, clippy target: wasm32-unknown-unknown override: true diff --git a/fs-dkr/src/add_party_message.rs b/fs-dkr/src/add_party_message.rs index a33563e8..ca01beb5 100644 --- a/fs-dkr/src/add_party_message.rs +++ b/fs-dkr/src/add_party_message.rs @@ -85,10 +85,17 @@ fn generate_dlog_statement_proofs( ) -> (DLogStatement, CompositeDLogProof, CompositeDLogProof) { let (n_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_n_tilde(); - let dlog_statement_base_h1 = - DLogStatement { N: n_tilde.clone(), g: h1.clone(), ni: h2.clone() }; + let dlog_statement_base_h1 = DLogStatement { + N: n_tilde.clone(), + g: h1.clone(), + ni: h2.clone(), + }; - let dlog_statement_base_h2 = DLogStatement { N: n_tilde, g: h2, ni: h1 }; + let dlog_statement_base_h2 = DLogStatement { + N: n_tilde, + g: h2, + ni: h1, + }; let composite_dlog_proof_base_h1 = CompositeDLogProof::prove(&dlog_statement_base_h1, &xhi); @@ -147,7 +154,8 @@ impl JoinMessage { /// Returns the party index if it has been assigned one, throws /// [FsDkrError::NewPartyUnassignedIndexError] otherwise pub fn get_party_index(&self) -> FsDkrResult { - self.party_index.ok_or(FsDkrError::NewPartyUnassignedIndexError) + self.party_index + .ok_or(FsDkrError::NewPartyUnassignedIndexError) } /// Collect phase of the protocol. Compared to the @@ -202,8 +210,10 @@ impl JoinMessage { join_message.get_party_index()?; } - let parameters = - ShamirSecretSharing { threshold: new_t, share_count: new_n }; + let parameters = ShamirSecretSharing { + threshold: new_t, + share_count: new_n, + }; // generate a new share, the details can be found here https://hackmd.io/@omershlo/Hy1jBo6JY. let (cipher_text_sum, li_vec) = RefreshMessage::get_ciphertext_sum( @@ -213,27 +223,31 @@ impl JoinMessage { &paillier_key.ek, current_t, ); - let new_share = - Paillier::decrypt(&paillier_key.dk, cipher_text_sum).0.into_owned(); + let new_share = Paillier::decrypt(&paillier_key.dk, cipher_text_sum) + .0 + .into_owned(); let new_share_fe: Scalar = Scalar::::from(&new_share); let paillier_dk = paillier_key.dk.clone(); let key_linear_x_i = new_share_fe.clone(); let key_linear_y = Point::::generator() * new_share_fe.clone(); - let keys_linear = SharedKeys { x_i: key_linear_x_i, y: key_linear_y }; + let keys_linear = SharedKeys { + x_i: key_linear_x_i, + y: key_linear_y, + }; let mut pk_vec: Vec<_> = (0..new_n as usize) .map(|i| { - refresh_messages[0].points_committed_vec[i].clone() * - li_vec[0].clone() + refresh_messages[0].points_committed_vec[i].clone() + * li_vec[0].clone() }) .collect(); #[allow(clippy::needless_range_loop)] for i in 0..new_n as usize { for j in 1..(current_t + 1) as usize { - pk_vec[i] = pk_vec[i].clone() + - refresh_messages[j].points_committed_vec[i].clone() * - li_vec[j].clone(); + pk_vec[i] = pk_vec[i].clone() + + refresh_messages[j].points_committed_vec[i].clone() + * li_vec[j].clone(); } } @@ -271,8 +285,10 @@ impl JoinMessage { .map(|party| { let ek = available_parties.get(&party); match ek { - None => - EncryptionKey { n: BigInt::zero(), nn: BigInt::zero() }, + None => EncryptionKey { + n: BigInt::zero(), + nn: BigInt::zero(), + }, Some(key) => (*key).clone(), } }) @@ -293,7 +309,7 @@ impl JoinMessage { // they differ, abort. TODO: this should be verifiable? for refresh_message in refresh_messages.iter() { if refresh_message.public_key != refresh_messages[0].public_key { - return Err(FsDkrError::BroadcastedPublicKeyError) + return Err(FsDkrError::BroadcastedPublicKeyError); } } diff --git a/fs-dkr/src/error.rs b/fs-dkr/src/error.rs index 064d79c5..c5d988a3 100644 --- a/fs-dkr/src/error.rs +++ b/fs-dkr/src/error.rs @@ -25,7 +25,11 @@ pub enum FsDkrError { }, #[error("PDLwSlack proof verification failed, results: u1 == u1_test: {is_u1_eq:?}, u2 == u2_test: {is_u2_eq:?}, u3 == u3_test: {is_u3_eq:?}")] - PDLwSlackProof { is_u1_eq: bool, is_u2_eq: bool, is_u3_eq: bool }, + PDLwSlackProof { + is_u1_eq: bool, + is_u2_eq: bool, + is_u3_eq: bool, + }, #[error("Ring Pedersen Proof Failed")] RingPedersenProofError, @@ -34,7 +38,10 @@ pub enum FsDkrError { RangeProof { party_index: usize }, #[error("The Paillier moduli size of party: {party_index:?} is {moduli_size:?} bits, when it should be 2047-2048 bits")] - ModuliTooSmall { party_index: u16, moduli_size: usize }, + ModuliTooSmall { + party_index: u16, + moduli_size: usize, + }, #[error("Paillier verification proof failed for party {party_index:?}")] PaillierVerificationError { party_index: u16 }, diff --git a/fs-dkr/src/range_proofs.rs b/fs-dkr/src/range_proofs.rs index ea93b329..b9645825 100644 --- a/fs-dkr/src/range_proofs.rs +++ b/fs-dkr/src/range_proofs.rs @@ -56,16 +56,24 @@ impl AliceZkpRound1 { let beta = BigInt::from_paillier_key(alice_ek); let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); let ro = BigInt::sample_below(&(q * N_tilde)); - let z = (BigInt::mod_pow(h1, a, N_tilde) * - BigInt::mod_pow(h2, &ro, N_tilde)) % - N_tilde; - let u = ((alpha.borrow() * &alice_ek.n + 1) * - BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % - &alice_ek.nn; - let w = (BigInt::mod_pow(h1, &alpha, N_tilde) * - BigInt::mod_pow(h2, &gamma, N_tilde)) % - N_tilde; - Self { alpha, beta, gamma, ro, z, u, w } + let z = (BigInt::mod_pow(h1, a, N_tilde) + * BigInt::mod_pow(h2, &ro, N_tilde)) + % N_tilde; + let u = ((alpha.borrow() * &alice_ek.n + 1) + * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) + % &alice_ek.nn; + let w = (BigInt::mod_pow(h1, &alpha, N_tilde) + * BigInt::mod_pow(h2, &gamma, N_tilde)) + % N_tilde; + Self { + alpha, + beta, + gamma, + ro, + z, + u, + w, + } } } @@ -85,8 +93,8 @@ impl AliceZkpRound2 { r: &BigInt, ) -> Self { Self { - s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) % - &alice_ek.n, + s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) + % &alice_ek.n, s1: (e * a) + round1.alpha.borrow(), s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), } @@ -120,7 +128,7 @@ impl AliceProof { let Gen = alice_ek.n.borrow() + 1; if self.s1 > Scalar::::group_order().pow(3) { - return false + return false; } let z_e_inv = BigInt::mod_inv( @@ -133,10 +141,10 @@ impl AliceProof { Some(c) => c, }; - let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) * - BigInt::mod_pow(h2, &self.s2, N_tilde) * - z_e_inv) % - N_tilde; + let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) + * BigInt::mod_pow(h2, &self.s2, N_tilde) + * z_e_inv) + % N_tilde; let gs1 = (self.s1.borrow() * N + 1) % NN; let cipher_e_inv = @@ -158,7 +166,7 @@ impl AliceProof { .result_bigint(); if e != self.e { - return false + return false; } true @@ -271,22 +279,22 @@ impl BobZkpRound1 { let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); let sigma = BigInt::sample_below(&(q * N_tilde)); let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * - BigInt::mod_pow(h2, &ro, N_tilde)) % - N_tilde; - let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) * - BigInt::mod_pow(h2, &ro_prim, N_tilde)) % - N_tilde; - let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * - BigInt::mod_pow(h2, &sigma, N_tilde)) % - N_tilde; - let w = (BigInt::mod_pow(h1, &gamma, N_tilde) * - BigInt::mod_pow(h2, &tau, N_tilde)) % - N_tilde; - let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) * - (gamma.borrow() * &alice_ek.n + 1) * - BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % - &alice_ek.nn; + let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) + * BigInt::mod_pow(h2, &ro, N_tilde)) + % N_tilde; + let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) + * BigInt::mod_pow(h2, &ro_prim, N_tilde)) + % N_tilde; + let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) + * BigInt::mod_pow(h2, &sigma, N_tilde)) + % N_tilde; + let w = (BigInt::mod_pow(h1, &gamma, N_tilde) + * BigInt::mod_pow(h2, &tau, N_tilde)) + % N_tilde; + let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) + * (gamma.borrow() * &alice_ek.n + 1) + * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) + % &alice_ek.nn; Self { alpha, beta, @@ -331,9 +339,9 @@ impl BobZkpRound2 { ) -> Self { let b_bn = b.to_bigint(); Self { - s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * - round1.beta.borrow()) % - &alice_ek.n, + s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) + * round1.beta.borrow()) + % &alice_ek.n, s1: (e * b_bn) + round1.alpha.borrow(), s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), t1: (e * beta_prim) + round1.gamma.borrow(), @@ -380,7 +388,7 @@ impl BobProof { let h2 = &dlog_statement.ni; if self.s1 > Scalar::::group_order().pow(3) { - return false + return false; } let z_e_inv = BigInt::mod_inv( @@ -393,10 +401,10 @@ impl BobProof { Some(c) => c, }; - let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) * - BigInt::mod_pow(h2, &self.s2, N_tilde) * - z_e_inv) % - N_tilde; + let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) + * BigInt::mod_pow(h2, &self.s2, N_tilde) + * z_e_inv) + % N_tilde; let mta_e_inv = BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); @@ -405,11 +413,11 @@ impl BobProof { Some(c) => c, }; - let v = (BigInt::mod_pow(a_enc, &self.s1, NN) * - BigInt::mod_pow(&self.s, N, NN) * - (self.t1.borrow() * N + 1) * - mta_e_inv) % - NN; + let v = (BigInt::mod_pow(a_enc, &self.s1, NN) + * BigInt::mod_pow(&self.s, N, NN) + * (self.t1.borrow() * N + 1) + * mta_e_inv) + % NN; let t_e_inv = BigInt::mod_inv( &BigInt::mod_pow(&self.t, &self.e, N_tilde), @@ -420,10 +428,10 @@ impl BobProof { Some(c) => c, }; - let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) * - BigInt::mod_pow(h2, &self.t2, N_tilde) * - t_e_inv) % - N_tilde; + let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) + * BigInt::mod_pow(h2, &self.t2, N_tilde) + * t_e_inv) + % N_tilde; let Gen = alice_ek.n.borrow() + 1; @@ -449,12 +457,12 @@ impl BobProof { .chain_bigint(&u_x_coor) .chain_bigint(&u_y_coor) .result_bigint() - }, + } None => hash.result_bigint(), }; if e != self.e { - return false + return false; } true @@ -553,9 +561,12 @@ impl BobProofExt { mta_avc_out, alice_ek, dlog_statement, - Some(&BobCheck { u: self.u.clone(), X: X.clone() }), + Some(&BobCheck { + u: self.u.clone(), + X: X.clone(), + }), ) { - return false + return false; } // fiddle with EC points @@ -567,7 +578,7 @@ impl BobProofExt { }; if x1 != x2 { - return false + return false; } true @@ -594,7 +605,10 @@ impl BobProofExt { true, ); - BobProofExt { proof: bob_proof, u: u.unwrap() } + BobProofExt { + proof: bob_proof, + u: u.unwrap(), + } } } @@ -610,7 +624,7 @@ impl SampleFromMultiplicativeGroup for BigInt { loop { let r = Self::sample_below(N); if r.gcd(N) == One { - return r + return r; } } } @@ -656,7 +670,11 @@ pub(crate) mod tests { let (ek, dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE) .keys(); - let dlog_statement = DLogStatement { g: h1, ni: h2, N: ek_tilde.n }; + let dlog_statement = DLogStatement { + g: h1, + ni: h2, + N: ek_tilde.n, + }; (dlog_statement, ek, dk) } diff --git a/fs-dkr/src/refresh_message.rs b/fs-dkr/src/refresh_message.rs index 15aced57..4cb31e30 100644 --- a/fs-dkr/src/refresh_message.rs +++ b/fs-dkr/src/refresh_message.rs @@ -59,7 +59,7 @@ impl RefreshMessage { let secret = local_key.keys_linear.x_i.clone(); // secret share old key if new_n <= new_t { - return Err(FsDkrError::NewPartyUnassignedIndexError) + return Err(FsDkrError::NewPartyUnassignedIndexError); } let (vss_scheme, secret_shares) = VerifiableSS::::share(new_t, new_n, &secret); @@ -72,8 +72,8 @@ impl RefreshMessage { .collect(); // encrypt points on the polynomial using Paillier keys - let (points_encrypted_vec, randomness_vec): (Vec<_>, Vec<_>) = (0.. - secret_shares.len()) + let (points_encrypted_vec, randomness_vec): (Vec<_>, Vec<_>) = (0 + ..secret_shares.len()) .map(|i| { let randomness = BigInt::sample_below(&local_key.paillier_key_vec[i].n); @@ -167,7 +167,7 @@ impl RefreshMessage { return Err(FsDkrError::PartiesThresholdViolation { threshold: current_t, refreshed_keys: refresh_messages.len(), - }) + }); } // check all vectors are of same length @@ -180,16 +180,16 @@ impl RefreshMessage { let points_encrypted_len = refresh_message.points_encrypted_vec.len(); - if !(pdl_proof_len == reference_len && - points_commited_len == reference_len && - points_encrypted_len == reference_len) + if !(pdl_proof_len == reference_len + && points_commited_len == reference_len + && points_encrypted_len == reference_len) { return Err(FsDkrError::SizeMismatchError { refresh_message_index: k, pdl_proof_len, points_commited_len, points_encrypted_len, - }) + }); } } @@ -204,7 +204,7 @@ impl RefreshMessage { ) .is_err() { - return Err(FsDkrError::PublicShareValidationError) + return Err(FsDkrError::PublicShareValidationError); } } } @@ -385,7 +385,7 @@ impl RefreshMessage { &statement.ek, &local_key.h1_h2_n_tilde_vec[i], ) { - return Err(FsDkrError::RangeProof { party_index: i }) + return Err(FsDkrError::RangeProof { party_index: i }); } } } @@ -423,7 +423,7 @@ impl RefreshMessage { { return Err(FsDkrError::PaillierVerificationError { party_index: refresh_message.party_index, - }) + }); } let n_length = refresh_message.ek.n.bit_length(); if !(crate::PAILLIER_KEY_SIZE - 1..=crate::PAILLIER_KEY_SIZE) @@ -432,7 +432,7 @@ impl RefreshMessage { return Err(FsDkrError::ModuliTooSmall { party_index: refresh_message.party_index, moduli_size: n_length, - }) + }); } // if the proof checks, we add the new paillier public key to the @@ -452,7 +452,7 @@ impl RefreshMessage { { return Err(FsDkrError::PaillierVerificationError { party_index, - }) + }); } // creating an inverse dlog statement @@ -464,13 +464,13 @@ impl RefreshMessage { if join_message .composite_dlog_proof_base_h1 .verify(&join_message.dlog_statement) - .is_err() || - join_message + .is_err() + || join_message .composite_dlog_proof_base_h2 .verify(&dlog_statement_base_h2) .is_err() { - return Err(FsDkrError::DLogProofValidation { party_index }) + return Err(FsDkrError::DLogProofValidation { party_index }); } let n_length = join_message.ek.n.bit_length(); @@ -482,7 +482,7 @@ impl RefreshMessage { return Err(FsDkrError::ModuliTooSmall { party_index: join_message.get_party_index()?, moduli_size: n_length, - }) + }); } // if the proof checks, we add the new paillier public key to the @@ -512,13 +512,13 @@ impl RefreshMessage { for i in 0..refresh_messages.len() + join_messages.len() { local_key.pk_vec.insert( i, - refresh_messages[0].points_committed_vec[i].clone() * - li_vec[0].clone(), + refresh_messages[0].points_committed_vec[i].clone() + * li_vec[0].clone(), ); for j in 1..current_t as usize + 1 { - local_key.pk_vec[i] = local_key.pk_vec[i].clone() + - refresh_messages[j].points_committed_vec[i].clone() * - li_vec[j].clone(); + local_key.pk_vec[i] = local_key.pk_vec[i].clone() + + refresh_messages[j].points_committed_vec[i].clone() + * li_vec[j].clone(); } } diff --git a/fs-dkr/src/ring_pedersen_proof.rs b/fs-dkr/src/ring_pedersen_proof.rs index 013f118d..ccb81fd3 100644 --- a/fs-dkr/src/ring_pedersen_proof.rs +++ b/fs-dkr/src/ring_pedersen_proof.rs @@ -107,7 +107,11 @@ impl RingPedersenProof { let mut Z = [(); M].map(|_| BigInt::zero()); for i in 0..M { - let e_i = if bitwise_e[i] { BigInt::one() } else { BigInt::zero() }; + let e_i = if bitwise_e[i] { + BigInt::one() + } else { + BigInt::zero() + }; let z_i = BigInt::mod_add( &a[i], &(e_i * &witness.lambda), @@ -116,7 +120,11 @@ impl RingPedersenProof { Z[i] = z_i; } - Self { A: A.to_vec(), Z: Z.to_vec(), phantom: PhantomData } + Self { + A: A.to_vec(), + Z: Z.to_vec(), + phantom: PhantomData, + } } pub fn verify( @@ -137,8 +145,8 @@ impl RingPedersenProof { e_i = 1; } - if BigInt::mod_pow(&statement.T, &proof.Z[i], &statement.N) == - BigInt::mod_mul( + if BigInt::mod_pow(&statement.T, &proof.Z[i], &statement.N) + == BigInt::mod_mul( &proof.A[i], &BigInt::mod_pow( &statement.S, @@ -148,9 +156,9 @@ impl RingPedersenProof { &statement.N, ) { - continue + continue; } else { - return Err(FsDkrError::RingPedersenProofError) + return Err(FsDkrError::RingPedersenProofError); } } Ok(()) diff --git a/fs-dkr/src/test.rs b/fs-dkr/src/test.rs index ab0e5568..6cd028f9 100644 --- a/fs-dkr/src/test.rs +++ b/fs-dkr/src/test.rs @@ -46,11 +46,15 @@ mod tests { .map(|i| old_keys[i].keys_linear.x_i.clone()) .collect(); - let new_linear_secret_key: Vec<_> = - (0..keys.len()).map(|i| keys[i].keys_linear.x_i.clone()).collect(); + let new_linear_secret_key: Vec<_> = (0..keys.len()) + .map(|i| keys[i].keys_linear.x_i.clone()) + .collect(); let indices: Vec<_> = (0..(t + 1) as u16).collect(); let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, + parameters: ShamirSecretSharing { + threshold: t, + share_count: n, + }, commitments: Vec::new(), proof: DLogProof::::prove( &Scalar::random(), @@ -127,8 +131,10 @@ mod tests { keys: &mut [LocalKey], old_to_new_map: &HashMap, join_messages: &[JoinMessage], - ) -> (Vec>, Vec) - { + ) -> ( + Vec>, + Vec, + ) { let new_n = (&keys.len() + join_messages.len()) as u16; keys.iter_mut() .map(|key| { @@ -236,11 +242,15 @@ mod tests { .map(|i| all_keys[i].keys_linear.x_i.clone()) .collect(); - let new_linear_secret_key: Vec<_> = - (0..keys.len()).map(|i| keys[i].keys_linear.x_i.clone()).collect(); + let new_linear_secret_key: Vec<_> = (0..keys.len()) + .map(|i| keys[i].keys_linear.x_i.clone()) + .collect(); let indices: Vec<_> = (0..(t + 1) as u16).collect(); let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, + parameters: ShamirSecretSharing { + threshold: t, + share_count: n, + }, commitments: Vec::new(), proof: DLogProof::::prove( &Scalar::random(), @@ -338,7 +348,7 @@ mod tests { .remove_party_indices .contains(&(*party_index as u16)) { - continue + continue; } refresh_bucket.push(refresh_message.clone()); } @@ -354,7 +364,7 @@ mod tests { // keys will be updated to refreshed values for (party, key) in party_key.iter_mut() { if remove_party_indices.contains(&(*party as u16)) { - continue + continue; } RefreshMessage::collect( @@ -382,7 +392,10 @@ mod tests { fn simulate_dkr( keys: &mut Vec>, new_t_option: Option, - ) -> (Vec>, Vec) { + ) -> ( + Vec>, + Vec, + ) { let mut broadcast_vec: Vec> = Vec::new(); let mut new_dks: Vec = Vec::new(); diff --git a/fs-dkr/src/zk_pdl_with_slack.rs b/fs-dkr/src/zk_pdl_with_slack.rs index a19c6d45..b70f0ae6 100644 --- a/fs-dkr/src/zk_pdl_with_slack.rs +++ b/fs-dkr/src/zk_pdl_with_slack.rs @@ -107,7 +107,16 @@ impl PDLwSlackProof { ); let s3 = &e * rho + gamma; - PDLwSlackProof { z, u1, u2, u3, s1, s2, s3, _phantom: PhantomData } + PDLwSlackProof { + z, + u1, + u2, + u3, + s1, + s2, + s3, + _phantom: PhantomData, + } } pub fn verify(&self, statement: &PDLwSlackStatement) -> FsDkrResult<()> { diff --git a/multi-party-ecdsa/examples/common.rs b/multi-party-ecdsa/examples/common.rs index d21a3f92..159892da 100644 --- a/multi-party-ecdsa/examples/common.rs +++ b/multi-party-ecdsa/examples/common.rs @@ -60,10 +60,14 @@ pub fn aes_encrypt(key: &[u8], plaintext: &[u8]) -> AEAD { OsRng.fill_bytes(&mut nonce); let nonce = Nonce::from_slice(&nonce); - let ciphertext = - cipher.encrypt(nonce, plaintext).expect("encryption failure!"); + let ciphertext = cipher + .encrypt(nonce, plaintext) + .expect("encryption failure!"); - AEAD { ciphertext, tag: nonce.to_vec() } + AEAD { + ciphertext, + tag: nonce.to_vec(), + } } #[allow(dead_code)] @@ -86,10 +90,13 @@ where let retries = 3; let retry_delay = time::Duration::from_millis(250); for _i in 1..retries { - let res = client.post(&format!("{}/{}", addr, path)).json(&body).send(); + let res = client + .post(&format!("{}/{}", addr, path)) + .json(&body) + .send(); if let Ok(mut res) = res { - return Some(res.text().unwrap()) + return Some(res.text().unwrap()); } thread::sleep(retry_delay); } @@ -151,7 +158,7 @@ pub fn poll_for_broadcasts( "[{:?}] party {:?} => party {:?}", round, i, party_num ); - break + break; } } } @@ -184,7 +191,7 @@ pub fn poll_for_p2p( "[{:?}] party {:?} => party {:?}", round, i, party_num ); - break + break; } } } diff --git a/multi-party-ecdsa/examples/gg20_keygen.rs b/multi-party-ecdsa/examples/gg20_keygen.rs index f89361b4..63f0133c 100644 --- a/multi-party-ecdsa/examples/gg20_keygen.rs +++ b/multi-party-ecdsa/examples/gg20_keygen.rs @@ -46,10 +46,12 @@ async fn main() -> Result<()> { let keygen = Keygen::new(args.index, args.threshold, args.number_of_parties)?; - let output = - AsyncProtocol::new(keygen, incoming, outgoing).run().await.map_err( - |e| anyhow!("protocol execution terminated with error: {}", e), - )?; + let output = AsyncProtocol::new(keygen, incoming, outgoing) + .run() + .await + .map_err(|e| { + anyhow!("protocol execution terminated with error: {}", e) + })?; let output = serde_json::to_vec_pretty(&output).context("serialize output")?; tokio::io::copy(&mut output.as_slice(), &mut output_file) diff --git a/multi-party-ecdsa/examples/gg20_signing.rs b/multi-party-ecdsa/examples/gg20_signing.rs index ba8b6f97..03434b14 100644 --- a/multi-party-ecdsa/examples/gg20_signing.rs +++ b/multi-party-ecdsa/examples/gg20_signing.rs @@ -53,9 +53,12 @@ async fn main() -> Result<()> { let signing = OfflineStage::new(i, args.parties, local_share)?; let completed_offline_stage = - AsyncProtocol::new(signing, incoming, outgoing).run().await.map_err( - |e| anyhow!("protocol execution terminated with error: {}", e), - )?; + AsyncProtocol::new(signing, incoming, outgoing) + .run() + .await + .map_err(|e| { + anyhow!("protocol execution terminated with error: {}", e) + })?; let (i, incoming, outgoing) = join_computation(args.address, &format!("{}-online", args.room)) @@ -71,7 +74,11 @@ async fn main() -> Result<()> { )?; outgoing - .send(Msg { sender: i, receiver: None, body: partial_signature }) + .send(Msg { + sender: i, + receiver: None, + body: partial_signature, + }) .await?; let partial_signatures: Vec<_> = incoming @@ -79,8 +86,9 @@ async fn main() -> Result<()> { .map_ok(|msg| msg.body) .try_collect() .await?; - let signature = - signing.complete(&partial_signatures).context("online stage failed")?; + let signature = signing + .complete(&partial_signatures) + .context("online stage failed")?; let signature = serde_json::to_string(&signature).context("serialize signature")?; println!("{}", signature); diff --git a/multi-party-ecdsa/examples/gg20_sm_client.rs b/multi-party-ecdsa/examples/gg20_sm_client.rs index c42b6e9c..c440d2af 100644 --- a/multi-party-ecdsa/examples/gg20_sm_client.rs +++ b/multi-party-ecdsa/examples/gg20_sm_client.rs @@ -34,8 +34,8 @@ where // Ignore incoming messages addressed to someone else let incoming = incoming.try_filter(move |msg| { futures::future::ready( - msg.sender != index && - (msg.receiver.is_none() || msg.receiver == Some(index)), + msg.sender != index + && (msg.receiver.is_none() || msg.receiver == Some(index)), ) }); @@ -44,7 +44,10 @@ where futures::sink::unfold(client, |client, message: Msg| async move { let serialized = serde_json::to_string(&message).context("serialize message")?; - client.broadcast(&serialized).await.context("broadcast message")?; + client + .broadcast(&serialized) + .await + .context("broadcast message")?; Ok::<_, anyhow::Error>(client) }); @@ -60,7 +63,9 @@ impl SmClient { let config = surf::Config::new() .set_base_url(address.join(&format!("rooms/{}/", room_id))?) .set_timeout(None); - Ok(Self { http_client: config.try_into()? }) + Ok(Self { + http_client: config.try_into()?, + }) } pub async fn issue_index(&self) -> Result { @@ -100,7 +105,7 @@ impl SmClient { Ok(_) => { // ignore other types of events None - }, + } Err(e) => Some(Err(e.into_inner())), } })) @@ -139,19 +144,21 @@ async fn main() -> Result<()> { let client = SmClient::new(args.address, &args.room).context("create SmClient")?; match args.cmd { - Cmd::Broadcast { message } => - client.broadcast(&message).await.context("broadcast message")?, + Cmd::Broadcast { message } => client + .broadcast(&message) + .await + .context("broadcast message")?, Cmd::IssueIdx => { let index = client.issue_index().await.context("issue index")?; println!("Index: {}", index); - }, + } Cmd::Subscribe => { let messages = client.subscribe().await.context("subsribe")?; tokio::pin!(messages); while let Some(message) = messages.next().await { println!("{:?}", message); } - }, + } } Ok(()) } diff --git a/multi-party-ecdsa/examples/gg20_sm_manager.rs b/multi-party-ecdsa/examples/gg20_sm_manager.rs index 3282f2b0..94cc46c4 100644 --- a/multi-party-ecdsa/examples/gg20_sm_manager.rs +++ b/multi-party-ecdsa/examples/gg20_sm_manager.rs @@ -67,7 +67,9 @@ struct Room { impl Db { pub fn empty() -> Self { - Self { rooms: RwLock::new(HashMap::new()) } + Self { + rooms: RwLock::new(HashMap::new()), + } } pub async fn get_room_or_create_empty(&self, room_id: &str) -> Arc { @@ -75,22 +77,24 @@ impl Db { if let Some(room) = rooms.get(room_id) { // If no one is watching this room - we need to clean it up first if !room.is_abandoned() { - return room.clone() + return room.clone(); } } drop(rooms); let mut rooms = self.rooms.write().await; match rooms.entry(room_id.to_owned()) { - Entry::Occupied(entry) if !entry.get().is_abandoned() => - entry.get().clone(), + Entry::Occupied(entry) if !entry.get().is_abandoned() => { + entry.get().clone() + } Entry::Occupied(entry) => { let room = Arc::new(Room::empty()); *entry.into_mut() = room.clone(); room - }, - Entry::Vacant(entry) => - entry.insert(Arc::new(Room::empty())).clone(), + } + Entry::Vacant(entry) => { + entry.insert(Arc::new(Room::empty())).clone() + } } } } @@ -143,7 +147,7 @@ impl Subscription { if let Some(msg) = history.get(usize::from(self.next_event)) { let event_id = self.next_event; self.next_event = event_id + 1; - return (event_id, msg.clone()) + return (event_id, msg.clone()); } let notification = self.room.message_appeared.notified(); drop(history); @@ -173,8 +177,9 @@ impl<'r> FromRequest<'r> for LastEventId { .get_one("Last-Event-ID") .map(|id| id.parse::()); match header { - Some(Ok(last_seen_msg)) => - Outcome::Success(LastEventId(Some(last_seen_msg))), + Some(Ok(last_seen_msg)) => { + Outcome::Success(LastEventId(Some(last_seen_msg))) + } Some(Err(_parse_err)) => Outcome::Failure(( Status::BadRequest, "last seen msg id is not valid", diff --git a/multi-party-ecdsa/src/gg_2020/blame.rs b/multi-party-ecdsa/src/gg_2020/blame.rs index 71db7b37..1312596c 100644 --- a/multi-party-ecdsa/src/gg_2020/blame.rs +++ b/multi-party-ecdsa/src/gg_2020/blame.rs @@ -396,8 +396,8 @@ impl GlobalStatePhase6 { .map(|i| { let g_wi_ki = &self.g_w_vec[i] * &self.k_vec[i]; let sum = self.miu_vec[i].iter().fold(g_wi_ki, |acc, x| { - acc + (Point::generator() * - &Scalar::::from(x)) + acc + (Point::generator() + * &Scalar::::from(x)) }); sum }) diff --git a/multi-party-ecdsa/src/gg_2020/party_i.rs b/multi-party-ecdsa/src/gg_2020/party_i.rs index 56cc8151..718ccd23 100644 --- a/multi-party-ecdsa/src/gg_2020/party_i.rs +++ b/multi-party-ecdsa/src/gg_2020/party_i.rs @@ -262,8 +262,10 @@ impl Keys { composite_dlog_proof_base_h1, composite_dlog_proof_base_h2, }; - let decom1 = - KeyGenDecommitMessage1 { blind_factor, y_i: self.y_i.clone() }; + let decom1 = KeyGenDecommitMessage1 { + blind_factor, + y_i: self.y_i.clone(), + }; (bcm1, decom1) } @@ -405,8 +407,8 @@ impl Keys { &secret_shares_vec[i], index.try_into().unwrap(), ) - .is_ok() && - vss_scheme_vec[i].commitments[0] == y_vec[i]; + .is_ok() + && vss_scheme_vec[i].commitments[0] == y_vec[i]; if !res { bad_actors_vec.push(i); false @@ -516,7 +518,11 @@ impl Keys { impl PartyPrivate { pub fn set_private(key: Keys, shared_key: SharedKeys) -> Self { - Self { u_i: key.u_i, x_i: shared_key.x_i, dk: key.dk } + Self { + u_i: key.u_i, + x_i: shared_key.x_i, + dk: key.dk, + } } pub fn y_i(&self) -> Point { @@ -647,7 +653,13 @@ impl SignKeys { let gamma_i = Scalar::::random(); let g_gamma_i = g * &gamma_i; let k_i = Scalar::::random(); - Self { w_i, g_w_i, k_i, gamma_i, g_gamma_i } + Self { + w_i, + g_w_i, + k_i, + gamma_i, + g_gamma_i, + } } pub fn phase1_broadcast( @@ -701,8 +713,11 @@ impl SignKeys { pub fn phase3_compute_t_i( sigma_i: &Scalar, - ) -> (Point, Scalar, PedersenProof) - { + ) -> ( + Point, + Scalar, + PedersenProof, + ) { let g_sigma_i = Point::generator() * sigma_i; let l = Scalar::::random(); let h_l = Point::::base_point2() * &l; @@ -791,8 +806,10 @@ impl LocalSignature { N_tilde: dlog_statement.N.clone(), }; - let pdl_w_slack_witness = - PDLwSlackWitness { x: k_i.clone(), r: k_enc_randomness.clone() }; + let pdl_w_slack_witness = PDLwSlackWitness { + x: k_i.clone(), + r: k_enc_randomness.clone(), + }; PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement) } @@ -836,7 +853,7 @@ impl LocalSignature { }) .all(|x| x); if proofs_verification { - return Ok(()) + return Ok(()); } } @@ -854,8 +871,8 @@ impl LocalSignature { let sum = R_dash_vec .iter() .fold(Point::generator().to_point(), |acc, x| acc + x); - match sum - &Point::generator().to_point() == - Point::generator().to_point() + match sum - &Point::generator().to_point() + == Point::generator().to_point() { true => Ok(()), false => Err(Phase5BadSum), @@ -876,7 +893,10 @@ impl LocalSignature { D: T.clone(), E: S.clone(), }; - let witness = HomoElGamalWitness { x: l.clone(), r: sigma.clone() }; + let witness = HomoElGamalWitness { + x: l.clone(), + r: sigma.clone(), + }; let proof = HomoELGamalProof::prove(&witness, &delta); (S, proof) @@ -913,7 +933,7 @@ impl LocalSignature { data: Vec::new(), }; Err(err_type) - }, + } } } @@ -921,8 +941,9 @@ impl LocalSignature { pubkey_y: &Point, S_vec: &[Point], ) -> Result<(), Error> { - let sum_plus_g = - S_vec.iter().fold(Point::generator().to_point(), |acc, x| acc + x); + let sum_plus_g = S_vec + .iter() + .fold(Point::generator().to_point(), |acc, x| acc + x); let sum = sum_plus_g - &Point::generator().to_point(); match &sum == pubkey_y { @@ -940,10 +961,18 @@ impl LocalSignature { ) -> Self { let m_fe = Scalar::::from(message); let r = Scalar::::from( - &R.x_coord().unwrap().mod_floor(Scalar::::group_order()), + &R.x_coord() + .unwrap() + .mod_floor(Scalar::::group_order()), ); let s_i = m_fe * k_i + &r * sigma_i; - Self { r, R: R.clone(), s_i, m: message.clone(), y: pubkey.clone() } + Self { + r, + R: R.clone(), + s_i, + m: message.clone(), + y: pubkey.clone(), + } } pub fn output_signature( @@ -1004,8 +1033,8 @@ pub fn verify( let yu2 = y * &u2; // can be faster using shamir trick - if sig.r == - Scalar::::from( + if sig.r + == Scalar::::from( &(gu1 + yu2) .x_coord() .unwrap() diff --git a/multi-party-ecdsa/src/gg_2020/state_machine/keygen.rs b/multi-party-ecdsa/src/gg_2020/state_machine/keygen.rs index 40ab9274..4810e635 100644 --- a/multi-party-ecdsa/src/gg_2020/state_machine/keygen.rs +++ b/multi-party-ecdsa/src/gg_2020/state_machine/keygen.rs @@ -58,13 +58,13 @@ impl Keygen { /// * `i` is not in range `[1; n]`, returns [Error::InvalidPartyIndex] pub fn new(i: u16, t: u16, n: u16) -> Result { if n < 2 { - return Err(Error::TooFewParties) + return Err(Error::TooFewParties); } if t == 0 || t >= n { - return Err(Error::InvalidThreshold) + return Err(Error::InvalidThreshold); } if i == 0 || i > n { - return Err(Error::InvalidPartyIndex) + return Err(Error::InvalidPartyIndex); } let mut state = Self { round: R::Round0(Round0 { party_i: i, t, n }), @@ -111,14 +111,14 @@ impl Keygen { .map(R::Round1) .map_err(Error::ProceedRound)?; true - }, + } s @ R::Round0(_) => { next_state = s; false - }, + } R::Round1(round) - if !store1_wants_more && - (!round.is_expensive() || may_block) => + if !store1_wants_more + && (!round.is_expensive() || may_block) => { let store = self.msgs1.take().ok_or(InternalError::StoreGone)?; @@ -130,14 +130,14 @@ impl Keygen { .map(R::Round2) .map_err(Error::ProceedRound)?; true - }, + } s @ R::Round1(_) => { next_state = s; false - }, + } R::Round2(round) - if !store2_wants_more && - (!round.is_expensive() || may_block) => + if !store2_wants_more + && (!round.is_expensive() || may_block) => { let store = self.msgs2.take().ok_or(InternalError::StoreGone)?; @@ -149,14 +149,14 @@ impl Keygen { .map(R::Round3) .map_err(Error::ProceedRound)?; true - }, + } s @ R::Round2(_) => { next_state = s; false - }, + } R::Round3(round) - if !store3_wants_more && - (!round.is_expensive() || may_block) => + if !store3_wants_more + && (!round.is_expensive() || may_block) => { let store = self.msgs3.take().ok_or(InternalError::StoreGone)?; @@ -168,14 +168,14 @@ impl Keygen { .map(R::Round4) .map_err(Error::ProceedRound)?; true - }, + } s @ R::Round3(_) => { next_state = s; false - }, + } R::Round4(round) - if !store4_wants_more && - (!round.is_expensive() || may_block) => + if !store4_wants_more + && (!round.is_expensive() || may_block) => { let store = self.msgs4.take().ok_or(InternalError::StoreGone)?; @@ -187,15 +187,15 @@ impl Keygen { .map(R::Final) .map_err(Error::ProceedRound)?; true - }, + } s @ R::Round4(_) => { next_state = s; false - }, + } s @ R::Final(_) | s @ R::Gone => { next_state = s; false - }, + } }; self.round = next_state; @@ -231,7 +231,7 @@ impl StateMachine for Keygen { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } ProtocolMessage(M::Round2(m)) => { let store = self.msgs2.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -247,7 +247,7 @@ impl StateMachine for Keygen { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } ProtocolMessage(M::Round3(m)) => { let store = self.msgs3.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -263,7 +263,7 @@ impl StateMachine for Keygen { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } ProtocolMessage(M::Round4(m)) => { let store = self.msgs4.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -279,7 +279,7 @@ impl StateMachine for Keygen { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } } } @@ -529,16 +529,16 @@ impl IsCritical for Error { // protocol and don't indicate a bug in the library. Error::HandleMessage(e) => !matches!( e, - StoreErr::MsgOverwrite | - StoreErr::NotForMe | - StoreErr::WantsMoreMessages + StoreErr::MsgOverwrite + | StoreErr::NotForMe + | StoreErr::WantsMoreMessages ), Error::ReceivedOutOfOrderMessage { .. } => false, - Error::DoublePickOutput | - Error::TooFewParties | - Error::InvalidThreshold | - Error::InvalidPartyIndex | - Error::InternalError(_) => true, + Error::DoublePickOutput + | Error::TooFewParties + | Error::InvalidThreshold + | Error::InvalidPartyIndex + | Error::InternalError(_) => true, } } } diff --git a/multi-party-ecdsa/src/gg_2020/state_machine/keygen/rounds.rs b/multi-party-ecdsa/src/gg_2020/state_machine/keygen/rounds.rs index bb6ce65b..66c4486c 100644 --- a/multi-party-ecdsa/src/gg_2020/state_machine/keygen/rounds.rs +++ b/multi-party-ecdsa/src/gg_2020/state_machine/keygen/rounds.rs @@ -142,7 +142,7 @@ impl Round2 { for (i, share) in vss_result.1.iter().enumerate() { if i + 1 == usize::from(self.party_i) { - continue + continue; } let enc_key_for_recipient = &self.received_comm[i].e; diff --git a/multi-party-ecdsa/src/gg_2020/state_machine/sign.rs b/multi-party-ecdsa/src/gg_2020/state_machine/sign.rs index 03d9285c..f77c1a9b 100644 --- a/multi-party-ecdsa/src/gg_2020/state_machine/sign.rs +++ b/multi-party-ecdsa/src/gg_2020/state_machine/sign.rs @@ -90,15 +90,15 @@ impl OfflineStage { local_key: LocalKey, ) -> Result { if s_l.len() < 2 { - return Err(Error::TooFewParties) + return Err(Error::TooFewParties); } if i == 0 || usize::from(i) > s_l.len() { - return Err(Error::InvalidPartyIndex) + return Err(Error::InvalidPartyIndex); } let keygen_n = local_key.n; if s_l.iter().any(|&i| i == 0 || i > keygen_n) { - return Err(Error::InvalidSl) + return Err(Error::InvalidSl); } { // Check if s_l has duplicates @@ -108,7 +108,7 @@ impl OfflineStage { s_l_sorted_deduped.dedup(); if s_l_sorted != s_l_sorted_deduped { - return Err(Error::InvalidSl) + return Err(Error::InvalidSl); } } @@ -159,14 +159,14 @@ impl OfflineStage { .map(OfflineR::R1) .map_err(Error::ProceedRound)?; true - }, + } s @ OfflineR::R0(_) => { next_state = s; false - }, + } OfflineR::R1(round) - if !store1_wants_more && - (!round.is_expensive() || may_block) => + if !store1_wants_more + && (!round.is_expensive() || may_block) => { let store = self.msgs1.take().ok_or(InternalError::StoreGone)?; @@ -178,14 +178,14 @@ impl OfflineStage { .map(OfflineR::R2) .map_err(Error::ProceedRound)?; true - }, + } s @ OfflineR::R1(_) => { next_state = s; false - }, + } OfflineR::R2(round) - if !store2_wants_more && - (!round.is_expensive() || may_block) => + if !store2_wants_more + && (!round.is_expensive() || may_block) => { let store = self.msgs2.take().ok_or(InternalError::StoreGone)?; @@ -197,14 +197,14 @@ impl OfflineStage { .map(OfflineR::R3) .map_err(Error::ProceedRound)?; true - }, + } s @ OfflineR::R2(_) => { next_state = s; false - }, + } OfflineR::R3(round) - if !store3_wants_more && - (!round.is_expensive() || may_block) => + if !store3_wants_more + && (!round.is_expensive() || may_block) => { let store = self.msgs3.take().ok_or(InternalError::StoreGone)?; @@ -216,14 +216,14 @@ impl OfflineStage { .map(OfflineR::R4) .map_err(Error::ProceedRound)?; true - }, + } s @ OfflineR::R3(_) => { next_state = s; false - }, + } OfflineR::R4(round) - if !store4_wants_more && - (!round.is_expensive() || may_block) => + if !store4_wants_more + && (!round.is_expensive() || may_block) => { let store = self.msgs4.take().ok_or(InternalError::StoreGone)?; @@ -235,14 +235,14 @@ impl OfflineStage { .map(OfflineR::R5) .map_err(Error::ProceedRound)?; false - }, + } s @ OfflineR::R4(_) => { next_state = s; false - }, + } OfflineR::R5(round) - if !store5_wants_more && - (!round.is_expensive() || may_block) => + if !store5_wants_more + && (!round.is_expensive() || may_block) => { let store = self.msgs5.take().ok_or(InternalError::StoreGone)?; @@ -254,14 +254,14 @@ impl OfflineStage { .map(OfflineR::R6) .map_err(Error::ProceedRound)?; false - }, + } s @ OfflineR::R5(_) => { next_state = s; false - }, + } OfflineR::R6(round) - if !store6_wants_more && - (!round.is_expensive() || may_block) => + if !store6_wants_more + && (!round.is_expensive() || may_block) => { let store = self.msgs6.take().ok_or(InternalError::StoreGone)?; @@ -273,15 +273,15 @@ impl OfflineStage { .map(OfflineR::Finished) .map_err(Error::ProceedRound)?; false - }, + } s @ OfflineR::R6(_) => { next_state = s; false - }, + } s @ OfflineR::Finished(_) | s @ OfflineR::Gone => { next_state = s; false - }, + } }; self.round = next_state; @@ -319,7 +319,7 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - }, + } OfflineProtocolMessage(OfflineM::M2(m)) => { let store = self.msgs2.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -334,7 +334,7 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - }, + } OfflineProtocolMessage(OfflineM::M3(m)) => { let store = self.msgs3.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -349,7 +349,7 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - }, + } OfflineProtocolMessage(OfflineM::M4(m)) => { let store = self.msgs4.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -364,7 +364,7 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - }, + } OfflineProtocolMessage(OfflineM::M5(m)) => { let store = self.msgs5.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -379,7 +379,7 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - }, + } OfflineProtocolMessage(OfflineM::M6(m)) => { let store = self.msgs6.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -394,7 +394,7 @@ impl StateMachine for OfflineStage { body: m, }) .map_err(Error::HandleMessage)?; - }, + } } self.proceed_round(false) } @@ -712,7 +712,9 @@ impl SignManual { self, sigs: &[PartialSignature], ) -> Result { - self.state.proceed_manual(sigs).map_err(SignError::CompleteSigning) + self.state + .proceed_manual(sigs) + .map_err(SignError::CompleteSigning) } } diff --git a/multi-party-ecdsa/src/gg_2020/state_machine/sign/fmt.rs b/multi-party-ecdsa/src/gg_2020/state_machine/sign/fmt.rs index ba6e3e4c..c8bf31db 100644 --- a/multi-party-ecdsa/src/gg_2020/state_machine/sign/fmt.rs +++ b/multi-party-ecdsa/src/gg_2020/state_machine/sign/fmt.rs @@ -43,7 +43,9 @@ impl From<&super::OfflineStage> for OfflineStageProgress { round4_msgs: ReceivedMessages::from_broadcast(state.msgs4.as_ref()), round5_msgs: ReceivedMessages::from_broadcast(state.msgs5.as_ref()), - msgs_queue: OutgoingMessages { len: state.msgs_queue.0.len() }, + msgs_queue: OutgoingMessages { + len: state.msgs_queue.0.len(), + }, } } } @@ -112,7 +114,7 @@ impl fmt::Debug for ReceivedMessages { container.total - container.waiting_for.len(), container.total ) - }, + } None => write!(f, "[gone]"), } } diff --git a/multi-party-ecdsa/src/gg_2020/state_machine/sign/rounds.rs b/multi-party-ecdsa/src/gg_2020/state_machine/sign/rounds.rs index 9b1d467c..fd66bd79 100644 --- a/multi-party-ecdsa/src/gg_2020/state_machine/sign/rounds.rs +++ b/multi-party-ecdsa/src/gg_2020/state_machine/sign/rounds.rs @@ -84,7 +84,11 @@ impl Round0 { &self.local_key.keys_linear.x_i, &self.local_key.vss_scheme.clone(), usize::from(self.s_l[usize::from(self.i - 1)]) - 1, - &self.s_l.iter().map(|&i| usize::from(i) - 1).collect::>(), + &self + .s_l + .iter() + .map(|&i| usize::from(i) - 1) + .collect::>(), ); let (bc1, decom1) = sign_keys.phase1_broadcast(); @@ -154,8 +158,12 @@ impl Round1 { let mut ni_vec = Vec::new(); let ttag = self.s_l.len(); - let l_s: Vec<_> = - self.s_l.iter().cloned().map(|i| usize::from(i) - 1).collect(); + let l_s: Vec<_> = self + .s_l + .iter() + .cloned() + .map(|i| usize::from(i) - 1) + .collect(); let i = usize::from(self.i - 1); for j in 0..ttag - 1 { let ind = if j < i { j } else { j + 1 }; @@ -267,8 +275,12 @@ impl Round2 { let ttag = self.s_l.len(); let index = usize::from(self.i) - 1; - let l_s: Vec<_> = - self.s_l.iter().cloned().map(|i| usize::from(i) - 1).collect(); + let l_s: Vec<_> = self + .s_l + .iter() + .cloned() + .map(|i| usize::from(i) - 1) + .collect(); let g_w_vec = SignKeys::g_w_vec( &self.local_key.pk_vec[..], &l_s[..], @@ -484,8 +496,12 @@ impl Round4 { // each party sends first message to all other parties let mut phase5_proofs_vec = Vec::new(); - let l_s: Vec<_> = - self.s_l.iter().cloned().map(|i| usize::from(i) - 1).collect(); + let l_s: Vec<_> = self + .s_l + .iter() + .cloned() + .map(|i| usize::from(i) - 1) + .collect(); let index = usize::from(self.i - 1); for j in 0..ttag - 1 { let ind = if j < index { j } else { j + 1 }; @@ -566,8 +582,12 @@ impl Round5 { .map(|(r_dash, pdl_proof)| (r_dash.0, pdl_proof)) .unzip(); - let l_s: Vec<_> = - self.s_l.iter().cloned().map(|i| usize::from(i) - 1).collect(); + let l_s: Vec<_> = self + .s_l + .iter() + .cloned() + .map(|i| usize::from(i) - 1) + .collect(); let ttag = self.s_l.len(); for i in 0..ttag { LocalSignature::phase5_verify_pdl( @@ -730,7 +750,9 @@ impl Round7 { sigs: &[PartialSignature], ) -> Result { let sigs = sigs.iter().map(|s_i| s_i.0.clone()).collect::>(); - self.local_signature.output_signature(&sigs).map_err(Error::Round7) + self.local_signature + .output_signature(&sigs) + .map_err(Error::Round7) } } diff --git a/multi-party-ecdsa/src/gg_2020/test.rs b/multi-party-ecdsa/src/gg_2020/test.rs index 2d516f74..76d370ad 100644 --- a/multi-party-ecdsa/src/gg_2020/test.rs +++ b/multi-party-ecdsa/src/gg_2020/test.rs @@ -171,7 +171,10 @@ fn keygen_t_n_parties( ), ErrorType, > { - let params = Parameters { threshold: t, share_count: n }; + let params = Parameters { + threshold: t, + share_count: n, + }; let (t, n) = (t as usize, n as usize); let party_keys_vec = (0..n).map(Keys::create).collect::>(); @@ -180,8 +183,10 @@ fn keygen_t_n_parties( .map(|k| k.phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2()) .unzip(); - let e_vec = - bc1_vec.iter().map(|bc1| bc1.e.clone()).collect::>(); + let e_vec = bc1_vec + .iter() + .map(|bc1| bc1.e.clone()) + .collect::>(); let h1_h2_N_tilde_vec = bc1_vec .iter() .map(|bc1| bc1.dlog_statement.clone()) @@ -239,7 +244,7 @@ fn keygen_t_n_parties( (&index_vec[i] + 1).into(), ); if res.is_err() { - return Err(res.err().unwrap()) + return Err(res.err().unwrap()); } let (shared_keys, dlog_proof) = res.unwrap(); shared_keys_vec.push(shared_keys); @@ -258,14 +263,15 @@ fn keygen_t_n_parties( ); if dlog_verification.is_err() { - return Err(dlog_verification.err().unwrap()) + return Err(dlog_verification.err().unwrap()); } //test let xi_vec = (0..=t) .map(|i| shared_keys_vec[i].x_i.clone()) .collect::>>(); - let x = - vss_scheme_for_test[0].clone().reconstruct(&index_vec[0..=t], &xi_vec); + let x = vss_scheme_for_test[0] + .clone() + .reconstruct(&index_vec[0..=t], &xi_vec); let sum_u_i = party_keys_vec .iter() .fold(Scalar::::zero(), |acc, x| acc + &x.u_i); @@ -579,7 +585,7 @@ fn sign( i, ); if phase5_verify_zk.is_err() { - return Err(phase5_verify_zk.err().unwrap()) + return Err(phase5_verify_zk.err().unwrap()); } } diff --git a/multi-party-ecdsa/src/utilities/mta/mod.rs b/multi-party-ecdsa/src/utilities/mta/mod.rs index e153e1d8..4c37f2b6 100644 --- a/multi-party-ecdsa/src/utilities/mta/mod.rs +++ b/multi-party-ecdsa/src/utilities/mta/mod.rs @@ -100,7 +100,10 @@ impl MessageA { }) .collect::>(); - Self { c: c_a, range_proofs: alice_range_proofs } + Self { + c: c_a, + range_proofs: alice_range_proofs, + } } } @@ -134,7 +137,7 @@ impl MessageB { dlog_statements: &[DLogStatement], ) -> Result<(Self, Scalar), Error> { if m_a.range_proofs.len() != dlog_statements.len() { - return Err(InvalidKey) + return Err(InvalidKey); } // verify proofs if !m_a @@ -147,7 +150,7 @@ impl MessageB { .all(|x| x) { log::info!("MP-ECDSA : Proof Mismatch"); - return Err(InvalidKey) + return Err(InvalidKey); }; let beta_tag_fe = Scalar::::from(beta_tag); let c_beta_tag = Paillier::encrypt_with_chosen_randomness( @@ -212,9 +215,9 @@ impl MessageB { let g_alpha = g * α let ba_btag = &self.b_proof.pk * a + &self.beta_tag_proof.pk; - if DLogProof::verify(&self.b_proof).is_ok() && - DLogProof::verify(&self.beta_tag_proof).is_ok() && - ba_btag == g_alpha + if DLogProof::verify(&self.b_proof).is_ok() + && DLogProof::verify(&self.beta_tag_proof).is_ok() + && ba_btag == g_alpha { Ok(alpha) } else { diff --git a/multi-party-ecdsa/src/utilities/mta/range_proofs.rs b/multi-party-ecdsa/src/utilities/mta/range_proofs.rs index f0d41cc2..6c9194dd 100644 --- a/multi-party-ecdsa/src/utilities/mta/range_proofs.rs +++ b/multi-party-ecdsa/src/utilities/mta/range_proofs.rs @@ -53,16 +53,24 @@ impl AliceZkpRound1 { let beta = BigInt::from_paillier_key(alice_ek); let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); let ro = BigInt::sample_below(&(q * N_tilde)); - let z = (BigInt::mod_pow(h1, a, N_tilde) * - BigInt::mod_pow(h2, &ro, N_tilde)) % - N_tilde; - let u = ((alpha.borrow() * &alice_ek.n + 1) * - BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % - &alice_ek.nn; - let w = (BigInt::mod_pow(h1, &alpha, N_tilde) * - BigInt::mod_pow(h2, &gamma, N_tilde)) % - N_tilde; - Self { alpha, beta, gamma, ro, z, u, w } + let z = (BigInt::mod_pow(h1, a, N_tilde) + * BigInt::mod_pow(h2, &ro, N_tilde)) + % N_tilde; + let u = ((alpha.borrow() * &alice_ek.n + 1) + * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) + % &alice_ek.nn; + let w = (BigInt::mod_pow(h1, &alpha, N_tilde) + * BigInt::mod_pow(h2, &gamma, N_tilde)) + % N_tilde; + Self { + alpha, + beta, + gamma, + ro, + z, + u, + w, + } } } @@ -82,8 +90,8 @@ impl AliceZkpRound2 { r: &BigInt, ) -> Self { Self { - s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) % - &alice_ek.n, + s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) + % &alice_ek.n, s1: (e * a) + round1.alpha.borrow(), s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), } @@ -116,7 +124,7 @@ impl AliceProof { let Gen = alice_ek.n.borrow() + 1; if self.s1 > Scalar::::group_order().pow(3) { - return false + return false; } let z_e_inv = BigInt::mod_inv( @@ -129,10 +137,10 @@ impl AliceProof { Some(c) => c, }; - let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) * - BigInt::mod_pow(h2, &self.s2, N_tilde) * - z_e_inv) % - N_tilde; + let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) + * BigInt::mod_pow(h2, &self.s2, N_tilde) + * z_e_inv) + % N_tilde; let gs1 = (self.s1.borrow() * N + 1) % NN; let cipher_e_inv = @@ -153,7 +161,7 @@ impl AliceProof { .chain_bigint(&w) .result_bigint(); if e != self.e { - return false + return false; } true @@ -239,23 +247,36 @@ impl BobZkpRound1 { let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); let sigma = BigInt::sample_below(&(q * N_tilde)); let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * - BigInt::mod_pow(h2, &ro, N_tilde)) % - N_tilde; - let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) * - BigInt::mod_pow(h2, &ro_prim, N_tilde)) % - N_tilde; - let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * - BigInt::mod_pow(h2, &sigma, N_tilde)) % - N_tilde; - let w = (BigInt::mod_pow(h1, &gamma, N_tilde) * - BigInt::mod_pow(h2, &tau, N_tilde)) % - N_tilde; - let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) * - (gamma.borrow() * &alice_ek.n + 1) * - BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % - &alice_ek.nn; - Self { alpha, beta, gamma, ro, ro_prim, sigma, tau, z, z_prim, t, w, v } + let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) + * BigInt::mod_pow(h2, &ro, N_tilde)) + % N_tilde; + let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) + * BigInt::mod_pow(h2, &ro_prim, N_tilde)) + % N_tilde; + let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) + * BigInt::mod_pow(h2, &sigma, N_tilde)) + % N_tilde; + let w = (BigInt::mod_pow(h1, &gamma, N_tilde) + * BigInt::mod_pow(h2, &tau, N_tilde)) + % N_tilde; + let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) + * (gamma.borrow() * &alice_ek.n + 1) + * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) + % &alice_ek.nn; + Self { + alpha, + beta, + gamma, + ro, + ro_prim, + sigma, + tau, + z, + z_prim, + t, + w, + v, + } } } @@ -284,9 +305,9 @@ impl BobZkpRound2 { ) -> Self { let b_bn = b.to_bigint(); Self { - s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * - round1.beta.borrow()) % - &alice_ek.n, + s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) + * round1.beta.borrow()) + % &alice_ek.n, s1: (e * b_bn) + round1.alpha.borrow(), s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), t1: (e * beta_prim) + round1.gamma.borrow(), @@ -331,7 +352,7 @@ impl BobProof { let h2 = &dlog_statement.ni; if self.s1 > Scalar::::group_order().pow(3) { - return false + return false; } let z_e_inv = BigInt::mod_inv( @@ -344,10 +365,10 @@ impl BobProof { Some(c) => c, }; - let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) * - BigInt::mod_pow(h2, &self.s2, N_tilde) * - z_e_inv) % - N_tilde; + let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) + * BigInt::mod_pow(h2, &self.s2, N_tilde) + * z_e_inv) + % N_tilde; let mta_e_inv = BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); @@ -356,11 +377,11 @@ impl BobProof { Some(c) => c, }; - let v = (BigInt::mod_pow(a_enc, &self.s1, NN) * - BigInt::mod_pow(&self.s, N, NN) * - (self.t1.borrow() * N + 1) * - mta_e_inv) % - NN; + let v = (BigInt::mod_pow(a_enc, &self.s1, NN) + * BigInt::mod_pow(&self.s, N, NN) + * (self.t1.borrow() * N + 1) + * mta_e_inv) + % NN; let t_e_inv = BigInt::mod_inv( &BigInt::mod_pow(&self.t, &self.e, N_tilde), @@ -371,10 +392,10 @@ impl BobProof { Some(c) => c, }; - let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) * - BigInt::mod_pow(h2, &self.t2, N_tilde) * - t_e_inv) % - N_tilde; + let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) + * BigInt::mod_pow(h2, &self.t2, N_tilde) + * t_e_inv) + % N_tilde; let Gen = alice_ek.n.borrow() + 1; let mut values_to_hash = vec![ @@ -402,7 +423,7 @@ impl BobProof { .into_iter() .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) .result_bigint() - }, + } None => values_to_hash .into_iter() .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) @@ -410,7 +431,7 @@ impl BobProof { }; if e != self.e { - return false + return false; } true @@ -515,9 +536,12 @@ impl BobProofExt { mta_avc_out, alice_ek, dlog_statement, - Some(&BobCheck { u: self.u.clone(), X: X.clone() }), + Some(&BobCheck { + u: self.u.clone(), + X: X.clone(), + }), ) { - return false + return false; } // fiddle with EC points @@ -529,7 +553,7 @@ impl BobProofExt { }; if x1 != x2 { - return false + return false; } true @@ -548,7 +572,7 @@ impl SampleFromMultiplicativeGroup for BigInt { loop { let r = Self::sample_below(N); if r.gcd(N) == One { - return r + return r; } } } @@ -587,7 +611,10 @@ pub(crate) mod tests { true, ); - BobProofExt { proof: bob_proof, u: u.unwrap() } + BobProofExt { + proof: bob_proof, + u: u.unwrap(), + } } pub(crate) fn generate_init( @@ -606,7 +633,11 @@ pub(crate) mod tests { let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); let (ek, dk) = Paillier::keypair().keys(); - let dlog_statement = DLogStatement { g: h1, ni: h2, N: ek_tilde.n }; + let dlog_statement = DLogStatement { + g: h1, + ni: h2, + N: ek_tilde.n, + }; (dlog_statement, ek, dk) } diff --git a/multi-party-ecdsa/src/utilities/zk_pdl/mod.rs b/multi-party-ecdsa/src/utilities/zk_pdl/mod.rs index cde03ff7..452a3fee 100644 --- a/multi-party-ecdsa/src/utilities/zk_pdl/mod.rs +++ b/multi-party-ecdsa/src/utilities/zk_pdl/mod.rs @@ -184,8 +184,8 @@ impl Verifier { &prover_second_message.decommit.blindness, ); - if prover_first_message.c_hat == c_hat_test && - prover_second_message.decommit.q_hat == state.q_tag + if prover_first_message.c_hat == c_hat_test + && prover_second_message.decommit.q_hat == state.q_tag { Ok(()) } else { @@ -227,8 +227,8 @@ impl Prover { witness: &PDLWitness, state: &PDLProverState, ) -> Result { - let ab_concat = &verifier_second_message.a + - verifier_second_message + let ab_concat = &verifier_second_message.a + + verifier_second_message .b .clone() .shl(verifier_second_message.a.bit_length()); // b|a (in the paper it is a|b) @@ -239,10 +239,12 @@ impl Prover { ); let ax1 = &verifier_second_message.a * witness.x.to_bigint(); let alpha_test = ax1 + &verifier_second_message.b; - if alpha_test == state.alpha && - verifier_first_message.c_tag_tag == c_tag_tag_test + if alpha_test == state.alpha + && verifier_first_message.c_tag_tag == c_tag_tag_test { - Ok(PDLProverSecondMessage { decommit: state.decommit.clone() }) + Ok(PDLProverSecondMessage { + decommit: state.decommit.clone(), + }) } else { Err(ZkPdlError::Message2) } diff --git a/multi-party-ecdsa/src/utilities/zk_pdl/test.rs b/multi-party-ecdsa/src/utilities/zk_pdl/test.rs index 928ca7d6..3e5a223b 100644 --- a/multi-party-ecdsa/src/utilities/zk_pdl/test.rs +++ b/multi-party-ecdsa/src/utilities/zk_pdl/test.rs @@ -32,9 +32,17 @@ fn test_zk_pdl() { ) .0 .into_owned(); - let statement = - PDLStatement { ciphertext: c, ek, Q, G: Point::generator().to_point() }; - let witness = PDLWitness { x, r: randomness.0, dk }; + let statement = PDLStatement { + ciphertext: c, + ek, + Q, + G: Point::generator().to_point(), + }; + let witness = PDLWitness { + x, + r: randomness.0, + dk, + }; // let (verifier_message1, mut verifier_state) = Verifier::message1(&statement); diff --git a/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs index 552757b4..dfa737e1 100644 --- a/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs +++ b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs @@ -129,7 +129,15 @@ impl PDLwSlackProof { ); let s3 = &e * rho + gamma; - PDLwSlackProof { z, u1, u2, u3, s1, s2, s3 } + PDLwSlackProof { + z, + u1, + u2, + u3, + s1, + s2, + s3, + } } pub fn verify( diff --git a/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs index 314288fa..227bb4f3 100644 --- a/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs +++ b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs @@ -24,8 +24,11 @@ fn test_zk_pdl_with_slack() { let xhi = BigInt::sample_below(&S); let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = - DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; + let statement = DLogStatement { + N: ek_tilde.n.clone(), + g: h1.clone(), + ni: h2.clone(), + }; let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); @@ -82,8 +85,11 @@ fn test_zk_pdl_with_slack_soundness() { let xhi = BigInt::sample_below(&S); let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = - DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; + let statement = DLogStatement { + N: ek_tilde.n.clone(), + g: h1.clone(), + ni: h2.clone(), + }; let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index ab3566a0..bebb9cb5 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,4 @@ [toolchain] -#channel = "nightly-2022-05-15" -channel = "nightly-2023-10-01" +channel = "stable" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml index 5a7d47a5..b6825604 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,23 +1,3 @@ # Basic hard_tabs = false -max_width = 80 -use_small_heuristics = "Max" -# Imports -imports_granularity = "Crate" -reorder_imports = true -# Consistency -newline_style = "Unix" -# Format comments -comment_width = 100 -wrap_comments = true -# Misc -chain_width = 80 -spaces_around_ranges = false -binop_separator = "Back" -reorder_impl_items = false -match_arm_leading_pipes = "Preserve" -match_arm_blocks = false -match_block_trailing_comma = true -trailing_comma = "Vertical" -trailing_semicolon = false -use_field_init_shorthand = true \ No newline at end of file +max_width = 80 \ No newline at end of file diff --git a/src/party_i.rs b/src/party_i.rs index dc26302b..f8f2915c 100644 --- a/src/party_i.rs +++ b/src/party_i.rs @@ -261,8 +261,10 @@ impl Keys { composite_dlog_proof_base_h1, composite_dlog_proof_base_h2, }; - let decom1 = - KeyGenDecommitMessage1 { blind_factor, y_i: self.y_i.clone() }; + let decom1 = KeyGenDecommitMessage1 { + blind_factor, + y_i: self.y_i.clone(), + }; (bcm1, decom1) } @@ -273,7 +275,11 @@ impl Keys { decom_vec: &[KeyGenDecommitMessage1], bc1_vec: &[KeyGenBroadcastMessage1], ) -> Result< - (VerifiableSS, Vec>, usize), + ( + VerifiableSS, + Vec>, + usize, + ), ErrorType, > { let mut bad_actors_vec = Vec::new(); @@ -354,8 +360,8 @@ impl Keys { &secret_shares_vec[i], index.try_into().unwrap(), ) - .is_ok() && - vss_scheme_vec[i].commitments[0] == y_vec[i]; + .is_ok() + && vss_scheme_vec[i].commitments[0] == y_vec[i]; if !res { bad_actors_vec.push(i); false @@ -466,7 +472,11 @@ impl Keys { impl PartyPrivate { pub fn set_private(key: Keys, shared_key: SharedKeys) -> Self { - Self { u_i: key.u_i, x_i: shared_key.x_i, dk: key.dk } + Self { + u_i: key.u_i, + x_i: shared_key.x_i, + dk: key.dk, + } } pub fn y_i(&self) -> Point { @@ -598,7 +608,13 @@ impl SignKeys { let gamma_i = Scalar::::random(); let g_gamma_i = g * &gamma_i; let k_i = Scalar::::random(); - Self { w_i, g_w_i, k_i, gamma_i, g_gamma_i } + Self { + w_i, + g_w_i, + k_i, + gamma_i, + g_gamma_i, + } } pub fn phase1_broadcast( @@ -652,8 +668,11 @@ impl SignKeys { pub fn phase3_compute_t_i( sigma_i: &Scalar, - ) -> (Point, Scalar, PedersenProof) - { + ) -> ( + Point, + Scalar, + PedersenProof, + ) { let g_sigma_i = Point::generator() * sigma_i; let l = Scalar::::random(); let h_l = Point::::base_point2() * &l; @@ -742,8 +761,10 @@ impl LocalSignature { N_tilde: dlog_statement.N.clone(), }; - let pdl_w_slack_witness = - PDLwSlackWitness { x: k_i.clone(), r: k_enc_randomness.clone() }; + let pdl_w_slack_witness = PDLwSlackWitness { + x: k_i.clone(), + r: k_enc_randomness.clone(), + }; PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement) } @@ -788,7 +809,7 @@ impl LocalSignature { }) .all(|x| x); if proofs_verification { - return Ok(()) + return Ok(()); } } @@ -806,8 +827,8 @@ impl LocalSignature { let sum = R_dash_vec .iter() .fold(Point::generator().to_point(), |acc, x| acc + x); - match sum - &Point::generator().to_point() == - Point::generator().to_point() + match sum - &Point::generator().to_point() + == Point::generator().to_point() { true => Ok(()), false => Err(Phase5BadSum), @@ -828,7 +849,10 @@ impl LocalSignature { D: T.clone(), E: S.clone(), }; - let witness = HomoElGamalWitness { x: l.clone(), r: sigma.clone() }; + let witness = HomoElGamalWitness { + x: l.clone(), + r: sigma.clone(), + }; let proof = HomoELGamalProof::prove(&witness, &delta); (S, proof) @@ -865,7 +889,7 @@ impl LocalSignature { data: Vec::new(), }; Err(err_type) - }, + } } } @@ -873,8 +897,9 @@ impl LocalSignature { pubkey_y: &Point, S_vec: &[Point], ) -> Result<(), Error> { - let sum_plus_g = - S_vec.iter().fold(Point::generator().to_point(), |acc, x| acc + x); + let sum_plus_g = S_vec + .iter() + .fold(Point::generator().to_point(), |acc, x| acc + x); let sum = sum_plus_g - &Point::generator().to_point(); match &sum == pubkey_y { @@ -892,10 +917,18 @@ impl LocalSignature { ) -> Self { let m_fe = Scalar::::from(message); let r = Scalar::::from( - &R.x_coord().unwrap().mod_floor(Scalar::::group_order()), + &R.x_coord() + .unwrap() + .mod_floor(Scalar::::group_order()), ); let s_i = m_fe * k_i + &r * sigma_i; - Self { r, R: R.clone(), s_i, m: message.clone(), y: pubkey.clone() } + Self { + r, + R: R.clone(), + s_i, + m: message.clone(), + y: pubkey.clone(), + } } pub fn output_signature( @@ -956,8 +989,8 @@ pub fn verify( let yu2 = y * &u2; // can be faster using shamir trick - if sig.r == - Scalar::::from( + if sig.r + == Scalar::::from( &(gu1 + yu2) .x_coord() .unwrap() diff --git a/src/presign/mod.rs b/src/presign/mod.rs index d63d6898..5935d096 100644 --- a/src/presign/mod.rs +++ b/src/presign/mod.rs @@ -46,7 +46,10 @@ pub mod rounds; pub mod state_machine; pub fn DEFAULT_ENCRYPTION_KEY() -> EncryptionKey { - EncryptionKey { n: BigInt::zero(), nn: BigInt::zero() } + EncryptionKey { + n: BigInt::zero(), + nn: BigInt::zero(), + } } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/presign/rounds.rs b/src/presign/rounds.rs index fe2ed53f..ecf7515f 100644 --- a/src/presign/rounds.rs +++ b/src/presign/rounds.rs @@ -238,13 +238,13 @@ impl Round1 { error_type: "enc".to_string(), bad_actors: vec![j.into()], data: bincode::serialize(&error_data).unwrap(), - })) + })); } } // Gamma_i = g^{gamma_i} - let Gamma_i = Point::::generator().as_point() * - Scalar::from_bigint(&self.gamma_i); + let Gamma_i = Point::::generator().as_point() + * Scalar::from_bigint(&self.gamma_i); // {beta, beta_hat, r, r_hat, s, s_hat}_i will store mapping from j to // {beta, beta_hat, r, r_hat, s, s_hat}_i_j. let mut beta_i: HashMap = HashMap::new(); @@ -495,8 +495,8 @@ impl Round1 { Y: F_hat_j_i.clone(), // We use omega_i in place of x_i, see doc on omega_i // definition for explanation. - X: Point::::generator().as_point() * - Scalar::from_bigint(&omega_i), + X: Point::::generator().as_point() + * Scalar::from_bigint(&omega_i), ek_prover: self.secrets.ek.clone(), ek_verifier: eks .get(j) @@ -784,16 +784,21 @@ impl Round2 { alpha_i.values().fold(BigInt::zero(), |acc, x| acc.add(x)); // Sum alpha_hat_i_j's - let sum_of_alpha_hats = - alpha_hat_i.values().fold(BigInt::zero(), |acc, x| acc.add(x)); + let sum_of_alpha_hats = alpha_hat_i + .values() + .fold(BigInt::zero(), |acc, x| acc.add(x)); // Sum beta_i_j's - let sum_of_betas = - self.beta_i.values().fold(BigInt::zero(), |acc, x| acc.add(x)); + let sum_of_betas = self + .beta_i + .values() + .fold(BigInt::zero(), |acc, x| acc.add(x)); // Sum beta_hat_i_j's - let sum_of_beta_hats = - self.beta_hat_i.values().fold(BigInt::zero(), |acc, x| acc.add(x)); + let sum_of_beta_hats = self + .beta_hat_i + .values() + .fold(BigInt::zero(), |acc, x| acc.add(x)); // delta_i = gamma_i * k_i + sum of alpha_i_j's + sum of beta_i_j's mod // q @@ -1011,13 +1016,13 @@ impl Round3 { let product_of_Deltas = Deltas.values().fold(self.Delta_i.clone(), |acc, x| acc + x); - if product_of_Deltas == - Point::::generator().as_point() * - Scalar::from_bigint(&delta) + if product_of_Deltas + == Point::::generator().as_point() + * Scalar::from_bigint(&delta) { // R = Gamma^{delta^{-1}} - let R = self.Gamma.clone() * - Scalar::from_bigint( + let R = self.Gamma.clone() + * Scalar::from_bigint( &BigInt::mod_inv(&delta, &self.ssid.q).unwrap(), ); let presigning_output = PresigningOutput { @@ -1093,44 +1098,92 @@ impl Round3 { PaillierAffineOpWithGroupComInRangeStatement, > = HashMap::new(); - self.ssid.P.iter().zip(self.ssid.P.iter()).for_each(|(j, l)| { - if *j != self.ssid.X.i && j != l { - let D_j_i = self.D_j.get(&self.ssid.X.i.clone()).unwrap(); - - // F_j_i = enc_i(beta_i_j, r_i_j) - let F_j_i = self.F_j.get(&self.ssid.X.i).unwrap(); - - let witness_D_j_i = PaillierAffineOpWithGroupComInRangeWitness::new( - self.gamma_i.clone(), - self.beta_i.get(j).unwrap_or(&BigInt::zero()).clone(), - self.s_i.get(j).unwrap_or(&BigInt::zero()).clone(), - self.r_i.get(j).unwrap_or(&BigInt::zero()).clone(), - ); - let statement_D_j_i = PaillierAffineOpWithGroupComInRangeStatement { - S: self.S.get(l).unwrap_or(&BigInt::zero()).clone(), - T: self.T.get(l).unwrap_or(&BigInt::zero()).clone(), - N_hat: self.N_hats.get(l).unwrap_or(&BigInt::zero()).clone(), - N0: self.secrets.ek.n.clone(), - N1: self.eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).n.clone(), - NN0: self.secrets.ek.nn.clone(), - NN1: self.eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).nn.clone(), - C: D_j_i.clone(), - D: self.K.get(j).unwrap_or(&BigInt::zero()).clone(), - Y: F_j_i.clone(), - X: self.Gamma_i.clone(), - ek_prover: self.secrets.ek.clone(), - ek_verifier: self.eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).clone(), - phantom: PhantomData, - }; - let D_j_i_proof = - PaillierAffineOpWithGroupComInRangeProof::::prove( - &witness_D_j_i, - &statement_D_j_i, - ); - proofs_D_j_i.insert((*l, *j), D_j_i_proof); - statements_D_j_i.insert((*l, *j), statement_D_j_i); - } - }); + self.ssid + .P + .iter() + .zip(self.ssid.P.iter()) + .for_each(|(j, l)| { + if *j != self.ssid.X.i && j != l { + let D_j_i = + self.D_j.get(&self.ssid.X.i.clone()).unwrap(); + + // F_j_i = enc_i(beta_i_j, r_i_j) + let F_j_i = self.F_j.get(&self.ssid.X.i).unwrap(); + + let witness_D_j_i = + PaillierAffineOpWithGroupComInRangeWitness::new( + self.gamma_i.clone(), + self.beta_i + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + self.s_i + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + self.r_i + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + ); + let statement_D_j_i = + PaillierAffineOpWithGroupComInRangeStatement { + S: self + .S + .get(l) + .unwrap_or(&BigInt::zero()) + .clone(), + T: self + .T + .get(l) + .unwrap_or(&BigInt::zero()) + .clone(), + N_hat: self + .N_hats + .get(l) + .unwrap_or(&BigInt::zero()) + .clone(), + N0: self.secrets.ek.n.clone(), + N1: self + .eks + .get(j) + .unwrap_or(&DEFAULT_ENCRYPTION_KEY()) + .n + .clone(), + NN0: self.secrets.ek.nn.clone(), + NN1: self + .eks + .get(j) + .unwrap_or(&DEFAULT_ENCRYPTION_KEY()) + .nn + .clone(), + C: D_j_i.clone(), + D: self + .K + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + Y: F_j_i.clone(), + X: self.Gamma_i.clone(), + ek_prover: self.secrets.ek.clone(), + ek_verifier: self + .eks + .get(j) + .unwrap_or(&DEFAULT_ENCRYPTION_KEY()) + .clone(), + phantom: PhantomData, + }; + let D_j_i_proof = + PaillierAffineOpWithGroupComInRangeProof::< + Secp256k1, + Sha256, + >::prove( + &witness_D_j_i, &statement_D_j_i + ); + proofs_D_j_i.insert((*l, *j), D_j_i_proof); + statements_D_j_i.insert((*l, *j), statement_D_j_i); + } + }); // H_i proof let H_i_randomness: BigInt = @@ -1248,7 +1301,11 @@ impl Round3 { receiver: None, body: Box::new(body), }); - Ok(Round4 { ssid: self.ssid, output: None, transcript: None }) + Ok(Round4 { + ssid: self.ssid, + output: None, + transcript: None, + }) } } @@ -1318,7 +1375,7 @@ impl Round4 { bad_actors: vec_D_si_j_proof_bad_actors, data: bincode::serialize(&error_data).unwrap(), }, - )) + )); } // Check H_j proofs let proof_H_si = msg.proof_H_i; @@ -1337,7 +1394,7 @@ impl Round4 { bad_actors: vec![si.into()], data: bincode::serialize(&error_data).unwrap(), }, - )) + )); } // Check delta_si_proof let proof_delta_si = @@ -1361,7 +1418,7 @@ impl Round4 { bad_actors: vec![si.into()], data: bincode::serialize(&error_data).unwrap(), }, - )) + )); } } Ok(None) diff --git a/src/presign/state_machine.rs b/src/presign/state_machine.rs index f67e60e3..31db8b0f 100644 --- a/src/presign/state_machine.rs +++ b/src/presign/state_machine.rs @@ -75,7 +75,7 @@ impl PreSigning { ) -> Result { let n = ssid.P.len(); if n < 2 { - return Err(Error::TooFewParties) + return Err(Error::TooFewParties); } let i = ssid.X.i; @@ -115,14 +115,26 @@ impl PreSigning { /// Proceeds round state if it received enough messages and if it's cheap to /// compute or `may_block == true` fn proceed_round(&mut self, may_block: bool) -> Result<()> { - let store1_wants_more = - self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = - self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store3_wants_more = - self.round2_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store4_wants_more = - self.round3_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store1_wants_more = self + .round0_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); + let store2_wants_more = self + .round1_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); + let store3_wants_more = self + .round2_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); + let store4_wants_more = self + .round3_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); let next_state: R; @@ -133,14 +145,14 @@ impl PreSigning { .map(|msg| R::Round1(Box::new(msg))) .map_err(|_e| Error::ProceedRound { msg_round: 0 })?; true - }, + } s @ R::Round0(_) => { next_state = s; false - }, + } R::Round1(round) - if !store1_wants_more && - (!round.is_expensive() || may_block) => + if !store1_wants_more + && (!round.is_expensive() || may_block) => { let store = self.round0_msgs.take().ok_or(InternalError::StoreGone)?; @@ -152,14 +164,14 @@ impl PreSigning { .map(|msg| R::Round2(Box::new(msg))) .map_err(|_e| Error::ProceedRound { msg_round: 1 })?; true - }, + } s @ R::Round1(_) => { next_state = s; false - }, + } R::Round2(round) - if !store2_wants_more && - (!round.is_expensive() || may_block) => + if !store2_wants_more + && (!round.is_expensive() || may_block) => { let store = self.round1_msgs.take().ok_or(InternalError::StoreGone)?; @@ -171,14 +183,14 @@ impl PreSigning { .map(|msg| R::Round3(Box::new(msg))) .map_err(|_e| Error::ProceedRound { msg_round: 2 })?; true - }, + } s @ R::Round2(_) => { next_state = s; false - }, + } R::Round3(round) - if !store3_wants_more && - (!round.is_expensive() || may_block) => + if !store3_wants_more + && (!round.is_expensive() || may_block) => { let store = self.round2_msgs.take().ok_or(InternalError::StoreGone)?; @@ -190,14 +202,14 @@ impl PreSigning { .map(|msg| R::Round4(Box::new(msg))) .map_err(|_e| Error::ProceedRound { msg_round: 3 })?; true - }, + } s @ R::Round3(_) => { next_state = s; false - }, + } R::Round4(round) - if !store4_wants_more && - (!round.is_expensive() || may_block) => + if !store4_wants_more + && (!round.is_expensive() || may_block) => { let store = self.round3_msgs.take().ok_or(InternalError::StoreGone)?; @@ -209,15 +221,15 @@ impl PreSigning { .map(|msg| R::Final(Box::new(msg))) .map_err(|_e| Error::ProceedRound { msg_round: 4 })?; true - }, + } s @ R::Round4(_) => { next_state = s; false - }, + } s @ R::Final(_) | s @ R::Gone => { next_state = s; false - }, + } }; self.round = next_state; if try_again { @@ -253,7 +265,7 @@ impl StateMachine for PreSigning { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } ProtocolMessage(M::Round2(m)) => { let store = self.round1_msgs.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -269,7 +281,7 @@ impl StateMachine for PreSigning { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } ProtocolMessage(M::Round3(m)) => { let store = self.round2_msgs.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -285,7 +297,7 @@ impl StateMachine for PreSigning { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } ProtocolMessage(M::Round4(m)) => { let store = self.round3_msgs.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -301,7 +313,7 @@ impl StateMachine for PreSigning { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } } } @@ -310,14 +322,26 @@ impl StateMachine for PreSigning { } fn wants_to_proceed(&self) -> bool { - let store1_wants_more = - self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = - self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store3_wants_more = - self.round2_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store4_wants_more = - self.round3_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store1_wants_more = self + .round0_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); + let store2_wants_more = self + .round1_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); + let store3_wants_more = self + .round2_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); + let store4_wants_more = self + .round3_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); match &self.round { R::Round0(_) => true, @@ -384,14 +408,26 @@ impl StateMachine for PreSigning { impl crate::traits::RoundBlame for PreSigning { fn round_blame(&self) -> (u16, Vec) { - let store1_blame = - self.round0_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store2_blame = - self.round1_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store3_blame = - self.round2_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store4_blame = - self.round3_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store1_blame = self + .round0_msgs + .as_ref() + .map(|s| s.blame()) + .unwrap_or_default(); + let store2_blame = self + .round1_msgs + .as_ref() + .map(|s| s.blame()) + .unwrap_or_default(); + let store3_blame = self + .round2_msgs + .as_ref() + .map(|s| s.blame()) + .unwrap_or_default(); + let store4_blame = self + .round3_msgs + .as_ref() + .map(|s| s.blame()) + .unwrap_or_default(); let default = (0, vec![]); match &self.round { @@ -615,8 +651,10 @@ pub mod test { pub fn extract_secret_key( local_keys: &[LocalKey], ) -> Scalar { - let secret_shares: Vec> = - local_keys.iter().map(|key| key.keys_linear.x_i.clone()).collect(); + let secret_shares: Vec> = local_keys + .iter() + .map(|key| key.keys_linear.x_i.clone()) + .collect(); local_keys[0].vss_scheme.reconstruct( &(0..local_keys.len() as u16).collect::>(), &secret_shares.clone(), @@ -727,8 +765,8 @@ pub mod test { // Composes SSID. // See Figure 6, Round 1. // Ref: . - let phi = (&paillier_dk.p - BigInt::one()) * - (&paillier_dk.q - BigInt::one()); + let phi = (&paillier_dk.p - BigInt::one()) + * (&paillier_dk.q - BigInt::one()); let r = BigInt::sample_below(&paillier_ek.n); let lambda = BigInt::sample_below(&phi); let t = BigInt::mod_pow(&r, &BigInt::from(2), &paillier_ek.n); diff --git a/src/refresh/rounds.rs b/src/refresh/rounds.rs index faf52288..f95d8a9d 100644 --- a/src/refresh/rounds.rs +++ b/src/refresh/rounds.rs @@ -67,7 +67,7 @@ impl Round0 { }), _ => Err(FsDkrError::NewPartyUnassignedIndexError), } - }, + } None => { let (mut join_message, paillier_keys) = JoinMessage::distribute(); @@ -90,10 +90,10 @@ impl Round0 { new_n: self.new_n, current_t: self.current_t, }) - }, + } None => Err(FsDkrError::NewPartyUnassignedIndexError), } - }, + } } } pub fn is_expensive(&self) -> bool { @@ -172,7 +172,7 @@ impl Round1 { new_n: self.new_n, current_t: self.current_t, }) - }, + } PartyType::New(boxed_new) => { let (join_message, paillier_keys, new_party_index) = *boxed_new; @@ -195,7 +195,7 @@ impl Round1 { new_n: self.new_n, current_t: self.current_t, }) - }, + } } } @@ -257,7 +257,7 @@ impl Round2 { self.current_t, )?; Ok(*local_key) - }, + } PartyType::New(boxed_new) => { let (join_message, paillier_keys, _new_party_index) = *boxed_new; @@ -272,7 +272,7 @@ impl Round2 { self.new_n, self.current_t, ) - }, + } } } diff --git a/src/refresh/state_machine.rs b/src/refresh/state_machine.rs index 49955179..e4bccf0b 100644 --- a/src/refresh/state_machine.rs +++ b/src/refresh/state_machine.rs @@ -60,10 +60,10 @@ impl KeyRefresh { current_t_option: Option, ) -> Result { if new_n < 2 { - return Err(Error::TooFewParties) + return Err(Error::TooFewParties); } if new_t == 0 || new_t >= new_n { - return Err(Error::InvalidThreshold) + return Err(Error::InvalidThreshold); } // Sets the party index either from the `local_key_option` or @@ -81,12 +81,14 @@ impl KeyRefresh { // Sets the current/old threshold either from the `local_key_option` or // `current_t_option`, otherwise returns an error if neither is // Some. - let current_t = - match local_key_option.as_ref().map(|it| it.t).or(current_t_option) - { - Some(it) => it, - None => return Err(Error::UnknownCurrentThreshold), - }; + let current_t = match local_key_option + .as_ref() + .map(|it| it.t) + .or(current_t_option) + { + Some(it) => it, + None => return Err(Error::UnknownCurrentThreshold), + }; let mut state = Self { round: R::Round0(Box::new(Round0 { @@ -123,10 +125,16 @@ impl KeyRefresh { /// Proceeds round state if it received enough messages and if it's cheap to /// compute or `may_block == true` fn proceed_round(&mut self, may_block: bool) -> Result<()> { - let store1_wants_more = - self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = - self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store1_wants_more = self + .round0_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); + let store2_wants_more = self + .round1_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); let next_state: R; @@ -137,14 +145,14 @@ impl KeyRefresh { .map(|msg| R::Round1(Box::new(msg))) .map_err(Error::ProceedRound)?; true - }, + } s @ R::Round0(_) => { next_state = s; false - }, + } R::Round1(round) - if !store1_wants_more && - (!round.is_expensive() || may_block) => + if !store1_wants_more + && (!round.is_expensive() || may_block) => { let store = self.round0_msgs.take().ok_or(InternalError::StoreGone)?; @@ -156,14 +164,14 @@ impl KeyRefresh { .map(|msg| R::Round2(Box::new(msg))) .map_err(Error::ProceedRound)?; true - }, + } s @ R::Round1(_) => { next_state = s; false - }, + } R::Round2(round) - if !store2_wants_more && - (!round.is_expensive() || may_block) => + if !store2_wants_more + && (!round.is_expensive() || may_block) => { let store = self.round1_msgs.take().ok_or(InternalError::StoreGone)?; @@ -175,15 +183,15 @@ impl KeyRefresh { .map(|msg| R::Final(Box::new(msg))) .map_err(Error::ProceedRound)?; true - }, + } s @ R::Round2(_) => { next_state = s; false - }, + } s @ R::Final(_) | s @ R::Gone => { next_state = s; false - }, + } }; self.round = next_state; if try_again { @@ -218,7 +226,7 @@ impl StateMachine for KeyRefresh { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } ProtocolMessage(M::Round2(m)) => { let store = self.round1_msgs.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -234,7 +242,7 @@ impl StateMachine for KeyRefresh { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } } } @@ -243,10 +251,16 @@ impl StateMachine for KeyRefresh { } fn wants_to_proceed(&self) -> bool { - let store1_wants_more = - self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = - self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store1_wants_more = self + .round0_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); + let store2_wants_more = self + .round1_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); match &self.round { R::Round0(_) => true, @@ -309,10 +323,16 @@ impl StateMachine for KeyRefresh { impl crate::traits::RoundBlame for KeyRefresh { fn round_blame(&self) -> (u16, Vec) { - let store1_blame = - self.round0_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store2_blame = - self.round1_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store1_blame = self + .round0_msgs + .as_ref() + .map(|s| s.blame()) + .unwrap_or_default(); + let store2_blame = self + .round1_msgs + .as_ref() + .map(|s| s.blame()) + .unwrap_or_default(); let default = (0, vec![]); match &self.round { @@ -544,7 +564,10 @@ pub mod test { .collect(); let indices: Vec<_> = (0..(t + 1)).collect(); let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, + parameters: ShamirSecretSharing { + threshold: t, + share_count: n, + }, commitments: Vec::new(), proof: DLogProof::::prove( &Scalar::random(), @@ -634,7 +657,10 @@ pub mod test { .collect(); let indices: Vec<_> = (0..(t + 1)).collect(); let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, + parameters: ShamirSecretSharing { + threshold: t, + share_count: n, + }, commitments: Vec::new(), proof: DLogProof::::prove( &Scalar::random(), @@ -747,7 +773,10 @@ pub mod test { .collect(); let indices: Vec<_> = (0..(t + 1)).collect(); let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, + parameters: ShamirSecretSharing { + threshold: t, + share_count: n, + }, commitments: Vec::new(), proof: DLogProof::::prove( &Scalar::random(), @@ -841,7 +870,10 @@ pub mod test { let old_indices = vec![0, 1, 2]; let new_indices = vec![0, 1, 2]; let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, + parameters: ShamirSecretSharing { + threshold: t, + share_count: n, + }, commitments: Vec::new(), proof: DLogProof::::prove( &Scalar::random(), diff --git a/src/sign/rounds.rs b/src/sign/rounds.rs index ae9fb1b2..69b6505f 100644 --- a/src/sign/rounds.rs +++ b/src/sign/rounds.rs @@ -158,8 +158,8 @@ impl Round1 { let r_sigma_inv = self.r.mul(&sigma_inv); let g = Point::::generator(); let X = self.ssid.X.public_key(); - let x_projection = ((g * Scalar::from_bigint(&m_sigma_inv)) + - (X * Scalar::from_bigint(&r_sigma_inv))) + let x_projection = ((g * Scalar::from_bigint(&m_sigma_inv)) + + (X * Scalar::from_bigint(&r_sigma_inv))) .x_coord() .unwrap_or_else(BigInt::zero); @@ -175,7 +175,10 @@ impl Round1 { receiver: None, body: Box::new(None), }); - Ok(Round2 { ssid: self.ssid, output: Some(signing_output) }) + Ok(Round2 { + ssid: self.ssid, + output: Some(signing_output), + }) } else { // (l,j) to proof for D_j_i let mut proofs_D_hat_j_i: HashMap< @@ -189,43 +192,47 @@ impl Round1 { PaillierAffineOpWithGroupComInRangeStatement, > = HashMap::new(); - self.ssid.P.iter().zip(self.ssid.P.iter()).for_each(|(j, l)| { - if *j != self.ssid.X.i && j != l { - let D_hat_j_i = self - .presigning_transcript - .D_hat_j - .get(&self.ssid.X.i) - .unwrap() - .clone(); - - // F_hat_j_i = enc_i(beta_hat_i_j, r_hat_i_j) - let F_hat_j_i = self - .presigning_transcript - .F_hat_j - .get(&self.ssid.X.i) - .unwrap() - .clone(); - - let witness_D_hat_j_i = - PaillierAffineOpWithGroupComInRangeWitness::new( - self.presigning_transcript.secrets.x_i.clone(), - self.presigning_transcript - .beta_hat_i - .get(j) - .unwrap_or(&BigInt::zero()) - .clone(), - self.presigning_transcript - .s_hat_i - .get(j) - .unwrap_or(&BigInt::zero()) - .clone(), - self.presigning_transcript - .r_hat_i - .get(j) - .unwrap_or(&BigInt::zero()) - .clone(), - ); - let statement_D_hat_j_i = + self.ssid + .P + .iter() + .zip(self.ssid.P.iter()) + .for_each(|(j, l)| { + if *j != self.ssid.X.i && j != l { + let D_hat_j_i = self + .presigning_transcript + .D_hat_j + .get(&self.ssid.X.i) + .unwrap() + .clone(); + + // F_hat_j_i = enc_i(beta_hat_i_j, r_hat_i_j) + let F_hat_j_i = self + .presigning_transcript + .F_hat_j + .get(&self.ssid.X.i) + .unwrap() + .clone(); + + let witness_D_hat_j_i = + PaillierAffineOpWithGroupComInRangeWitness::new( + self.presigning_transcript.secrets.x_i.clone(), + self.presigning_transcript + .beta_hat_i + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + self.presigning_transcript + .s_hat_i + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + self.presigning_transcript + .r_hat_i + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + ); + let statement_D_hat_j_i = crate::utilities::aff_g::PaillierAffineOpWithGroupComInRangeStatement { S: self .presigning_transcript @@ -280,15 +287,16 @@ impl Round1 { .clone(), phantom: PhantomData, }; - let proof_D_hat_j_i = + let proof_D_hat_j_i = crate::utilities::aff_g::PaillierAffineOpWithGroupComInRangeProof::< Secp256k1, Sha256, >::prove(&witness_D_hat_j_i, &statement_D_hat_j_i); - proofs_D_hat_j_i.insert((*l, *j), proof_D_hat_j_i); - statements_D_hat_j_i.insert((*l, *j), statement_D_hat_j_i); - } - }); + proofs_D_hat_j_i.insert((*l, *j), proof_D_hat_j_i); + statements_D_hat_j_i + .insert((*l, *j), statement_D_hat_j_i); + } + }); // mul* H_hat_i proof let H_hat_i_randomness = sample_relatively_prime_integer( @@ -309,8 +317,8 @@ impl Round1 { self.presigning_transcript.rho_i.mul(&H_hat_i_randomness), ); - let X_i = Point::::generator() * - Scalar::from_bigint(&self.presigning_transcript.secrets.x_i); + let X_i = Point::::generator() + * Scalar::from_bigint(&self.presigning_transcript.secrets.x_i); let mut proof_H_hat_i: HashMap< u16, @@ -499,7 +507,10 @@ impl Round1 { receiver: None, body: Box::new(body), }); - Ok(Round2 { ssid: self.ssid, output: None }) + Ok(Round2 { + ssid: self.ssid, + output: None, + }) } } @@ -567,7 +578,7 @@ impl Round2 { error_type: "aff-g".to_string(), bad_actors: vec_D_hat_si_j_proof_bad_actors, data: bincode::serialize(&error_data).unwrap(), - })) + })); } // Check H_j proofs let proof_H_hat_si = @@ -589,7 +600,7 @@ impl Round2 { error_type: "mul".to_string(), bad_actors: vec![si.into()], data: bincode::serialize(&error_data).unwrap(), - })) + })); } // Check delta_si_proof let proof_sigma_si = @@ -611,7 +622,7 @@ impl Round2 { error_type: "dec-q".to_string(), bad_actors: vec![si.into()], data: bincode::serialize(&error_data).unwrap(), - })) + })); } } Ok(None) diff --git a/src/sign/state_machine.rs b/src/sign/state_machine.rs index aa413e1c..d323fc33 100644 --- a/src/sign/state_machine.rs +++ b/src/sign/state_machine.rs @@ -71,11 +71,16 @@ impl Signing { let n = ssid.P.len() as u16; let i = ssid.X.i; if n < 2 { - return Err(Error::TooFewParties) + return Err(Error::TooFewParties); } let mut state = Self { - round: R::Round0(Box::new(Round0 { ssid, l, m, presigning_data })), + round: R::Round0(Box::new(Round0 { + ssid, + l, + m, + presigning_data, + })), round0_msgs: Some(Round1::expects_messages(i, n)), round1_msgs: Some(Round2::expects_messages(i, n)), @@ -101,10 +106,16 @@ impl Signing { /// Proceeds round state if it received enough messages and if it's cheap to /// compute or `may_block == true` fn proceed_round(&mut self, may_block: bool) -> Result<()> { - let store1_wants_more = - self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = - self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store1_wants_more = self + .round0_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); + let store2_wants_more = self + .round1_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); let next_state: R; @@ -115,14 +126,14 @@ impl Signing { .map(|msg| R::Round1(Box::new(msg))) .map_err(|_e| Error::ProceedRound { msg_round: 0 })?; true - }, + } s @ R::Round0(_) => { next_state = s; false - }, + } R::Round1(round) - if !store1_wants_more && - (!round.is_expensive() || may_block) => + if !store1_wants_more + && (!round.is_expensive() || may_block) => { let store = self.round0_msgs.take().ok_or(InternalError::StoreGone)?; @@ -134,14 +145,14 @@ impl Signing { .map(|msg| R::Round2(Box::new(msg))) .map_err(|_e| Error::ProceedRound { msg_round: 1 })?; true - }, + } s @ R::Round1(_) => { next_state = s; false - }, + } R::Round2(round) - if !store2_wants_more && - (!round.is_expensive() || may_block) => + if !store2_wants_more + && (!round.is_expensive() || may_block) => { let store = self.round1_msgs.take().ok_or(InternalError::StoreGone)?; @@ -153,16 +164,16 @@ impl Signing { .map(|msg| R::Final(Box::new(msg))) .map_err(|_e| Error::ProceedRound { msg_round: 2 })?; true - }, + } s @ R::Round2(_) => { next_state = s; false - }, + } s @ R::Final(_) | s @ R::Gone => { next_state = s; false - }, + } }; self.round = next_state; if try_again { @@ -197,7 +208,7 @@ impl StateMachine for Signing { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } ProtocolMessage(M::Round2(m)) => { let store = self.round1_msgs.as_mut().ok_or( Error::ReceivedOutOfOrderMessage { @@ -213,7 +224,7 @@ impl StateMachine for Signing { }) .map_err(Error::HandleMessage)?; self.proceed_round(false) - }, + } } } @@ -222,10 +233,16 @@ impl StateMachine for Signing { } fn wants_to_proceed(&self) -> bool { - let store1_wants_more = - self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = - self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store1_wants_more = self + .round0_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); + let store2_wants_more = self + .round1_msgs + .as_ref() + .map(|s| s.wants_more()) + .unwrap_or(false); match &self.round { R::Round0(_) => true, @@ -288,10 +305,16 @@ impl StateMachine for Signing { impl crate::traits::RoundBlame for Signing { fn round_blame(&self) -> (u16, Vec) { - let store1_blame = - self.round0_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store2_blame = - self.round1_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store1_blame = self + .round0_msgs + .as_ref() + .map(|s| s.blame()) + .unwrap_or_default(); + let store2_blame = self + .round1_msgs + .as_ref() + .map(|s| s.blame()) + .unwrap_or_default(); let default = (0, vec![]); match &self.round { @@ -523,8 +546,8 @@ mod test { let r_direct = (Point::::generator() * k.invert().unwrap()) .x_coord() .unwrap(); - let s_direct = (k.to_bigint() * - (message_digest + (&r_direct * &sec_key.to_bigint()))) + let s_direct = (k.to_bigint() + * (message_digest + (&r_direct * &sec_key.to_bigint()))) .mod_floor(q); let expected_signature = (r_direct, s_direct); assert_eq!(signature, expected_signature); diff --git a/src/utilities/aff_g/mod.rs b/src/utilities/aff_g/mod.rs index c689728e..3f83886d 100644 --- a/src/utilities/aff_g/mod.rs +++ b/src/utilities/aff_g/mod.rs @@ -344,7 +344,16 @@ impl &statement.N1, ); - Self { z1, z2, z3, z4, w, wy, commitment, phantom: PhantomData } + Self { + z1, + z2, + z3, + z4, + w, + wy, + commitment, + phantom: PhantomData, + } } pub fn verify( @@ -415,8 +424,8 @@ impl // g^{z1} = B_x ·X^e ∈ G let left_2 = Point::::generator().as_point() * Scalar::from_bigint(&proof.z1); - let right_2 = proof.commitment.B_x.clone() + - (statement.X.clone() * Scalar::from_bigint(&e)); + let right_2 = proof.commitment.B_x.clone() + + (statement.X.clone() * Scalar::from_bigint(&e)); // Assert left == right assert!(left_2 == right_2); /* diff --git a/src/utilities/dec_q/mod.rs b/src/utilities/dec_q/mod.rs index 39bd27bf..8a963633 100644 --- a/src/utilities/dec_q/mod.rs +++ b/src/utilities/dec_q/mod.rs @@ -53,7 +53,11 @@ pub struct PaillierDecryptionModQWitness { impl PaillierDecryptionModQWitness { pub fn new(y: BigInt, rho: BigInt) -> Self { - PaillierDecryptionModQWitness { y, rho, phantom: PhantomData } + PaillierDecryptionModQWitness { + y, + rho, + phantom: PhantomData, + } } } @@ -92,7 +96,11 @@ impl PaillierDecryptionModQStatement { ek_prover, phantom: PhantomData, }, - PaillierDecryptionModQWitness { y, rho, phantom: PhantomData }, + PaillierDecryptionModQWitness { + y, + rho, + phantom: PhantomData, + }, ) } } @@ -178,7 +186,12 @@ impl PaillierDecryptionModQProof { .add(&BigInt::one()) .mul(&e); let commitment: PaillierDecryptionModQCommitment = - PaillierDecryptionModQCommitment { A, gamma, big_S, big_T }; + PaillierDecryptionModQCommitment { + A, + gamma, + big_S, + big_T, + }; // z1 = α + e · y let z1 = BigInt::add(&alpha, &BigInt::mul(&e, &witness.y)); // z2 = ν + e · μ diff --git a/src/utilities/enc/mod.rs b/src/utilities/enc/mod.rs index 10558057..b1d909d4 100644 --- a/src/utilities/enc/mod.rs +++ b/src/utilities/enc/mod.rs @@ -56,7 +56,11 @@ pub struct PaillierEncryptionInRangeWitness { impl PaillierEncryptionInRangeWitness { pub fn new(k: BigInt, rho: BigInt) -> Self { - PaillierEncryptionInRangeWitness { k, rho, phantom: PhantomData } + PaillierEncryptionInRangeWitness { + k, + rho, + phantom: PhantomData, + } } } @@ -83,8 +87,20 @@ impl PaillierEncryptionInRangeStatement { .into(); ( - Self { N0, NN0, K, s, t, N_hat, phantom: PhantomData }, - PaillierEncryptionInRangeWitness { k, rho, phantom: PhantomData }, + Self { + N0, + NN0, + K, + s, + t, + N_hat, + phantom: PhantomData, + }, + PaillierEncryptionInRangeWitness { + k, + rho, + phantom: PhantomData, + }, ) } } @@ -192,7 +208,13 @@ impl PaillierEncryptionInRangeProof { // z_3 = gamma + e*mu let z_3 = BigInt::add(&gamma, &BigInt::mul(&e, &mu)); - Self { z_1, z_2, z_3, commitment, phantom: PhantomData } + Self { + z_1, + z_2, + z_3, + commitment, + phantom: PhantomData, + } } #[allow(dead_code)] @@ -210,7 +232,10 @@ impl PaillierEncryptionInRangeProof { let NN0 = statement.NN0.clone(); // left_1 = (1+N_0)^{z_1}z_2^{N_0} mod N_0^2 let left_1: BigInt = Paillier::encrypt_with_chosen_randomness( - &EncryptionKey { n: statement.N0.clone(), nn: NN0.clone() }, + &EncryptionKey { + n: statement.N0.clone(), + nn: NN0.clone(), + }, RawPlaintext::from(&proof.z_1), &Randomness::from(&proof.z_2), ) @@ -236,24 +261,24 @@ impl PaillierEncryptionInRangeProof { ); if left_1.mod_floor(&NN0) != right_1 || left_2 != right_2 { - return Err(IncorrectProof) + return Err(IncorrectProof); } // Range Check -2^{L + eps} <= z_1 <= 2^{L+eps} - let lower_bound_check: bool = proof.z_1 >= - BigInt::from(-1).mul(&BigInt::pow( + let lower_bound_check: bool = proof.z_1 + >= BigInt::from(-1).mul(&BigInt::pow( &BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32, )); - let upper_bound_check = proof.z_1 <= - BigInt::pow( + let upper_bound_check = proof.z_1 + <= BigInt::pow( &BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32, ); if !(lower_bound_check && upper_bound_check) { - return Err(IncorrectProof) + return Err(IncorrectProof); } Ok(()) diff --git a/src/utilities/log_star/mod.rs b/src/utilities/log_star/mod.rs index 27b5d77a..8189f617 100644 --- a/src/utilities/log_star/mod.rs +++ b/src/utilities/log_star/mod.rs @@ -103,13 +103,26 @@ impl let g = g.unwrap_or(Point::::generator().to_point()); let X = &g * Scalar::from(&x); let C: BigInt = Paillier::encrypt_with_chosen_randomness( - &EncryptionKey { n: N0.clone(), nn: NN0.clone() }, + &EncryptionKey { + n: N0.clone(), + nn: NN0.clone(), + }, RawPlaintext::from(&x), &Randomness::from(&rho), ) .into(); ( - Self { N0, NN0, C, X, g, N_hat, s, t, phantom: PhantomData }, + Self { + N0, + NN0, + C, + X, + g, + N_hat, + s, + t, + phantom: PhantomData, + }, KnowledgeOfExponentPaillierEncryptionWitness { x, rho, @@ -229,7 +242,13 @@ impl // z_3 = gamma + e*mu let z_3 = BigInt::add(&gamma, &BigInt::mul(&e, &mu)); - Self { z_1, z_2, z_3, commitment, phantom: PhantomData } + Self { + z_1, + z_2, + z_3, + commitment, + phantom: PhantomData, + } } pub fn verify( @@ -264,8 +283,8 @@ impl // left_2 = g^z_1 let left_2 = &statement.g * Scalar::from_bigint(&proof.z_1); // right_2 = Y * X^e - let right_2 = proof.commitment.Y.clone() + - (statement.X.clone() * Scalar::from_bigint(&e)); + let right_2 = proof.commitment.Y.clone() + + (statement.X.clone() * Scalar::from_bigint(&e)); // left_3 = s^z_1 t^z_3 mod N_hat let left_3 = BigInt::mod_mul( @@ -281,28 +300,28 @@ impl &statement.N_hat, ); - if left_1.mod_floor(&statement.NN0) != right_1 || - left_2 != right_2 || - left_3 != right_3 + if left_1.mod_floor(&statement.NN0) != right_1 + || left_2 != right_2 + || left_3 != right_3 { - return Err(IncorrectProof) + return Err(IncorrectProof); } // Range Check -2^{L + eps} <= z_1 <= 2^{L+eps} - let lower_bound_check: bool = proof.z_1 >= - BigInt::from(-1).mul(&BigInt::pow( + let lower_bound_check: bool = proof.z_1 + >= BigInt::from(-1).mul(&BigInt::pow( &BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32, )); - let upper_bound_check = proof.z_1 <= - BigInt::pow( + let upper_bound_check = proof.z_1 + <= BigInt::pow( &BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32, ); if !(lower_bound_check && upper_bound_check) { - return Err(IncorrectProof) + return Err(IncorrectProof); } Ok(()) } diff --git a/src/utilities/mta/mod.rs b/src/utilities/mta/mod.rs index 79bb3d86..bd24ada8 100644 --- a/src/utilities/mta/mod.rs +++ b/src/utilities/mta/mod.rs @@ -104,7 +104,10 @@ impl MessageA { }) .collect::>(); - Self { c: c_a, range_proofs: alice_range_proofs } + Self { + c: c_a, + range_proofs: alice_range_proofs, + } } } @@ -138,7 +141,7 @@ impl MessageB { dlog_statements: &[DLogStatement], ) -> Result<(Self, Scalar), Error> { if m_a.range_proofs.len() != dlog_statements.len() { - return Err(InvalidKey) + return Err(InvalidKey); } // verify proofs if !m_a @@ -150,7 +153,7 @@ impl MessageB { }) .all(|x| x) { - return Err(InvalidKey) + return Err(InvalidKey); }; let beta_tag_fe = Scalar::::from(beta_tag); let c_beta_tag = Paillier::encrypt_with_chosen_randomness( @@ -215,9 +218,9 @@ impl MessageB { let g_alpha = g * α let ba_btag = &self.b_proof.pk * a + &self.beta_tag_proof.pk; - if DLogProof::verify(&self.b_proof).is_ok() && - DLogProof::verify(&self.beta_tag_proof).is_ok() && - ba_btag == g_alpha + if DLogProof::verify(&self.b_proof).is_ok() + && DLogProof::verify(&self.beta_tag_proof).is_ok() + && ba_btag == g_alpha { Ok(alpha) } else { diff --git a/src/utilities/mta/range_proofs.rs b/src/utilities/mta/range_proofs.rs index 5401ac2c..97c417b5 100644 --- a/src/utilities/mta/range_proofs.rs +++ b/src/utilities/mta/range_proofs.rs @@ -53,16 +53,24 @@ impl AliceZkpRound1 { let beta = BigInt::from_paillier_key(alice_ek); let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); let ro = BigInt::sample_below(&(q * N_tilde)); - let z = (BigInt::mod_pow(h1, a, N_tilde) * - BigInt::mod_pow(h2, &ro, N_tilde)) % - N_tilde; - let u = ((alpha.borrow() * &alice_ek.n + 1) * - BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % - &alice_ek.nn; - let w = (BigInt::mod_pow(h1, &alpha, N_tilde) * - BigInt::mod_pow(h2, &gamma, N_tilde)) % - N_tilde; - Self { alpha, beta, gamma, ro, z, u, w } + let z = (BigInt::mod_pow(h1, a, N_tilde) + * BigInt::mod_pow(h2, &ro, N_tilde)) + % N_tilde; + let u = ((alpha.borrow() * &alice_ek.n + 1) + * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) + % &alice_ek.nn; + let w = (BigInt::mod_pow(h1, &alpha, N_tilde) + * BigInt::mod_pow(h2, &gamma, N_tilde)) + % N_tilde; + Self { + alpha, + beta, + gamma, + ro, + z, + u, + w, + } } } @@ -82,8 +90,8 @@ impl AliceZkpRound2 { r: &BigInt, ) -> Self { Self { - s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) % - &alice_ek.n, + s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) + % &alice_ek.n, s1: (e * a) + round1.alpha.borrow(), s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), } @@ -116,7 +124,7 @@ impl AliceProof { let Gen = alice_ek.n.borrow() + 1; if self.s1 > Scalar::::group_order().pow(3) { - return false + return false; } let z_e_inv = BigInt::mod_inv( @@ -129,10 +137,10 @@ impl AliceProof { Some(c) => c, }; - let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) * - BigInt::mod_pow(h2, &self.s2, N_tilde) * - z_e_inv) % - N_tilde; + let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) + * BigInt::mod_pow(h2, &self.s2, N_tilde) + * z_e_inv) + % N_tilde; let gs1 = (self.s1.borrow() * N + 1) % NN; let cipher_e_inv = @@ -153,7 +161,7 @@ impl AliceProof { .chain_bigint(&w) .result_bigint(); if e != self.e { - return false + return false; } true @@ -239,23 +247,36 @@ impl BobZkpRound1 { let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); let sigma = BigInt::sample_below(&(q * N_tilde)); let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * - BigInt::mod_pow(h2, &ro, N_tilde)) % - N_tilde; - let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) * - BigInt::mod_pow(h2, &ro_prim, N_tilde)) % - N_tilde; - let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * - BigInt::mod_pow(h2, &sigma, N_tilde)) % - N_tilde; - let w = (BigInt::mod_pow(h1, &gamma, N_tilde) * - BigInt::mod_pow(h2, &tau, N_tilde)) % - N_tilde; - let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) * - (gamma.borrow() * &alice_ek.n + 1) * - BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % - &alice_ek.nn; - Self { alpha, beta, gamma, ro, ro_prim, sigma, tau, z, z_prim, t, w, v } + let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) + * BigInt::mod_pow(h2, &ro, N_tilde)) + % N_tilde; + let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) + * BigInt::mod_pow(h2, &ro_prim, N_tilde)) + % N_tilde; + let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) + * BigInt::mod_pow(h2, &sigma, N_tilde)) + % N_tilde; + let w = (BigInt::mod_pow(h1, &gamma, N_tilde) + * BigInt::mod_pow(h2, &tau, N_tilde)) + % N_tilde; + let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) + * (gamma.borrow() * &alice_ek.n + 1) + * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) + % &alice_ek.nn; + Self { + alpha, + beta, + gamma, + ro, + ro_prim, + sigma, + tau, + z, + z_prim, + t, + w, + v, + } } } @@ -284,9 +305,9 @@ impl BobZkpRound2 { ) -> Self { let b_bn = b.to_bigint(); Self { - s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * - round1.beta.borrow()) % - &alice_ek.n, + s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) + * round1.beta.borrow()) + % &alice_ek.n, s1: (e * b_bn) + round1.alpha.borrow(), s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), t1: (e * beta_prim) + round1.gamma.borrow(), @@ -331,7 +352,7 @@ impl BobProof { let h2 = &dlog_statement.ni; if self.s1 > Scalar::::group_order().pow(3) { - return false + return false; } let z_e_inv = BigInt::mod_inv( @@ -344,10 +365,10 @@ impl BobProof { Some(c) => c, }; - let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) * - BigInt::mod_pow(h2, &self.s2, N_tilde) * - z_e_inv) % - N_tilde; + let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) + * BigInt::mod_pow(h2, &self.s2, N_tilde) + * z_e_inv) + % N_tilde; let mta_e_inv = BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); @@ -356,11 +377,11 @@ impl BobProof { Some(c) => c, }; - let v = (BigInt::mod_pow(a_enc, &self.s1, NN) * - BigInt::mod_pow(&self.s, N, NN) * - (self.t1.borrow() * N + 1) * - mta_e_inv) % - NN; + let v = (BigInt::mod_pow(a_enc, &self.s1, NN) + * BigInt::mod_pow(&self.s, N, NN) + * (self.t1.borrow() * N + 1) + * mta_e_inv) + % NN; let t_e_inv = BigInt::mod_inv( &BigInt::mod_pow(&self.t, &self.e, N_tilde), @@ -371,10 +392,10 @@ impl BobProof { Some(c) => c, }; - let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) * - BigInt::mod_pow(h2, &self.t2, N_tilde) * - t_e_inv) % - N_tilde; + let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) + * BigInt::mod_pow(h2, &self.t2, N_tilde) + * t_e_inv) + % N_tilde; let Gen = alice_ek.n.borrow() + 1; let mut values_to_hash = vec![ @@ -402,7 +423,7 @@ impl BobProof { .into_iter() .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) .result_bigint() - }, + } None => values_to_hash .into_iter() .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) @@ -410,7 +431,7 @@ impl BobProof { }; if e != self.e { - return false + return false; } true @@ -515,9 +536,12 @@ impl BobProofExt { mta_avc_out, alice_ek, dlog_statement, - Some(&BobCheck { u: self.u.clone(), X: X.clone() }), + Some(&BobCheck { + u: self.u.clone(), + X: X.clone(), + }), ) { - return false + return false; } // fiddle with EC points @@ -529,7 +553,7 @@ impl BobProofExt { }; if x1 != x2 { - return false + return false; } true @@ -548,7 +572,7 @@ impl SampleFromMultiplicativeGroup for BigInt { loop { let r = Self::sample_below(N); if r.gcd(N) == One { - return r + return r; } } } @@ -587,7 +611,10 @@ pub(crate) mod tests { true, ); - BobProofExt { proof: bob_proof, u: u.unwrap() } + BobProofExt { + proof: bob_proof, + u: u.unwrap(), + } } pub(crate) fn generate_init( @@ -606,7 +633,11 @@ pub(crate) mod tests { let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); let (ek, dk) = Paillier::keypair().keys(); - let dlog_statement = DLogStatement { g: h1, ni: h2, N: ek_tilde.n }; + let dlog_statement = DLogStatement { + g: h1, + ni: h2, + N: ek_tilde.n, + }; (dlog_statement, ek, dk) } diff --git a/src/utilities/mul/mod.rs b/src/utilities/mul/mod.rs index 3e22555e..8b966fbd 100644 --- a/src/utilities/mul/mod.rs +++ b/src/utilities/mul/mod.rs @@ -52,7 +52,12 @@ pub struct PaillierMulWitness { impl PaillierMulWitness { pub fn new(x: BigInt, rho: BigInt, rho_x: BigInt) -> Self { - PaillierMulWitness { x, rho, rho_x, phantom: PhantomData } + PaillierMulWitness { + x, + rho, + rho_x, + phantom: PhantomData, + } } } @@ -90,7 +95,12 @@ impl PaillierMulStatement { ek_prover, phantom: PhantomData, }, - PaillierMulWitness { x, rho, rho_x, phantom: PhantomData }, + PaillierMulWitness { + x, + rho, + rho_x, + phantom: PhantomData, + }, ) } } @@ -161,7 +171,13 @@ impl PaillierMulProof { &statement.N, ); // Return the proof - PaillierMulProof { z, u, v, commitment, phantom: PhantomData } + PaillierMulProof { + z, + u, + v, + commitment, + phantom: PhantomData, + } } pub fn verify( diff --git a/src/utilities/mul_star/mod.rs b/src/utilities/mul_star/mod.rs index 01e6f11f..25a1e787 100644 --- a/src/utilities/mul_star/mod.rs +++ b/src/utilities/mul_star/mod.rs @@ -96,7 +96,17 @@ impl &NN0, ); ( - Self { N0, NN0, C, D, X, N_hat, s, t, phantom: PhantomData }, + Self { + N0, + NN0, + C, + D, + X, + N_hat, + s, + t, + phantom: PhantomData, + }, PaillierMultiplicationVersusGroupWitness { x, rho, @@ -204,7 +214,13 @@ impl PaillierMultiplicationVersusGroupProof { ); let commitment = PaillierMultiplicationVersusGroupCommitment { A, B_x, E, S }; - Self { z_1, z_2, w, commitment, phantom: PhantomData } + Self { + z_1, + z_2, + w, + commitment, + phantom: PhantomData, + } } pub fn verify( @@ -233,11 +249,11 @@ impl PaillierMultiplicationVersusGroupProof { ); // left_2 = g^z_1 - let left_2 = Point::::generator().as_point() * - Scalar::from_bigint(&proof.z_1); + let left_2 = Point::::generator().as_point() + * Scalar::from_bigint(&proof.z_1); // right_2 = B_x * X^e - let right_2 = proof.commitment.B_x.clone() + - (statement.X.clone() * Scalar::from_bigint(&e)); + let right_2 = proof.commitment.B_x.clone() + + (statement.X.clone() * Scalar::from_bigint(&e)); // left_3 = s^z_1 t^z_2 mod N_hat let left_3 = BigInt::mod_mul( @@ -254,24 +270,24 @@ impl PaillierMultiplicationVersusGroupProof { ); if left_1 != right_1 || left_2 != right_2 || left_3 != right_3 { - return Err(IncorrectProof) + return Err(IncorrectProof); } // Range Check -2^{L + eps} <= z_1 <= 2^{L+eps} - let lower_bound_check: bool = proof.z_1 >= - BigInt::from(-1).mul(&BigInt::pow( + let lower_bound_check: bool = proof.z_1 + >= BigInt::from(-1).mul(&BigInt::pow( &BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32, )); - let upper_bound_check = proof.z_1 <= - BigInt::pow( + let upper_bound_check = proof.z_1 + <= BigInt::pow( &BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32, ); if !(lower_bound_check && upper_bound_check) { - return Err(IncorrectProof) + return Err(IncorrectProof); } Ok(()) } diff --git a/src/utilities/sha2.rs b/src/utilities/sha2.rs index 135478cc..4f5528b9 100644 --- a/src/utilities/sha2.rs +++ b/src/utilities/sha2.rs @@ -14,7 +14,9 @@ impl Digest for Sha256 { type OutputSize = U32; fn new() -> Self { - Self { inner: sha2::Sha256::new() } + Self { + inner: sha2::Sha256::new(), + } } fn update(&mut self, data: impl AsRef<[u8]>) { @@ -25,7 +27,9 @@ impl Digest for Sha256 { where Self: Sized, { - Self { inner: self.inner.chain(data) } + Self { + inner: self.inner.chain(data), + } } fn finalize(self) -> Output { diff --git a/src/utilities/zk_pdl/mod.rs b/src/utilities/zk_pdl/mod.rs index a173f207..cfe85291 100644 --- a/src/utilities/zk_pdl/mod.rs +++ b/src/utilities/zk_pdl/mod.rs @@ -184,8 +184,8 @@ impl Verifier { &prover_second_message.decommit.blindness, ); - if prover_first_message.c_hat == c_hat_test && - prover_second_message.decommit.q_hat == state.q_tag + if prover_first_message.c_hat == c_hat_test + && prover_second_message.decommit.q_hat == state.q_tag { Ok(()) } else { @@ -227,8 +227,8 @@ impl Prover { witness: &PDLWitness, state: &PDLProverState, ) -> Result { - let ab_concat = &verifier_second_message.a + - verifier_second_message + let ab_concat = &verifier_second_message.a + + verifier_second_message .b .clone() .shl(verifier_second_message.a.bit_length()); // b|a (in the paper it is a|b) @@ -239,10 +239,12 @@ impl Prover { ); let ax1 = &verifier_second_message.a * witness.x.to_bigint(); let alpha_test = ax1 + &verifier_second_message.b; - if alpha_test == state.alpha && - verifier_first_message.c_tag_tag == c_tag_tag_test + if alpha_test == state.alpha + && verifier_first_message.c_tag_tag == c_tag_tag_test { - Ok(PDLProverSecondMessage { decommit: state.decommit.clone() }) + Ok(PDLProverSecondMessage { + decommit: state.decommit.clone(), + }) } else { Err(ZkPdlError::Message2) } diff --git a/src/utilities/zk_pdl/test.rs b/src/utilities/zk_pdl/test.rs index 928ca7d6..3e5a223b 100644 --- a/src/utilities/zk_pdl/test.rs +++ b/src/utilities/zk_pdl/test.rs @@ -32,9 +32,17 @@ fn test_zk_pdl() { ) .0 .into_owned(); - let statement = - PDLStatement { ciphertext: c, ek, Q, G: Point::generator().to_point() }; - let witness = PDLWitness { x, r: randomness.0, dk }; + let statement = PDLStatement { + ciphertext: c, + ek, + Q, + G: Point::generator().to_point(), + }; + let witness = PDLWitness { + x, + r: randomness.0, + dk, + }; // let (verifier_message1, mut verifier_state) = Verifier::message1(&statement); diff --git a/src/utilities/zk_pdl_with_slack/mod.rs b/src/utilities/zk_pdl_with_slack/mod.rs index 552757b4..dfa737e1 100644 --- a/src/utilities/zk_pdl_with_slack/mod.rs +++ b/src/utilities/zk_pdl_with_slack/mod.rs @@ -129,7 +129,15 @@ impl PDLwSlackProof { ); let s3 = &e * rho + gamma; - PDLwSlackProof { z, u1, u2, u3, s1, s2, s3 } + PDLwSlackProof { + z, + u1, + u2, + u3, + s1, + s2, + s3, + } } pub fn verify( diff --git a/src/utilities/zk_pdl_with_slack/test.rs b/src/utilities/zk_pdl_with_slack/test.rs index 314288fa..227bb4f3 100644 --- a/src/utilities/zk_pdl_with_slack/test.rs +++ b/src/utilities/zk_pdl_with_slack/test.rs @@ -24,8 +24,11 @@ fn test_zk_pdl_with_slack() { let xhi = BigInt::sample_below(&S); let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = - DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; + let statement = DLogStatement { + N: ek_tilde.n.clone(), + g: h1.clone(), + ni: h2.clone(), + }; let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); @@ -82,8 +85,11 @@ fn test_zk_pdl_with_slack_soundness() { let xhi = BigInt::sample_below(&S); let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = - DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; + let statement = DLogStatement { + N: ek_tilde.n.clone(), + g: h1.clone(), + ni: h2.clone(), + }; let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); From 3cc47c9527a9876e72a7923c09e6cf214032916e Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 09:23:38 +0800 Subject: [PATCH 38/41] Fix lint workflow for stable channel. --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d9b88c3a..964df296 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -88,4 +88,4 @@ jobs: - name: Run Linters run: | cargo fmt --all -- --check - cargo clippy -Zunstable-options -- -D warnings + cargo clippy -- -D warnings From 4e3307e8e786a38bff1bf7733975ea6981a09068 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 09:31:02 +0800 Subject: [PATCH 39/41] Remove obsolete patch, not necessary on stable. --- Cargo.lock | 10 +++++++++- Cargo.toml | 3 --- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77d22dee..9ed45b51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -905,6 +905,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "cryptoxide" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35f15e1a0699dd988fed910dd78fdc6407f44654cd12589c91fa44ea67d9159" + [[package]] name = "csv" version = "1.2.2" @@ -978,8 +984,10 @@ dependencies = [ [[package]] name = "curv-kzen" version = "0.10.0" -source = "git+https://github.com/tmpfs/curv.git?branch=wasm-bigint#4bbe397e487bdaa84140433d08277c668e1e1f4e" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616b5f766fd80307f0e83de6326ccd9fe7b3ba4225fe4e12ae3a692a939d07b" dependencies = [ + "cryptoxide", "curve25519-dalek", "digest 0.9.0", "ff-zeroize", diff --git a/Cargo.toml b/Cargo.toml index cd880869..689d8b9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,3 @@ default = ["rust-gmp-kzen"] rust-gmp-kzen = ["fs-dkr/rust-gmp-kzen"] num-bigint = ["fs-dkr/num-bigint"] js = ["getrandom/js"] - -[patch.crates-io] -curv-kzen = { git = "https://github.com/tmpfs/curv.git", branch = "wasm-bigint" } From 5a65ab46f53af0fb3a253cbacb0a34ff23b70bd0 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 09:53:40 +0800 Subject: [PATCH 40/41] Remove duplicate types from party_i. --- src/party_i.rs | 44 ++++++++++++++++---------------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/party_i.rs b/src/party_i.rs index f8f2915c..d239813c 100644 --- a/src/party_i.rs +++ b/src/party_i.rs @@ -55,16 +55,20 @@ use curv::cryptographic_primitives::proofs::sigma_valid_pedersen::PedersenProof; use std::convert::TryInto; +pub use crate::mpc_ecdsa::gg_2020::party_i::{ + Parameters, + KeyGenBroadcastMessage1, + SharedKeys, + KeyGenDecommitMessage1, + SignBroadcastPhase1, + SignDecommitPhase1, + SignatureRecid, +}; + const SECURITY: usize = 256; const PAILLIER_MIN_BIT_LENGTH: usize = 2047; const PAILLIER_MAX_BIT_LENGTH: usize = 2048; -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Parameters { - pub threshold: u16, //t - pub share_count: u16, //n -} - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Keys { pub u_i: Scalar, @@ -87,28 +91,6 @@ pub struct PartyPrivate { dk: DecryptionKey, } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KeyGenBroadcastMessage1 { - pub e: EncryptionKey, - pub dlog_statement: DLogStatement, - pub com: BigInt, - pub correct_key_proof: NiCorrectKeyProof, - pub composite_dlog_proof_base_h1: CompositeDLogProof, - pub composite_dlog_proof_base_h2: CompositeDLogProof, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KeyGenDecommitMessage1 { - pub blind_factor: BigInt, - pub y_i: Point, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SharedKeys { - pub y: Point, - pub x_i: Scalar, -} - #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SignKeys { pub w_i: Scalar, @@ -118,16 +100,20 @@ pub struct SignKeys { pub g_gamma_i: Point, } +/* #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SignBroadcastPhase1 { pub com: BigInt, } +*/ +/* #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SignDecommitPhase1 { pub blind_factor: BigInt, pub g_gamma_i: Point, } +*/ #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LocalSignature { @@ -138,12 +124,14 @@ pub struct LocalSignature { pub y: Point, } +/* #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SignatureRecid { pub r: Scalar, pub s: Scalar, pub recid: u8, } +*/ pub fn generate_h1_h2_N_tilde() -> (BigInt, BigInt, BigInt, BigInt, BigInt) { // note, should be safe primes: From 4c4dcf432764928b68238ecac44a5d709ba40f58 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 09:55:06 +0800 Subject: [PATCH 41/41] Formatting. --- src/party_i.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/party_i.rs b/src/party_i.rs index d239813c..82ea208a 100644 --- a/src/party_i.rs +++ b/src/party_i.rs @@ -56,13 +56,8 @@ use curv::cryptographic_primitives::proofs::sigma_valid_pedersen::PedersenProof; use std::convert::TryInto; pub use crate::mpc_ecdsa::gg_2020::party_i::{ - Parameters, - KeyGenBroadcastMessage1, - SharedKeys, - KeyGenDecommitMessage1, - SignBroadcastPhase1, - SignDecommitPhase1, - SignatureRecid, + KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Parameters, SharedKeys, + SignBroadcastPhase1, SignDecommitPhase1, SignatureRecid, }; const SECURITY: usize = 256;

c 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, + pub tag: Vec, +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct PartySignup { + pub number: u16, + pub uuid: String, +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct Index { + pub key: Key, +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct Entry { + pub key: Key, + pub value: String, +} + +#[derive(Serialize, Deserialize)] +pub struct Params { + pub parties: String, + pub threshold: String, +} + +#[allow(dead_code)] +pub fn aes_encrypt(key: &[u8], plaintext: &[u8]) -> AEAD { + let aes_key = aes_gcm::Key::from_slice(key); + let cipher = Aes256Gcm::new(aes_key); + + let mut nonce = [0u8; 12]; + OsRng.fill_bytes(&mut nonce); + let nonce = Nonce::from_slice(&nonce); + + let ciphertext = cipher + .encrypt(nonce, plaintext) + .expect("encryption failure!"); + + AEAD { + ciphertext: ciphertext, + tag: nonce.to_vec(), + } +} + +#[allow(dead_code)] +pub fn aes_decrypt(key: &[u8], aead_pack: AEAD) -> Vec { + let aes_key = aes_gcm::Key::from_slice(key); + let nonce = Nonce::from_slice(&aead_pack.tag); + let gcm = Aes256Gcm::new(aes_key); + + let out = gcm.decrypt(nonce, aead_pack.ciphertext.as_slice()); + out.unwrap() +} + +pub fn postb(client: &Client, path: &str, body: T) -> Option +where + T: serde::ser::Serialize, +{ + let addr = env::args() + .nth(1) + .unwrap_or_else(|| "http://127.0.0.1:8001".to_string()); + let retries = 3; + let retry_delay = time::Duration::from_millis(250); + for _i in 1..retries { + let res = client + .post(&format!("{}/{}", addr, path)) + .json(&body) + .send(); + + if let Ok(mut res) = res { + return Some(res.text().unwrap()); + } + thread::sleep(retry_delay); + } + None +} + +pub fn broadcast( + client: &Client, + party_num: u16, + round: &str, + data: String, + sender_uuid: String, +) -> Result<(), ()> { + let key = format!("{}-{}-{}", party_num, round, sender_uuid); + let entry = Entry { key, value: data }; + + let res_body = postb(client, "set", entry).unwrap(); + serde_json::from_str(&res_body).unwrap() +} + +pub fn sendp2p( + client: &Client, + party_from: u16, + party_to: u16, + round: &str, + data: String, + sender_uuid: String, +) -> Result<(), ()> { + let key = format!("{}-{}-{}-{}", party_from, party_to, round, sender_uuid); + + let entry = Entry { key, value: data }; + + let res_body = postb(client, "set", entry).unwrap(); + serde_json::from_str(&res_body).unwrap() +} + +pub fn poll_for_broadcasts( + client: &Client, + party_num: u16, + n: u16, + delay: Duration, + round: &str, + sender_uuid: String, +) -> Vec { + let mut ans_vec = Vec::new(); + for i in 1..=n { + if i != party_num { + let key = format!("{}-{}-{}", i, round, sender_uuid); + let index = Index { key }; + loop { + // add delay to allow the server to process request: + thread::sleep(delay); + let res_body = postb(client, "get", index.clone()).unwrap(); + let answer: Result = serde_json::from_str(&res_body).unwrap(); + if let Ok(answer) = answer { + ans_vec.push(answer.value); + println!("[{:?}] party {:?} => party {:?}", round, i, party_num); + break; + } + } + } + } + ans_vec +} + +pub fn poll_for_p2p( + client: &Client, + party_num: u16, + n: u16, + delay: Duration, + round: &str, + sender_uuid: String, +) -> Vec { + let mut ans_vec = Vec::new(); + for i in 1..=n { + if i != party_num { + let key = format!("{}-{}-{}-{}", i, party_num, round, sender_uuid); + let index = Index { key }; + loop { + // add delay to allow the server to process request: + thread::sleep(delay); + let res_body = postb(client, "get", index.clone()).unwrap(); + let answer: Result = serde_json::from_str(&res_body).unwrap(); + if let Ok(answer) = answer { + ans_vec.push(answer.value); + println!("[{:?}] party {:?} => party {:?}", round, i, party_num); + break; + } + } + } + } + ans_vec +} + +pub fn check_sig( + r: &Scalar, + s: &Scalar, + msg: &BigInt, + pk: &Point, +) { + use secp256k1::{Message, PublicKey, Signature, SECP256K1}; + + let raw_msg = BigInt::to_bytes(msg); + let mut msg: Vec = Vec::new(); // padding + msg.extend(vec![0u8; 32 - raw_msg.len()]); + msg.extend(raw_msg.iter()); + + let msg = Message::from_slice(msg.as_slice()).unwrap(); + let mut raw_pk = pk.to_bytes(false).to_vec(); + if raw_pk.len() == 64 { + raw_pk.insert(0, 4u8); + } + let pk = PublicKey::from_slice(&raw_pk).unwrap(); + + let mut compact: Vec = Vec::new(); + let bytes_r = &r.to_bytes().to_vec(); + compact.extend(vec![0u8; 32 - bytes_r.len()]); + compact.extend(bytes_r.iter()); + + let bytes_s = &s.to_bytes().to_vec(); + compact.extend(vec![0u8; 32 - bytes_s.len()]); + compact.extend(bytes_s.iter()); + + let secp_sig = Signature::from_compact(compact.as_slice()).unwrap(); + + let is_correct = SECP256K1.verify(&msg, &secp_sig, &pk).is_ok(); + assert!(is_correct); +} diff --git a/multi-party-ecdsa/examples/gg18_keygen_client.rs b/multi-party-ecdsa/examples/gg18_keygen_client.rs new file mode 100644 index 00000000..f65845c0 --- /dev/null +++ b/multi-party-ecdsa/examples/gg18_keygen_client.rs @@ -0,0 +1,271 @@ +#![allow(non_snake_case)] +/// to run: +/// 1: go to rocket_server -> cargo run +/// 2: cargo run from PARTIES number of terminals +use curv::{ + arithmetic::traits::Converter, + cryptographic_primitives::{ + proofs::sigma_dlog::DLogProof, secret_sharing::feldman_vss::VerifiableSS, + }, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, +}; +use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2018::party_i::{ + KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Keys, Parameters, +}; +use paillier::EncryptionKey; +use reqwest::Client; +use sha2::Sha256; +use std::{env, fs, time}; + +mod common; +use common::{ + aes_decrypt, aes_encrypt, broadcast, poll_for_broadcasts, poll_for_p2p, postb, sendp2p, Params, + PartySignup, AEAD, AES_KEY_BYTES_LEN, +}; + +fn main() { + if env::args().nth(3).is_some() { + panic!("too many arguments") + } + if env::args().nth(2).is_none() { + panic!("too few arguments") + } + //read parameters: + let data = fs::read_to_string("params.json") + .expect("Unable to read params, make sure config file is present in the same folder "); + let params: Params = serde_json::from_str(&data).unwrap(); + let PARTIES: u16 = params.parties.parse::().unwrap(); + let THRESHOLD: u16 = params.threshold.parse::().unwrap(); + + let client = Client::new(); + + // delay: + let delay = time::Duration::from_millis(25); + let params = Parameters { + threshold: THRESHOLD, + share_count: PARTIES, + }; + + //signup: + let (party_num_int, uuid) = match signup(&client).unwrap() { + PartySignup { number, uuid } => (number, uuid), + }; + println!("number: {:?}, uuid: {:?}", party_num_int, uuid); + + let party_keys = Keys::create(party_num_int); + let (bc_i, decom_i) = party_keys.phase1_broadcast_phase3_proof_of_correct_key(); + + // send commitment to ephemeral public keys, get round 1 commitments of other parties + assert!(broadcast( + &client, + party_num_int, + "round1", + serde_json::to_string(&bc_i).unwrap(), + uuid.clone() + ) + .is_ok()); + let round1_ans_vec = poll_for_broadcasts( + &client, + party_num_int, + PARTIES, + delay, + "round1", + uuid.clone(), + ); + + let mut bc1_vec = round1_ans_vec + .iter() + .map(|m| serde_json::from_str::(m).unwrap()) + .collect::>(); + + bc1_vec.insert(party_num_int as usize - 1, bc_i); + + // send ephemeral public keys and check commitments correctness + assert!(broadcast( + &client, + party_num_int, + "round2", + serde_json::to_string(&decom_i).unwrap(), + uuid.clone() + ) + .is_ok()); + let round2_ans_vec = poll_for_broadcasts( + &client, + party_num_int, + PARTIES, + delay, + "round2", + uuid.clone(), + ); + + let mut j = 0; + let mut point_vec: Vec> = Vec::new(); + let mut decom_vec: Vec = Vec::new(); + let mut enc_keys: Vec> = Vec::new(); + for i in 1..=PARTIES { + if i == party_num_int { + point_vec.push(decom_i.y_i.clone()); + decom_vec.push(decom_i.clone()); + } else { + let decom_j: KeyGenDecommitMessage1 = serde_json::from_str(&round2_ans_vec[j]).unwrap(); + point_vec.push(decom_j.y_i.clone()); + decom_vec.push(decom_j.clone()); + let key_bn: BigInt = (decom_j.y_i.clone() * party_keys.u_i.clone()) + .x_coord() + .unwrap(); + let key_bytes = BigInt::to_bytes(&key_bn); + let mut template: Vec = vec![0u8; AES_KEY_BYTES_LEN - key_bytes.len()]; + template.extend_from_slice(&key_bytes[..]); + enc_keys.push(template); + j += 1; + } + } + + let (head, tail) = point_vec.split_at(1); + let y_sum = tail.iter().fold(head[0].clone(), |acc, x| acc + x); + + let (vss_scheme, secret_shares, _index) = party_keys + .phase1_verify_com_phase3_verify_correct_key_phase2_distribute( + ¶ms, &decom_vec, &bc1_vec, + ) + .expect("invalid key"); + + ////////////////////////////////////////////////////////////////////////////// + + let mut j = 0; + for (k, i) in (1..=PARTIES).enumerate() { + if i != party_num_int { + // prepare encrypted ss for party i: + let key_i = &enc_keys[j]; + let plaintext = BigInt::to_bytes(&secret_shares[k].to_bigint()); + let aead_pack_i = aes_encrypt(key_i, &plaintext); + assert!(sendp2p( + &client, + party_num_int, + i, + "round3", + serde_json::to_string(&aead_pack_i).unwrap(), + uuid.clone() + ) + .is_ok()); + j += 1; + } + } + + let round3_ans_vec = poll_for_p2p( + &client, + party_num_int, + PARTIES, + delay, + "round3", + uuid.clone(), + ); + + let mut j = 0; + let mut party_shares: Vec> = Vec::new(); + for i in 1..=PARTIES { + if i == party_num_int { + party_shares.push(secret_shares[(i - 1) as usize].clone()); + } else { + let aead_pack: AEAD = serde_json::from_str(&round3_ans_vec[j]).unwrap(); + let key_i = &enc_keys[j]; + let out = aes_decrypt(key_i, aead_pack); + let out_bn = BigInt::from_bytes(&out[..]); + let out_fe = Scalar::::from(&out_bn); + party_shares.push(out_fe); + + j += 1; + } + } + + // round 4: send vss commitments + assert!(broadcast( + &client, + party_num_int, + "round4", + serde_json::to_string(&vss_scheme).unwrap(), + uuid.clone() + ) + .is_ok()); + let round4_ans_vec = poll_for_broadcasts( + &client, + party_num_int, + PARTIES, + delay, + "round4", + uuid.clone(), + ); + + let mut j = 0; + let mut vss_scheme_vec: Vec> = Vec::new(); + for i in 1..=PARTIES { + if i == party_num_int { + vss_scheme_vec.push(vss_scheme.clone()); + } else { + let vss_scheme_j: VerifiableSS = + serde_json::from_str(&round4_ans_vec[j]).unwrap(); + vss_scheme_vec.push(vss_scheme_j); + j += 1; + } + } + + let (shared_keys, dlog_proof) = party_keys + .phase2_verify_vss_construct_keypair_phase3_pok_dlog( + ¶ms, + &point_vec, + &party_shares, + &vss_scheme_vec, + party_num_int, + ) + .expect("invalid vss"); + + // round 5: send dlog proof + assert!(broadcast( + &client, + party_num_int, + "round5", + serde_json::to_string(&dlog_proof).unwrap(), + uuid.clone() + ) + .is_ok()); + let round5_ans_vec = + poll_for_broadcasts(&client, party_num_int, PARTIES, delay, "round5", uuid); + + let mut j = 0; + let mut dlog_proof_vec: Vec> = Vec::new(); + for i in 1..=PARTIES { + if i == party_num_int { + dlog_proof_vec.push(dlog_proof.clone()); + } else { + let dlog_proof_j: DLogProof = + serde_json::from_str(&round5_ans_vec[j]).unwrap(); + dlog_proof_vec.push(dlog_proof_j); + j += 1; + } + } + Keys::verify_dlog_proofs(¶ms, &dlog_proof_vec, &point_vec).expect("bad dlog proof"); + + //save key to file: + let paillier_key_vec = (0..PARTIES) + .map(|i| bc1_vec[i as usize].e.clone()) + .collect::>(); + + let keygen_json = serde_json::to_string(&( + party_keys, + shared_keys, + party_num_int, + vss_scheme_vec, + paillier_key_vec, + y_sum, + )) + .unwrap(); + fs::write(env::args().nth(2).unwrap(), keygen_json).expect("Unable to save !"); +} + +pub fn signup(client: &Client) -> Result { + let key = "signup-keygen".to_string(); + + let res_body = postb(client, "signupkeygen", key).unwrap(); + serde_json::from_str(&res_body).unwrap() +} diff --git a/multi-party-ecdsa/examples/gg18_sign_client.rs b/multi-party-ecdsa/examples/gg18_sign_client.rs new file mode 100644 index 00000000..1cd39c23 --- /dev/null +++ b/multi-party-ecdsa/examples/gg18_sign_client.rs @@ -0,0 +1,531 @@ +#![allow(non_snake_case)] + +use curv::{ + arithmetic::traits::*, + cryptographic_primitives::{ + proofs::sigma_correct_homomorphic_elgamal_enc::HomoELGamalProof, + proofs::sigma_dlog::DLogProof, secret_sharing::feldman_vss::VerifiableSS, + }, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, +}; +use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2018::party_i::{ + Keys, LocalSignature, PartyPrivate, Phase5ADecom1, Phase5Com1, Phase5Com2, Phase5DDecom2, + SharedKeys, SignBroadcastPhase1, SignDecommitPhase1, SignKeys, +}; +use multi_party_ecdsa::utilities::mta::*; +use sha2::Sha256; + +use paillier::EncryptionKey; +use reqwest::Client; +use std::{env, fs, time}; + +mod common; +use common::{ + broadcast, check_sig, poll_for_broadcasts, poll_for_p2p, postb, sendp2p, Params, PartySignup, +}; + +#[allow(clippy::cognitive_complexity)] +fn main() { + if env::args().nth(4).is_some() { + panic!("too many arguments") + } + if env::args().nth(3).is_none() { + panic!("too few arguments") + } + let message_str = env::args().nth(3).unwrap_or_else(|| "".to_string()); + let message = match hex::decode(message_str.clone()) { + Ok(x) => x, + Err(_e) => message_str.as_bytes().to_vec(), + }; + let message = &message[..]; + let client = Client::new(); + // delay: + let delay = time::Duration::from_millis(25); + // read key file + let data = fs::read_to_string(env::args().nth(2).unwrap()) + .expect("Unable to load keys, did you run keygen first? "); + let (party_keys, shared_keys, party_id, vss_scheme_vec, paillier_key_vector, y_sum): ( + Keys, + SharedKeys, + u16, + Vec>, + Vec, + Point, + ) = serde_json::from_str(&data).unwrap(); + + //read parameters: + let data = fs::read_to_string("params.json") + .expect("Unable to read params, make sure config file is present in the same folder "); + let params: Params = serde_json::from_str(&data).unwrap(); + let THRESHOLD = params.threshold.parse::().unwrap(); + + //signup: + let (party_num_int, uuid) = match signup(&client).unwrap() { + PartySignup { number, uuid } => (number, uuid), + }; + println!("number: {:?}, uuid: {:?}", party_num_int, uuid); + + // round 0: collect signers IDs + assert!(broadcast( + &client, + party_num_int, + "round0", + serde_json::to_string(&party_id).unwrap(), + uuid.clone() + ) + .is_ok()); + let round0_ans_vec = poll_for_broadcasts( + &client, + party_num_int, + THRESHOLD + 1, + delay, + "round0", + uuid.clone(), + ); + + let mut j = 0; + let mut signers_vec: Vec = Vec::new(); + for i in 1..=THRESHOLD + 1 { + if i == party_num_int { + signers_vec.push(party_id - 1); + } else { + let signer_j: u16 = serde_json::from_str(&round0_ans_vec[j]).unwrap(); + signers_vec.push(signer_j - 1); + j += 1; + } + } + + let private = PartyPrivate::set_private(party_keys.clone(), shared_keys); + + let sign_keys = SignKeys::create( + &private, + &vss_scheme_vec[usize::from(signers_vec[usize::from(party_num_int - 1)])], + signers_vec[usize::from(party_num_int - 1)], + &signers_vec, + ); + + let xi_com_vec = Keys::get_commitments_to_xi(&vss_scheme_vec); + ////////////////////////////////////////////////////////////////////////////// + let (com, decommit) = sign_keys.phase1_broadcast(); + let (m_a_k, _) = MessageA::a(&sign_keys.k_i, &party_keys.ek, &[]); + assert!(broadcast( + &client, + party_num_int, + "round1", + serde_json::to_string(&(com.clone(), m_a_k)).unwrap(), + uuid.clone() + ) + .is_ok()); + let round1_ans_vec = poll_for_broadcasts( + &client, + party_num_int, + THRESHOLD + 1, + delay, + "round1", + uuid.clone(), + ); + + let mut j = 0; + let mut bc1_vec: Vec = Vec::new(); + let mut m_a_vec: Vec = Vec::new(); + + for i in 1..THRESHOLD + 2 { + if i == party_num_int { + bc1_vec.push(com.clone()); + // m_a_vec.push(m_a_k.clone()); + } else { + // if signers_vec.contains(&(i as usize)) { + let (bc1_j, m_a_party_j): (SignBroadcastPhase1, MessageA) = + serde_json::from_str(&round1_ans_vec[j]).unwrap(); + bc1_vec.push(bc1_j); + m_a_vec.push(m_a_party_j); + + j += 1; + // } + } + } + assert_eq!(signers_vec.len(), bc1_vec.len()); + + ////////////////////////////////////////////////////////////////////////////// + let mut m_b_gamma_send_vec: Vec = Vec::new(); + let mut beta_vec: Vec> = Vec::new(); + let mut m_b_w_send_vec: Vec = Vec::new(); + let mut ni_vec: Vec> = Vec::new(); + let mut j = 0; + for i in 1..THRESHOLD + 2 { + if i != party_num_int { + let (m_b_gamma, beta_gamma, _, _) = MessageB::b( + &sign_keys.gamma_i, + &paillier_key_vector[usize::from(signers_vec[usize::from(i - 1)])], + m_a_vec[j].clone(), + &[], + ) + .unwrap(); + let (m_b_w, beta_wi, _, _) = MessageB::b( + &sign_keys.w_i, + &paillier_key_vector[usize::from(signers_vec[usize::from(i - 1)])], + m_a_vec[j].clone(), + &[], + ) + .unwrap(); + m_b_gamma_send_vec.push(m_b_gamma); + m_b_w_send_vec.push(m_b_w); + beta_vec.push(beta_gamma); + ni_vec.push(beta_wi); + j += 1; + } + } + + let mut j = 0; + for i in 1..THRESHOLD + 2 { + if i != party_num_int { + assert!(sendp2p( + &client, + party_num_int, + i, + "round2", + serde_json::to_string(&(m_b_gamma_send_vec[j].clone(), m_b_w_send_vec[j].clone())) + .unwrap(), + uuid.clone() + ) + .is_ok()); + j += 1; + } + } + + let round2_ans_vec = poll_for_p2p( + &client, + party_num_int, + THRESHOLD + 1, + delay, + "round2", + uuid.clone(), + ); + + let mut m_b_gamma_rec_vec: Vec = Vec::new(); + let mut m_b_w_rec_vec: Vec = Vec::new(); + + for i in 0..THRESHOLD { + // if signers_vec.contains(&(i as usize)) { + let (m_b_gamma_i, m_b_w_i): (MessageB, MessageB) = + serde_json::from_str(&round2_ans_vec[i as usize]).unwrap(); + m_b_gamma_rec_vec.push(m_b_gamma_i); + m_b_w_rec_vec.push(m_b_w_i); + // } + } + + let mut alpha_vec: Vec> = Vec::new(); + let mut miu_vec: Vec> = Vec::new(); + + let mut j = 0; + for i in 1..THRESHOLD + 2 { + if i != party_num_int { + let m_b = m_b_gamma_rec_vec[j].clone(); + + let alpha_ij_gamma = m_b + .verify_proofs_get_alpha(&party_keys.dk, &sign_keys.k_i) + .expect("wrong dlog or m_b"); + let m_b = m_b_w_rec_vec[j].clone(); + let alpha_ij_wi = m_b + .verify_proofs_get_alpha(&party_keys.dk, &sign_keys.k_i) + .expect("wrong dlog or m_b"); + alpha_vec.push(alpha_ij_gamma.0); + miu_vec.push(alpha_ij_wi.0); + let g_w_i = Keys::update_commitments_to_xi( + &xi_com_vec[usize::from(signers_vec[usize::from(i - 1)])], + &vss_scheme_vec[usize::from(signers_vec[usize::from(i - 1)])], + signers_vec[usize::from(i - 1)], + &signers_vec, + ); + assert_eq!(m_b.b_proof.pk, g_w_i); + j += 1; + } + } + ////////////////////////////////////////////////////////////////////////////// + let delta_i = sign_keys.phase2_delta_i(&alpha_vec, &beta_vec); + let sigma = sign_keys.phase2_sigma_i(&miu_vec, &ni_vec); + + assert!(broadcast( + &client, + party_num_int, + "round3", + serde_json::to_string(&delta_i).unwrap(), + uuid.clone() + ) + .is_ok()); + let round3_ans_vec = poll_for_broadcasts( + &client, + party_num_int, + THRESHOLD + 1, + delay, + "round3", + uuid.clone(), + ); + let mut delta_vec: Vec> = Vec::new(); + format_vec_from_reads( + &round3_ans_vec, + party_num_int as usize, + delta_i, + &mut delta_vec, + ); + let delta_inv = SignKeys::phase3_reconstruct_delta(&delta_vec); + + ////////////////////////////////////////////////////////////////////////////// + // decommit to gamma_i + assert!(broadcast( + &client, + party_num_int, + "round4", + serde_json::to_string(&decommit).unwrap(), + uuid.clone() + ) + .is_ok()); + let round4_ans_vec = poll_for_broadcasts( + &client, + party_num_int, + THRESHOLD + 1, + delay, + "round4", + uuid.clone(), + ); + + let mut decommit_vec: Vec = Vec::new(); + format_vec_from_reads( + &round4_ans_vec, + party_num_int as usize, + decommit, + &mut decommit_vec, + ); + let decomm_i = decommit_vec.remove(usize::from(party_num_int - 1)); + bc1_vec.remove(usize::from(party_num_int - 1)); + let b_proof_vec = (0..m_b_gamma_rec_vec.len()) + .map(|i| &m_b_gamma_rec_vec[i].b_proof) + .collect::>>(); + let R = SignKeys::phase4(&delta_inv, &b_proof_vec, decommit_vec, &bc1_vec) + .expect("bad gamma_i decommit"); + + // adding local g_gamma_i + let R = R + decomm_i.g_gamma_i * delta_inv; + + // we assume the message is already hashed (by the signer). + let message_bn = BigInt::from_bytes(message); + let local_sig = + LocalSignature::phase5_local_sig(&sign_keys.k_i, &message_bn, &R, &sigma, &y_sum); + + let (phase5_com, phase_5a_decom, helgamal_proof, dlog_proof_rho) = + local_sig.phase5a_broadcast_5b_zkproof(); + + //phase (5A) broadcast commit + assert!(broadcast( + &client, + party_num_int, + "round5", + serde_json::to_string(&phase5_com).unwrap(), + uuid.clone() + ) + .is_ok()); + let round5_ans_vec = poll_for_broadcasts( + &client, + party_num_int, + THRESHOLD + 1, + delay, + "round5", + uuid.clone(), + ); + + let mut commit5a_vec: Vec = Vec::new(); + format_vec_from_reads( + &round5_ans_vec, + party_num_int as usize, + phase5_com, + &mut commit5a_vec, + ); + + //phase (5B) broadcast decommit and (5B) ZK proof + assert!(broadcast( + &client, + party_num_int, + "round6", + serde_json::to_string(&( + phase_5a_decom.clone(), + helgamal_proof.clone(), + dlog_proof_rho.clone() + )) + .unwrap(), + uuid.clone() + ) + .is_ok()); + let round6_ans_vec = poll_for_broadcasts( + &client, + party_num_int, + THRESHOLD + 1, + delay, + "round6", + uuid.clone(), + ); + + let mut decommit5a_and_elgamal_and_dlog_vec: Vec<( + Phase5ADecom1, + HomoELGamalProof, + DLogProof, + )> = Vec::new(); + format_vec_from_reads( + &round6_ans_vec, + party_num_int as usize, + (phase_5a_decom.clone(), helgamal_proof, dlog_proof_rho), + &mut decommit5a_and_elgamal_and_dlog_vec, + ); + let decommit5a_and_elgamal_and_dlog_vec_includes_i = + decommit5a_and_elgamal_and_dlog_vec.clone(); + decommit5a_and_elgamal_and_dlog_vec.remove(usize::from(party_num_int - 1)); + commit5a_vec.remove(usize::from(party_num_int - 1)); + let phase_5a_decomm_vec = (0..THRESHOLD) + .map(|i| decommit5a_and_elgamal_and_dlog_vec[i as usize].0.clone()) + .collect::>(); + let phase_5a_elgamal_vec = (0..THRESHOLD) + .map(|i| decommit5a_and_elgamal_and_dlog_vec[i as usize].1.clone()) + .collect::>>(); + let phase_5a_dlog_vec = (0..THRESHOLD) + .map(|i| decommit5a_and_elgamal_and_dlog_vec[i as usize].2.clone()) + .collect::>>(); + let (phase5_com2, phase_5d_decom2) = local_sig + .phase5c( + &phase_5a_decomm_vec, + &commit5a_vec, + &phase_5a_elgamal_vec, + &phase_5a_dlog_vec, + &phase_5a_decom.V_i, + &R, + ) + .expect("error phase5"); + + ////////////////////////////////////////////////////////////////////////////// + assert!(broadcast( + &client, + party_num_int, + "round7", + serde_json::to_string(&phase5_com2).unwrap(), + uuid.clone() + ) + .is_ok()); + let round7_ans_vec = poll_for_broadcasts( + &client, + party_num_int, + THRESHOLD + 1, + delay, + "round7", + uuid.clone(), + ); + + let mut commit5c_vec: Vec = Vec::new(); + format_vec_from_reads( + &round7_ans_vec, + party_num_int as usize, + phase5_com2, + &mut commit5c_vec, + ); + + //phase (5B) broadcast decommit and (5B) ZK proof + assert!(broadcast( + &client, + party_num_int, + "round8", + serde_json::to_string(&phase_5d_decom2).unwrap(), + uuid.clone() + ) + .is_ok()); + let round8_ans_vec = poll_for_broadcasts( + &client, + party_num_int, + THRESHOLD + 1, + delay, + "round8", + uuid.clone(), + ); + + let mut decommit5d_vec: Vec = Vec::new(); + format_vec_from_reads( + &round8_ans_vec, + party_num_int as usize, + phase_5d_decom2, + &mut decommit5d_vec, + ); + + let phase_5a_decomm_vec_includes_i = (0..=THRESHOLD) + .map(|i| { + decommit5a_and_elgamal_and_dlog_vec_includes_i[i as usize] + .0 + .clone() + }) + .collect::>(); + let s_i = local_sig + .phase5d( + &decommit5d_vec, + &commit5c_vec, + &phase_5a_decomm_vec_includes_i, + ) + .expect("bad com 5d"); + + ////////////////////////////////////////////////////////////////////////////// + assert!(broadcast( + &client, + party_num_int, + "round9", + serde_json::to_string(&s_i).unwrap(), + uuid.clone() + ) + .is_ok()); + let round9_ans_vec = + poll_for_broadcasts(&client, party_num_int, THRESHOLD + 1, delay, "round9", uuid); + + let mut s_i_vec: Vec> = Vec::new(); + format_vec_from_reads(&round9_ans_vec, party_num_int as usize, s_i, &mut s_i_vec); + + s_i_vec.remove(usize::from(party_num_int - 1)); + let sig = local_sig + .output_signature(&s_i_vec) + .expect("verification failed"); + println!("party {:?} Output Signature: \n", party_num_int); + println!("R: {:?}", sig.r); + println!("s: {:?} \n", sig.s); + println!("recid: {:?} \n", sig.recid.clone()); + + let sign_json = serde_json::to_string(&( + "r", + BigInt::from_bytes(sig.r.to_bytes().as_ref()).to_str_radix(16), + "s", + BigInt::from_bytes(sig.s.to_bytes().as_ref()).to_str_radix(16), + )) + .unwrap(); + + // check sig against secp256k1 + check_sig(&sig.r, &sig.s, &message_bn, &y_sum); + + fs::write("signature".to_string(), sign_json).expect("Unable to save !"); +} + +fn format_vec_from_reads<'a, T: serde::Deserialize<'a> + Clone>( + ans_vec: &'a [String], + party_num: usize, + value_i: T, + new_vec: &'a mut Vec, +) { + let mut j = 0; + for i in 1..ans_vec.len() + 2 { + if i == party_num { + new_vec.push(value_i.clone()); + } else { + let value_j: T = serde_json::from_str(&ans_vec[j]).unwrap(); + new_vec.push(value_j); + j += 1; + } + } +} + +pub fn signup(client: &Client) -> Result { + let key = "signup-sign".to_string(); + + let res_body = postb(client, "signupsign", key).unwrap(); + serde_json::from_str(&res_body).unwrap() +} diff --git a/multi-party-ecdsa/examples/gg18_sm_manager.rs b/multi-party-ecdsa/examples/gg18_sm_manager.rs new file mode 100644 index 00000000..9bbe55dd --- /dev/null +++ b/multi-party-ecdsa/examples/gg18_sm_manager.rs @@ -0,0 +1,143 @@ +use std::collections::HashMap; +use std::fs; +use std::sync::RwLock; + +use rocket::serde::json::Json; +use rocket::{post, routes, State}; +use uuid::Uuid; + +mod common; +use common::{Entry, Index, Key, Params, PartySignup}; + +#[post("/get", format = "json", data = "")] +fn get( + db_mtx: &State>>, + request: Json, +) -> Json> { + let index: Index = request.0; + let hm = db_mtx.read().unwrap(); + match hm.get(&index.key) { + Some(v) => { + let entry = Entry { + key: index.key, + value: v.clone(), + }; + Json(Ok(entry)) + } + None => Json(Err(())), + } +} + +#[post("/set", format = "json", data = "")] +fn set(db_mtx: &State>>, request: Json) -> Json> { + let entry: Entry = request.0; + let mut hm = db_mtx.write().unwrap(); + hm.insert(entry.key.clone(), entry.value); + Json(Ok(())) +} + +#[post("/signupkeygen", format = "json")] +fn signup_keygen(db_mtx: &State>>) -> Json> { + let data = fs::read_to_string("params.json") + .expect("Unable to read params, make sure config file is present in the same folder "); + let params: Params = serde_json::from_str(&data).unwrap(); + let parties = params.parties.parse::().unwrap(); + + let key = "signup-keygen".to_string(); + + let party_signup = { + let hm = db_mtx.read().unwrap(); + let value = hm.get(&key).unwrap(); + let client_signup: PartySignup = serde_json::from_str(value).unwrap(); + if client_signup.number < parties { + PartySignup { + number: client_signup.number + 1, + uuid: client_signup.uuid, + } + } else { + PartySignup { + number: 1, + uuid: Uuid::new_v4().to_string(), + } + } + }; + + let mut hm = db_mtx.write().unwrap(); + hm.insert(key, serde_json::to_string(&party_signup).unwrap()); + Json(Ok(party_signup)) +} + +#[post("/signupsign", format = "json")] +fn signup_sign(db_mtx: &State>>) -> Json> { + //read parameters: + let data = fs::read_to_string("params.json") + .expect("Unable to read params, make sure config file is present in the same folder "); + let params: Params = serde_json::from_str(&data).unwrap(); + let threshold = params.threshold.parse::().unwrap(); + let key = "signup-sign".to_string(); + + let party_signup = { + let hm = db_mtx.read().unwrap(); + let value = hm.get(&key).unwrap(); + let client_signup: PartySignup = serde_json::from_str(value).unwrap(); + if client_signup.number < threshold + 1 { + PartySignup { + number: client_signup.number + 1, + uuid: client_signup.uuid, + } + } else { + PartySignup { + number: 1, + uuid: Uuid::new_v4().to_string(), + } + } + }; + + let mut hm = db_mtx.write().unwrap(); + hm.insert(key, serde_json::to_string(&party_signup).unwrap()); + Json(Ok(party_signup)) +} + +#[tokio::main] +async fn main() { + // let mut my_config = Config::development(); + // my_config.set_port(18001); + let db: HashMap = HashMap::new(); + let db_mtx = RwLock::new(db); + //rocket::custom(my_config).mount("/", routes![get, set]).manage(db_mtx).launch(); + + ///////////////////////////////////////////////////////////////// + //////////////////////////init signups:////////////////////////// + ///////////////////////////////////////////////////////////////// + + let keygen_key = "signup-keygen".to_string(); + let sign_key = "signup-sign".to_string(); + + let uuid_keygen = Uuid::new_v4().to_string(); + let uuid_sign = Uuid::new_v4().to_string(); + + let party1 = 0; + let party_signup_keygen = PartySignup { + number: party1, + uuid: uuid_keygen, + }; + let party_signup_sign = PartySignup { + number: party1, + uuid: uuid_sign, + }; + { + let mut hm = db_mtx.write().unwrap(); + hm.insert( + keygen_key, + serde_json::to_string(&party_signup_keygen).unwrap(), + ); + hm.insert(sign_key, serde_json::to_string(&party_signup_sign).unwrap()); + } + ///////////////////////////////////////////////////////////////// + rocket::build() + .mount("/", routes![get, set, signup_keygen, signup_sign]) + .manage(db_mtx) + .launch() + .await + .unwrap(); +} diff --git a/multi-party-ecdsa/examples/gg20_keygen.rs b/multi-party-ecdsa/examples/gg20_keygen.rs new file mode 100644 index 00000000..2de51dac --- /dev/null +++ b/multi-party-ecdsa/examples/gg20_keygen.rs @@ -0,0 +1,58 @@ +use anyhow::{anyhow, Context, Result}; +use futures::StreamExt; +use std::path::PathBuf; +use structopt::StructOpt; + +use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::keygen::Keygen; +use round_based::async_runtime::AsyncProtocol; + +mod gg20_sm_client; +use gg20_sm_client::join_computation; + +#[derive(Debug, StructOpt)] +struct Cli { + #[structopt(short, long, default_value = "http://localhost:8000/")] + address: surf::Url, + #[structopt(short, long, default_value = "default-keygen")] + room: String, + #[structopt(short, long)] + output: PathBuf, + + #[structopt(short, long)] + index: u16, + #[structopt(short, long)] + threshold: u16, + #[structopt(short, long)] + number_of_parties: u16, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args: Cli = Cli::from_args(); + let mut output_file = tokio::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(args.output) + .await + .context("cannot create output file")?; + + let (_i, incoming, outgoing) = join_computation(args.address, &args.room) + .await + .context("join computation")?; + + let incoming = incoming.fuse(); + tokio::pin!(incoming); + tokio::pin!(outgoing); + + let keygen = Keygen::new(args.index, args.threshold, args.number_of_parties)?; + let output = AsyncProtocol::new(keygen, incoming, outgoing) + .run() + .await + .map_err(|e| anyhow!("protocol execution terminated with error: {}", e))?; + let output = serde_json::to_vec_pretty(&output).context("serialize output")?; + tokio::io::copy(&mut output.as_slice(), &mut output_file) + .await + .context("save output to file")?; + + Ok(()) +} diff --git a/multi-party-ecdsa/examples/gg20_signing.rs b/multi-party-ecdsa/examples/gg20_signing.rs new file mode 100644 index 00000000..59e3c23b --- /dev/null +++ b/multi-party-ecdsa/examples/gg20_signing.rs @@ -0,0 +1,90 @@ +use std::path::PathBuf; + +use anyhow::{anyhow, Context, Result}; +use futures::{SinkExt, StreamExt, TryStreamExt}; +use structopt::StructOpt; + +use curv::arithmetic::Converter; +use curv::BigInt; + +use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::sign::{ + OfflineStage, SignManual, +}; +use round_based::async_runtime::AsyncProtocol; +use round_based::Msg; + +mod gg20_sm_client; +use gg20_sm_client::join_computation; + +#[derive(Debug, StructOpt)] +struct Cli { + #[structopt(short, long, default_value = "http://localhost:8000/")] + address: surf::Url, + #[structopt(short, long, default_value = "default-signing")] + room: String, + #[structopt(short, long)] + local_share: PathBuf, + + #[structopt(short, long, use_delimiter(true))] + parties: Vec, + #[structopt(short, long)] + data_to_sign: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args: Cli = Cli::from_args(); + let local_share = tokio::fs::read(args.local_share) + .await + .context("cannot read local share")?; + let local_share = serde_json::from_slice(&local_share).context("parse local share")?; + let number_of_parties = args.parties.len(); + + let (i, incoming, outgoing) = + join_computation(args.address.clone(), &format!("{}-offline", args.room)) + .await + .context("join offline computation")?; + + let incoming = incoming.fuse(); + tokio::pin!(incoming); + tokio::pin!(outgoing); + + let signing = OfflineStage::new(i, args.parties, local_share)?; + let completed_offline_stage = AsyncProtocol::new(signing, incoming, outgoing) + .run() + .await + .map_err(|e| anyhow!("protocol execution terminated with error: {}", e))?; + + let (i, incoming, outgoing) = join_computation(args.address, &format!("{}-online", args.room)) + .await + .context("join online computation")?; + + tokio::pin!(incoming); + tokio::pin!(outgoing); + + let (signing, partial_signature) = SignManual::new( + BigInt::from_bytes(args.data_to_sign.as_bytes()), + completed_offline_stage, + )?; + + outgoing + .send(Msg { + sender: i, + receiver: None, + body: partial_signature, + }) + .await?; + + let partial_signatures: Vec<_> = incoming + .take(number_of_parties - 1) + .map_ok(|msg| msg.body) + .try_collect() + .await?; + let signature = signing + .complete(&partial_signatures) + .context("online stage failed")?; + let signature = serde_json::to_string(&signature).context("serialize signature")?; + println!("{}", signature); + + Ok(()) +} diff --git a/multi-party-ecdsa/examples/gg20_sm_client.rs b/multi-party-ecdsa/examples/gg20_sm_client.rs new file mode 100644 index 00000000..f28c44dd --- /dev/null +++ b/multi-party-ecdsa/examples/gg20_sm_client.rs @@ -0,0 +1,159 @@ +use std::convert::TryInto; + +use anyhow::{Context, Result}; +use futures::{Sink, Stream, StreamExt, TryStreamExt}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use structopt::StructOpt; + +use round_based::Msg; + +pub async fn join_computation( + address: surf::Url, + room_id: &str, +) -> Result<( + u16, + impl Stream>>, + impl Sink, Error = anyhow::Error>, +)> +where + M: Serialize + DeserializeOwned, +{ + let client = SmClient::new(address, room_id).context("construct SmClient")?; + + // Construct channel of incoming messages + let incoming = client + .subscribe() + .await + .context("subscribe")? + .and_then(|msg| async move { + serde_json::from_str::>(&msg).context("deserialize message") + }); + + // Obtain party index + let index = client.issue_index().await.context("issue an index")?; + + // Ignore incoming messages addressed to someone else + let incoming = incoming.try_filter(move |msg| { + futures::future::ready( + msg.sender != index && (msg.receiver.is_none() || msg.receiver == Some(index)), + ) + }); + + // Construct channel of outgoing messages + let outgoing = futures::sink::unfold(client, |client, message: Msg| async move { + let serialized = serde_json::to_string(&message).context("serialize message")?; + client + .broadcast(&serialized) + .await + .context("broadcast message")?; + Ok::<_, anyhow::Error>(client) + }); + + Ok((index, incoming, outgoing)) +} + +pub struct SmClient { + http_client: surf::Client, +} + +impl SmClient { + pub fn new(address: surf::Url, room_id: &str) -> Result { + let config = surf::Config::new() + .set_base_url(address.join(&format!("rooms/{}/", room_id))?) + .set_timeout(None); + Ok(Self { + http_client: config.try_into()?, + }) + } + + pub async fn issue_index(&self) -> Result { + let response = self + .http_client + .post("issue_unique_idx") + .recv_json::() + .await + .map_err(|e| e.into_inner())?; + Ok(response.unique_idx) + } + + pub async fn broadcast(&self, message: &str) -> Result<()> { + self.http_client + .post("broadcast") + .body(message) + .await + .map_err(|e| e.into_inner())?; + Ok(()) + } + + pub async fn subscribe(&self) -> Result>> { + let response = self + .http_client + .get("subscribe") + .await + .map_err(|e| e.into_inner())?; + let events = async_sse::decode(response); + Ok(events.filter_map(|msg| async { + match msg { + Ok(async_sse::Event::Message(msg)) => Some( + String::from_utf8(msg.into_bytes()) + .context("SSE message is not valid UTF-8 string"), + ), + Ok(_) => { + // ignore other types of events + None + } + Err(e) => Some(Err(e.into_inner())), + } + })) + } +} + +#[derive(Deserialize, Debug)] +struct IssuedUniqueIdx { + unique_idx: u16, +} + +#[derive(StructOpt, Debug)] +struct Cli { + #[structopt(short, long)] + address: surf::Url, + #[structopt(short, long)] + room: String, + #[structopt(subcommand)] + cmd: Cmd, +} + +#[derive(StructOpt, Debug)] +enum Cmd { + Subscribe, + Broadcast { + #[structopt(short, long)] + message: String, + }, + IssueIdx, +} + +#[tokio::main] +#[allow(dead_code)] +async fn main() -> Result<()> { + let args: Cli = Cli::from_args(); + let client = SmClient::new(args.address, &args.room).context("create SmClient")?; + match args.cmd { + Cmd::Broadcast { message } => client + .broadcast(&message) + .await + .context("broadcast message")?, + Cmd::IssueIdx => { + let index = client.issue_index().await.context("issue index")?; + println!("Index: {}", index); + } + Cmd::Subscribe => { + let messages = client.subscribe().await.context("subsribe")?; + tokio::pin!(messages); + while let Some(message) = messages.next().await { + println!("{:?}", message); + } + } + } + Ok(()) +} diff --git a/multi-party-ecdsa/examples/gg20_sm_manager.rs b/multi-party-ecdsa/examples/gg20_sm_manager.rs new file mode 100644 index 00000000..087f37ca --- /dev/null +++ b/multi-party-ecdsa/examples/gg20_sm_manager.rs @@ -0,0 +1,193 @@ +use std::collections::hash_map::{Entry, HashMap}; +use std::sync::{ + atomic::{AtomicU16, Ordering}, + Arc, +}; + +use futures::Stream; +use rocket::data::ToByteUnit; +use rocket::http::Status; +use rocket::request::{FromRequest, Outcome, Request}; +use rocket::response::stream::{stream, Event, EventStream}; +use rocket::serde::json::Json; +use rocket::State; +use serde::{Deserialize, Serialize}; +use tokio::sync::{Notify, RwLock}; + +#[rocket::get("/rooms//subscribe")] +async fn subscribe( + db: &State, + mut shutdown: rocket::Shutdown, + last_seen_msg: LastEventId, + room_id: &str, +) -> EventStream> { + let room = db.get_room_or_create_empty(room_id).await; + let mut subscription = room.subscribe(last_seen_msg.0); + EventStream::from(stream! { + loop { + let (id, msg) = tokio::select! { + message = subscription.next() => message, + _ = &mut shutdown => return, + }; + yield Event::data(msg) + .event("new-message") + .id(id.to_string()) + } + }) +} + +#[rocket::post("/rooms//issue_unique_idx")] +async fn issue_idx(db: &State, room_id: &str) -> Json { + let room = db.get_room_or_create_empty(room_id).await; + let idx = room.issue_unique_idx(); + Json::from(IssuedUniqueIdx { unique_idx: idx }) +} + +#[rocket::post("/rooms//broadcast", data = "")] +async fn broadcast(db: &State, room_id: &str, message: String) -> Status { + let room = db.get_room_or_create_empty(room_id).await; + room.publish(message).await; + Status::Ok +} + +struct Db { + rooms: RwLock>>, +} + +struct Room { + messages: RwLock>, + message_appeared: Notify, + subscribers: AtomicU16, + next_idx: AtomicU16, +} + +impl Db { + pub fn empty() -> Self { + Self { + rooms: RwLock::new(HashMap::new()), + } + } + + pub async fn get_room_or_create_empty(&self, room_id: &str) -> Arc { + let rooms = self.rooms.read().await; + if let Some(room) = rooms.get(room_id) { + // If no one is watching this room - we need to clean it up first + if !room.is_abandoned() { + return room.clone(); + } + } + drop(rooms); + + let mut rooms = self.rooms.write().await; + match rooms.entry(room_id.to_owned()) { + Entry::Occupied(entry) if !entry.get().is_abandoned() => entry.get().clone(), + Entry::Occupied(entry) => { + let room = Arc::new(Room::empty()); + *entry.into_mut() = room.clone(); + room + } + Entry::Vacant(entry) => entry.insert(Arc::new(Room::empty())).clone(), + } + } +} + +impl Room { + pub fn empty() -> Self { + Self { + messages: RwLock::new(vec![]), + message_appeared: Notify::new(), + subscribers: AtomicU16::new(0), + next_idx: AtomicU16::new(1), + } + } + + pub async fn publish(self: &Arc, message: String) { + let mut messages = self.messages.write().await; + messages.push(message); + self.message_appeared.notify_waiters(); + } + + pub fn subscribe(self: Arc, last_seen_msg: Option) -> Subscription { + self.subscribers.fetch_add(1, Ordering::SeqCst); + Subscription { + room: self, + next_event: last_seen_msg.map(|i| i + 1).unwrap_or(0), + } + } + + pub fn is_abandoned(&self) -> bool { + self.subscribers.load(Ordering::SeqCst) == 0 + } + + pub fn issue_unique_idx(&self) -> u16 { + self.next_idx.fetch_add(1, Ordering::Relaxed) + } +} + +struct Subscription { + room: Arc, + next_event: u16, +} + +impl Subscription { + pub async fn next(&mut self) -> (u16, String) { + loop { + let history = self.room.messages.read().await; + if let Some(msg) = history.get(usize::from(self.next_event)) { + let event_id = self.next_event; + self.next_event = event_id + 1; + return (event_id, msg.clone()); + } + let notification = self.room.message_appeared.notified(); + drop(history); + notification.await; + } + } +} + +impl Drop for Subscription { + fn drop(&mut self) { + self.room.subscribers.fetch_sub(1, Ordering::SeqCst); + } +} + +/// Represents a header Last-Event-ID +struct LastEventId(Option); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for LastEventId { + type Error = &'static str; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + let header = request + .headers() + .get_one("Last-Event-ID") + .map(|id| id.parse::()); + match header { + Some(Ok(last_seen_msg)) => Outcome::Success(LastEventId(Some(last_seen_msg))), + Some(Err(_parse_err)) => { + Outcome::Failure((Status::BadRequest, "last seen msg id is not valid")) + } + None => Outcome::Success(LastEventId(None)), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +struct IssuedUniqueIdx { + unique_idx: u16, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let figment = rocket::Config::figment().merge(( + "limits", + rocket::data::Limits::new().limit("string", 100.megabytes()), + )); + rocket::custom(figment) + .mount("/", rocket::routes![subscribe, issue_idx, broadcast]) + .manage(Db::empty()) + .launch() + .await?; + Ok(()) +} diff --git a/multi-party-ecdsa/kzen-dev-setup.sh b/multi-party-ecdsa/kzen-dev-setup.sh new file mode 100755 index 00000000..f926700c --- /dev/null +++ b/multi-party-ecdsa/kzen-dev-setup.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +echo "KZen dev Setup" +echo "Installing Git hooks..." +git clone https://github.com/KZen-networks/scripts.git +chmod -R +x scripts/git/hooks/rust/ +cp scripts/git/hooks/rust/* .git/hooks/ +rm -rf scripts +echo "Done." \ No newline at end of file diff --git a/multi-party-ecdsa/params.json b/multi-party-ecdsa/params.json new file mode 100644 index 00000000..8dcdefd6 --- /dev/null +++ b/multi-party-ecdsa/params.json @@ -0,0 +1 @@ +{"parties":"3", "threshold":"1"} diff --git a/multi-party-ecdsa/src/lib.rs b/multi-party-ecdsa/src/lib.rs new file mode 100644 index 00000000..2b6be622 --- /dev/null +++ b/multi-party-ecdsa/src/lib.rs @@ -0,0 +1,49 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +#![allow(clippy::many_single_char_names)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::type_complexity)] + +pub mod protocols; +pub mod utilities; +use std::fmt; + +pub use protocols::multi_party_ecdsa::gg_2020::state_machine::traits::MessageRoundID; + +#[derive(Copy, PartialEq, Eq, Clone, Debug)] +pub enum Error { + InvalidKey, + InvalidSS, + InvalidCom, + InvalidSig, + Phase5BadSum, + Phase6Error, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Error::*; + match *self { + InvalidKey => write!(f, "InvalidKey"), + InvalidSS => write!(f, "InvalidSS"), + InvalidCom => write!(f, "InvalidCom"), + InvalidSig => write!(f, "InvalidSig"), + Phase5BadSum => write!(f, "Phase5BadSum"), + Phase6Error => write!(f, "Phase6Error"), + } + } +} diff --git a/multi-party-ecdsa/src/protocols/mod.rs b/multi-party-ecdsa/src/protocols/mod.rs new file mode 100644 index 00000000..9c17a1a0 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/mod.rs @@ -0,0 +1,18 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +pub mod multi_party_ecdsa; +pub mod two_party_ecdsa; diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/mod.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/mod.rs new file mode 100644 index 00000000..c526d54f --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/mod.rs @@ -0,0 +1,23 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +pub mod party_i; + +#[cfg(test)] +mod test; + +use curv::cryptographic_primitives::secret_sharing::feldman_vss::VerifiableSS as VSS; +pub type VerifiableSS = VSS; diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/party_i.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/party_i.rs new file mode 100644 index 00000000..0eaf37df --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/party_i.rs @@ -0,0 +1,738 @@ +#![allow(non_snake_case)] + +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +use std::convert::TryFrom; + +use centipede::juggling::proof_system::{Helgamalsegmented, Witness}; +use centipede::juggling::segmentation::Msegmentation; +use curv::arithmetic::traits::*; +use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; +use curv::cryptographic_primitives::commitments::traits::Commitment; +use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; +use curv::cryptographic_primitives::proofs::sigma_correct_homomorphic_elgamal_enc::*; +use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; +use curv::elliptic::curves::{Curve, Point, Scalar, Secp256k1}; +use curv::BigInt; +use paillier::{ + Decrypt, DecryptionKey, EncryptionKey, KeyGeneration, Paillier, RawCiphertext, RawPlaintext, +}; +use sha2::Sha256; +use zk_paillier::zkproofs::NiCorrectKeyProof; + +use serde::{Deserialize, Serialize}; + +use crate::Error::{self, InvalidCom, InvalidKey, InvalidSS, InvalidSig}; + +use super::VerifiableSS; + +const SECURITY: usize = 256; + +#[derive(Debug)] +pub struct Parameters { + pub threshold: u16, //t + pub share_count: u16, //n +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Keys { + pub u_i: Scalar, + pub y_i: Point, + pub dk: DecryptionKey, + pub ek: EncryptionKey, + pub party_index: u16, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PartyPrivate { + u_i: Scalar, + x_i: Scalar, + dk: DecryptionKey, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyGenBroadcastMessage1 { + pub e: EncryptionKey, + pub com: BigInt, + pub correct_key_proof: NiCorrectKeyProof, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyGenDecommitMessage1 { + pub blind_factor: BigInt, + pub y_i: Point, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SharedKeys { + pub y: Point, + pub x_i: Scalar, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignKeys { + pub w_i: Scalar, + pub g_w_i: Point, + pub k_i: Scalar, + pub gamma_i: Scalar, + pub g_gamma_i: Point, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignBroadcastPhase1 { + pub com: BigInt, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignDecommitPhase1 { + pub blind_factor: BigInt, + pub g_gamma_i: Point, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LocalSignature { + pub l_i: Scalar, + pub rho_i: Scalar, + pub R: Point, + pub s_i: Scalar, + pub m: BigInt, + pub y: Point, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Phase5Com1 { + pub com: BigInt, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Phase5Com2 { + pub com: BigInt, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Phase5ADecom1 { + pub V_i: Point, + pub A_i: Point, + pub B_i: Point, + pub blind_factor: BigInt, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Phase5DDecom2 { + pub u_i: Point, + pub t_i: Point, + pub blind_factor: BigInt, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignatureRecid { + pub r: Scalar, + pub s: Scalar, + pub recid: u8, +} + +impl Keys { + pub fn create(index: u16) -> Self { + let u = Scalar::::random(); + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair().keys(); + + Self { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + } + } + + // we recommend using safe primes if the code is used in production + pub fn create_safe_prime(index: u16) -> Keys { + let u = Scalar::::random(); + let y = Point::generator() * &u; + + let (ek, dk) = Paillier::keypair_safe_primes().keys(); + + Keys { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + } + } + pub fn create_from(u: Scalar, index: u16) -> Keys { + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair().keys(); + + Self { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + } + } + + pub fn phase1_broadcast_phase3_proof_of_correct_key( + &self, + ) -> (KeyGenBroadcastMessage1, KeyGenDecommitMessage1) { + let blind_factor = BigInt::sample(SECURITY); + let correct_key_proof = NiCorrectKeyProof::proof(&self.dk, None); + let com = HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(self.y_i.to_bytes(true).as_ref()), + &blind_factor, + ); + let bcm1 = KeyGenBroadcastMessage1 { + e: self.ek.clone(), + com, + correct_key_proof, + }; + let decom1 = KeyGenDecommitMessage1 { + blind_factor, + y_i: self.y_i.clone(), + }; + (bcm1, decom1) + } + + #[allow(clippy::type_complexity)] + pub fn phase1_verify_com_phase3_verify_correct_key_phase2_distribute( + &self, + params: &Parameters, + decom_vec: &[KeyGenDecommitMessage1], + bc1_vec: &[KeyGenBroadcastMessage1], + ) -> Result<(VerifiableSS, Vec>, u16), Error> { + // test length: + assert_eq!(decom_vec.len(), usize::from(params.share_count)); + assert_eq!(bc1_vec.len(), usize::from(params.share_count)); + // test paillier correct key and test decommitments + let correct_key_correct_decom_all = (0..bc1_vec.len()).all(|i| { + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(decom_vec[i].y_i.to_bytes(true).as_ref()), + &decom_vec[i].blind_factor, + ) == bc1_vec[i].com + && bc1_vec[i] + .correct_key_proof + .verify(&bc1_vec[i].e, zk_paillier::zkproofs::SALT_STRING) + .is_ok() + }); + + let (vss_scheme, secret_shares) = + VerifiableSS::share(params.threshold, params.share_count, &self.u_i); + if correct_key_correct_decom_all { + Ok((vss_scheme, secret_shares.to_vec(), self.party_index)) + } else { + Err(InvalidKey) + } + } + + pub fn phase2_verify_vss_construct_keypair_phase3_pok_dlog( + &self, + params: &Parameters, + y_vec: &[Point], + secret_shares_vec: &[Scalar], + vss_scheme_vec: &[VerifiableSS], + index: u16, + ) -> Result<(SharedKeys, DLogProof), Error> { + assert_eq!(y_vec.len(), usize::from(params.share_count)); + assert_eq!(secret_shares_vec.len(), usize::from(params.share_count)); + assert_eq!(vss_scheme_vec.len(), usize::from(params.share_count)); + + let correct_ss_verify = (0..y_vec.len()).all(|i| { + vss_scheme_vec[i] + .validate_share(&secret_shares_vec[i], index) + .is_ok() + && vss_scheme_vec[i].commitments[0] == y_vec[i] + }); + + if correct_ss_verify { + let y: Point = y_vec.iter().sum(); + let x_i: Scalar = secret_shares_vec.iter().sum(); + let dlog_proof = DLogProof::prove(&x_i); + Ok((SharedKeys { y, x_i }, dlog_proof)) + } else { + Err(InvalidSS) + } + } + + pub fn get_commitments_to_xi( + vss_scheme_vec: &[VerifiableSS], + ) -> Vec> { + let len = vss_scheme_vec.len(); + (1..=u16::try_from(len).unwrap()) + .map(|i| { + (0..len) + .map(|j| vss_scheme_vec[j].get_point_commitment(i)) + .sum() + }) + .collect::>>() + } + + pub fn update_commitments_to_xi( + comm: &Point, + vss_scheme: &VerifiableSS, + index: u16, + s: &[u16], + ) -> Point { + let li = + VerifiableSS::::map_share_to_new_params(&vss_scheme.parameters, index, s); + comm * &li + } + + pub fn verify_dlog_proofs( + params: &Parameters, + dlog_proofs_vec: &[DLogProof], + y_vec: &[Point], + ) -> Result<(), Error> { + assert_eq!(y_vec.len(), usize::from(params.share_count)); + assert_eq!(dlog_proofs_vec.len(), usize::from(params.share_count)); + + let xi_dlog_verify = + (0..y_vec.len()).all(|i| DLogProof::verify(&dlog_proofs_vec[i]).is_ok()); + + if xi_dlog_verify { + Ok(()) + } else { + Err(InvalidKey) + } + } +} + +impl PartyPrivate { + pub fn set_private(key: Keys, shared_key: SharedKeys) -> Self { + Self { + u_i: key.u_i, + x_i: shared_key.x_i, + dk: key.dk, + } + } + + pub fn y_i(&self) -> Point { + Point::generator() * &self.u_i + } + + pub fn decrypt(&self, ciphertext: BigInt) -> RawPlaintext { + Paillier::decrypt(&self.dk, &RawCiphertext::from(ciphertext)) + } + + pub fn refresh_private_key(&self, factor: &Scalar, index: u16) -> Keys { + let u: Scalar = &self.u_i + factor; + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair().keys(); + + Keys { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + } + } + + // we recommend using safe primes if the code is used in production + pub fn refresh_private_key_safe_prime(&self, factor: &Scalar, index: u16) -> Keys { + let u: Scalar = &self.u_i + factor; + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair_safe_primes().keys(); + + Keys { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + } + } + + // used for verifiable recovery + pub fn to_encrypted_segment( + &self, + segment_size: usize, + num_of_segments: usize, + pub_ke_y: &Point, + g: &Point, + ) -> (Witness, Helgamalsegmented) { + Msegmentation::to_encrypted_segments(&self.u_i, &segment_size, num_of_segments, pub_ke_y, g) + } + + pub fn update_private_key( + &self, + factor_u_i: &Scalar, + factor_x_i: &Scalar, + ) -> Self { + PartyPrivate { + u_i: &self.u_i + factor_u_i, + x_i: &self.x_i + factor_x_i, + dk: self.dk.clone(), + } + } +} + +impl SignKeys { + pub fn create( + private: &PartyPrivate, + vss_scheme: &VerifiableSS, + index: u16, + s: &[u16], + ) -> Self { + let li = + VerifiableSS::::map_share_to_new_params(&vss_scheme.parameters, index, s); + let w_i = li * &private.x_i; + let g = Point::generator(); + let g_w_i = g * &w_i; + let gamma_i = Scalar::::random(); + let g_gamma_i = g * &gamma_i; + + Self { + w_i, + g_w_i, + k_i: Scalar::::random(), + gamma_i, + g_gamma_i, + } + } + + pub fn phase1_broadcast(&self) -> (SignBroadcastPhase1, SignDecommitPhase1) { + let blind_factor = BigInt::sample(SECURITY); + let g = Point::generator(); + let g_gamma_i = g * &self.gamma_i; + let com = HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(g_gamma_i.to_bytes(true).as_ref()), + &blind_factor, + ); + + ( + SignBroadcastPhase1 { com }, + SignDecommitPhase1 { + blind_factor, + g_gamma_i: self.g_gamma_i.clone(), + }, + ) + } + + pub fn phase2_delta_i( + &self, + alpha_vec: &[Scalar], + beta_vec: &[Scalar], + ) -> Scalar { + assert_eq!(alpha_vec.len(), beta_vec.len()); + let ki_gamma_i = &self.k_i * &self.gamma_i; + ki_gamma_i + alpha_vec.iter().chain(beta_vec).sum::>() + } + + pub fn phase2_sigma_i( + &self, + miu_vec: &[Scalar], + ni_vec: &[Scalar], + ) -> Scalar { + assert_eq!(miu_vec.len(), ni_vec.len()); + let ki_w_i = &self.k_i * &self.w_i; + ki_w_i + miu_vec.iter().chain(ni_vec).sum::>() + } + + pub fn phase3_reconstruct_delta(delta_vec: &[Scalar]) -> Scalar { + delta_vec + .iter() + .sum::>() + .invert() + .expect("sum of deltas is zero") + } + + pub fn phase4( + delta_inv: &Scalar, + b_proof_vec: &[&DLogProof], + phase1_decommit_vec: Vec, + bc1_vec: &[SignBroadcastPhase1], + ) -> Result, Error> { + // note: b_proof_vec is populated using the results + //from the MtAwc, which is handling the proof of knowledge verification of gamma_i such that + // Gamme_i = gamma_i * G in the verify_proofs_get_alpha() + let test_b_vec_and_com = (0..b_proof_vec.len()).all(|i| { + b_proof_vec[i].pk == phase1_decommit_vec[i].g_gamma_i + && HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(phase1_decommit_vec[i].g_gamma_i.to_bytes(true).as_ref()), + &phase1_decommit_vec[i].blind_factor, + ) == bc1_vec[i].com + }); + + if test_b_vec_and_com { + Ok({ + let gamma_sum: Point = phase1_decommit_vec + .iter() + .map(|decom| &decom.g_gamma_i) + .sum(); + // R + gamma_sum * delta_inv + }) + } else { + Err(InvalidKey) + } + } +} + +impl LocalSignature { + pub fn phase5_local_sig( + k_i: &Scalar, + message: &BigInt, + R: &Point, + sigma_i: &Scalar, + pubkey: &Point, + ) -> Self { + let m_fe = Scalar::::from(message); + let r = Scalar::::from( + &R.x_coord() + .unwrap() + .mod_floor(Scalar::::group_order()), + ); + let s_i = m_fe * k_i + r * sigma_i; + let l_i = Scalar::::random(); + let rho_i = Scalar::::random(); + Self { + l_i, + rho_i, + R: R.clone(), + s_i, + m: message.clone(), + y: pubkey.clone(), + } + } + + pub fn phase5a_broadcast_5b_zkproof( + &self, + ) -> ( + Phase5Com1, + Phase5ADecom1, + HomoELGamalProof, + DLogProof, + ) { + let blind_factor = BigInt::sample(SECURITY); + let g = Point::generator(); + let A_i = g * &self.rho_i; + let l_i_rho_i = &self.l_i * &self.rho_i; + let B_i = g * l_i_rho_i; + let V_i = &self.R * &self.s_i + g * &self.l_i; + let input_hash = Sha256::new() + .chain_points([&V_i, &A_i, &B_i]) + .result_bigint(); + let com = HashCommitment::::create_commitment_with_user_defined_randomness( + &input_hash, + &blind_factor, + ); + let witness = HomoElGamalWitness { + r: self.l_i.clone(), + x: self.s_i.clone(), + }; + let delta = HomoElGamalStatement { + G: A_i.clone(), + H: self.R.clone(), + Y: g.to_point(), + D: V_i.clone(), + E: B_i.clone(), + }; + let dlog_proof_rho = DLogProof::prove(&self.rho_i); + let proof = HomoELGamalProof::prove(&witness, &delta); + + ( + Phase5Com1 { com }, + Phase5ADecom1 { + V_i, + A_i, + B_i, + blind_factor, + }, + proof, + dlog_proof_rho, + ) + } + + pub fn phase5c( + &self, + decom_vec: &[Phase5ADecom1], + com_vec: &[Phase5Com1], + elgamal_proofs: &[HomoELGamalProof], + dlog_proofs_rho: &[DLogProof], + v_i: &Point, + R: &Point, + ) -> Result<(Phase5Com2, Phase5DDecom2), Error> { + assert_eq!(decom_vec.len(), com_vec.len()); + + let g = Point::generator(); + let test_com_elgamal = (0..com_vec.len()).all(|i| { + let delta = HomoElGamalStatement { + G: decom_vec[i].A_i.clone(), + H: R.clone(), + Y: g.to_point(), + D: decom_vec[i].V_i.clone(), + E: decom_vec[i].B_i.clone(), + }; + + let input_hash = Sha256::new() + .chain_points([&decom_vec[i].V_i, &decom_vec[i].A_i, &decom_vec[i].B_i]) + .result_bigint(); + + HashCommitment::::create_commitment_with_user_defined_randomness( + &input_hash, + &decom_vec[i].blind_factor, + ) == com_vec[i].com + && elgamal_proofs[i].verify(&delta).is_ok() + && DLogProof::verify(&dlog_proofs_rho[i]).is_ok() + }); + + let v_iter = (0..com_vec.len()).map(|i| &decom_vec[i].V_i); + let a_iter = (0..com_vec.len()).map(|i| &decom_vec[i].A_i); + + let v = v_i + v_iter.sum::>(); + // V = -mG -ry - sum (vi) + let a: Point = a_iter.sum(); + + let r = Scalar::::from( + &self + .R + .x_coord() + .ok_or(Error::InvalidSig)? + .mod_floor(Scalar::::group_order()), + ); + let yr = &self.y * r; + let g = Point::generator(); + let m_fe = Scalar::::from(&self.m); + let gm = g * m_fe; + let v = v - &gm - &yr; + let u_i = v * &self.rho_i; + let t_i = a * &self.l_i; + let input_hash = Sha256::new().chain_points([&u_i, &t_i]).result_bigint(); + let blind_factor = BigInt::sample(SECURITY); + let com = HashCommitment::::create_commitment_with_user_defined_randomness( + &input_hash, + &blind_factor, + ); + + if test_com_elgamal { + Ok({ + ( + Phase5Com2 { com }, + Phase5DDecom2 { + u_i, + t_i, + blind_factor, + }, + ) + }) + } else { + Err(InvalidCom) + } + } + + pub fn phase5d( + &self, + decom_vec2: &[Phase5DDecom2], + com_vec2: &[Phase5Com2], + decom_vec1: &[Phase5ADecom1], + ) -> Result, Error> { + assert_eq!(decom_vec2.len(), decom_vec1.len()); + assert_eq!(decom_vec2.len(), com_vec2.len()); + + let test_com = (0..com_vec2.len()).all(|i| { + let input_hash = Sha256::new() + .chain_points([&decom_vec2[i].u_i, &decom_vec2[i].t_i]) + .result_bigint(); + HashCommitment::::create_commitment_with_user_defined_randomness( + &input_hash, + &decom_vec2[i].blind_factor, + ) == com_vec2[i].com + }); + + let t_iter = decom_vec2.iter().map(|decom| &decom.t_i); + let u_iter = decom_vec2.iter().map(|decom| &decom.u_i); + let b_iter = decom_vec1.iter().map(|decom| &decom.B_i); + + let g = Point::generator(); + let biased_sum_tb = g + t_iter.chain(b_iter).sum::>(); + let biased_sum_tb_minus_u = biased_sum_tb - u_iter.sum::>(); + if test_com { + if *g.as_point() == biased_sum_tb_minus_u { + Ok(self.s_i.clone()) + } else { + Err(InvalidKey) + } + } else { + Err(InvalidCom) + } + } + pub fn output_signature(&self, s_vec: &[Scalar]) -> Result { + let mut s = &self.s_i + s_vec.iter().sum::>(); + let s_bn = s.to_bigint(); + + let r = Scalar::::from( + &self + .R + .x_coord() + .ok_or(Error::InvalidSig)? + .mod_floor(Scalar::::group_order()), + ); + let ry: BigInt = self + .R + .y_coord() + .ok_or(Error::InvalidSig)? + .mod_floor(Scalar::::group_order()); + + /* + Calculate recovery id - it is not possible to compute the public key out of the signature + itself. Recovery id is used to enable extracting the public key uniquely. + 1. id = R.y & 1 + 2. if (s > curve.q / 2) id = id ^ 1 + */ + let is_ry_odd = ry.test_bit(0); + let mut recid = if is_ry_odd { 1 } else { 0 }; + let s_tag_bn = Scalar::::group_order() - &s_bn; + if s_bn > s_tag_bn { + s = Scalar::::from(&s_tag_bn); + recid ^= 1; + } + let sig = SignatureRecid { r, s, recid }; + let ver = verify(&sig, &self.y, &self.m).is_ok(); + if ver { + Ok(sig) + } else { + Err(InvalidSig) + } + } +} + +pub fn verify(sig: &SignatureRecid, y: &Point, message: &BigInt) -> Result<(), Error> { + let b = sig.s.invert().ok_or(Error::InvalidSig)?; + let a = Scalar::::from(message); + let u1 = a * &b; + let u2 = &sig.r * &b; + + let g = Point::generator(); + let gu1 = g * u1; + let yu2 = y * &u2; + // can be faster using shamir trick + + if sig.r + == Scalar::::from( + &(gu1 + yu2) + .x_coord() + .ok_or(Error::InvalidSig)? + .mod_floor(Scalar::::group_order()), + ) + { + Ok(()) + } else { + Err(InvalidSig) + } +} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/test.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/test.rs new file mode 100644 index 00000000..3e37f271 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/test.rs @@ -0,0 +1,460 @@ +#![allow(non_snake_case)] + +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +use crate::protocols::multi_party_ecdsa::gg_2018::party_i::{ + verify, KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Keys, LocalSignature, Parameters, + PartyPrivate, Phase5ADecom1, Phase5Com1, SharedKeys, SignKeys, +}; +use crate::utilities::mta::{MessageA, MessageB}; + +use curv::arithmetic::traits::Converter; +use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; +use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use paillier::*; +use sha2::Sha256; + +use super::VerifiableSS; + +#[test] +fn test_keygen_t1_n2() { + keygen_t_n_parties(1, 2); +} + +#[test] +fn test_keygen_t2_n3() { + keygen_t_n_parties(2, 3); +} + +#[test] +fn test_keygen_t2_n4() { + keygen_t_n_parties(2, 4); +} + +#[test] +fn test_sign_n5_t2_ttag4() { + sign(2, 5, 4, vec![0, 2, 3, 4]) +} +#[test] +fn test_sign_n8_t4_ttag6() { + sign(4, 8, 6, vec![0, 1, 2, 4, 6, 7]) +} + +fn keygen_t_n_parties( + t: u16, + n: u16, +) -> ( + Vec, + Vec>, + Vec>, + Point, + VerifiableSS, +) { + let parames = Parameters { + threshold: t, + share_count: n, + }; + let party_keys_vec = (0..n).map(Keys::create).collect::>(); + + let (bc1_vec, decom_vec): (Vec<_>, Vec<_>) = party_keys_vec + .iter() + .map(|k| k.phase1_broadcast_phase3_proof_of_correct_key()) + .unzip(); + + let y_vec = (0..usize::from(n)) + .map(|i| decom_vec[i].y_i.clone()) + .collect::>>(); + let mut y_vec_iter = y_vec.iter(); + let head = y_vec_iter.next().unwrap(); + let tail = y_vec_iter; + let y_sum = tail.fold(head.clone(), |acc, x| acc + x); + + let mut vss_scheme_vec = Vec::new(); + let mut secret_shares_vec = Vec::new(); + let mut index_vec = Vec::new(); + + let vss_result: Vec<_> = party_keys_vec + .iter() + .map(|k| { + k.phase1_verify_com_phase3_verify_correct_key_phase2_distribute( + ¶mes, &decom_vec, &bc1_vec, + ) + .expect("invalid key") + }) + .collect(); + + for (vss_scheme, secret_shares, index) in vss_result { + vss_scheme_vec.push(vss_scheme); + secret_shares_vec.push(secret_shares); // cannot unzip + index_vec.push(index as u16); + } + + let vss_scheme_for_test = vss_scheme_vec.clone(); + + let party_shares = (0..usize::from(n)) + .map(|i| { + (0..usize::from(n)) + .map(|j| secret_shares_vec[j][i].clone()) + .collect::>>() + }) + .collect::>>>(); + + let mut shared_keys_vec = Vec::new(); + let mut dlog_proof_vec = Vec::new(); + for (i, key) in party_keys_vec.iter().enumerate() { + let (shared_keys, dlog_proof) = key + .phase2_verify_vss_construct_keypair_phase3_pok_dlog( + ¶mes, + &y_vec, + &party_shares[i], + &vss_scheme_vec, + (&index_vec[i] + 1).into(), + ) + .expect("invalid vss"); + shared_keys_vec.push(shared_keys); + dlog_proof_vec.push(dlog_proof); + } + + let pk_vec = dlog_proof_vec + .iter() + .map(|dlog_proof| dlog_proof.pk.clone()) + .collect::>>(); + + //both parties run: + Keys::verify_dlog_proofs(¶mes, &dlog_proof_vec, &y_vec).expect("bad dlog proof"); + + //test + let xi_vec = shared_keys_vec + .iter() + .take(usize::from(t + 1)) + .map(|shared_keys| shared_keys.x_i.clone()) + .collect::>>(); + let x = vss_scheme_for_test[0] + .clone() + .reconstruct(&index_vec[0..=usize::from(t)], &xi_vec); + let sum_u_i = party_keys_vec + .iter() + .fold(Scalar::::zero(), |acc, x| acc + &x.u_i); + assert_eq!(x, sum_u_i); + + ( + party_keys_vec, + shared_keys_vec, + pk_vec, + y_sum, + vss_scheme_for_test[0].clone(), + ) +} + +fn sign(t: u16, n: u16, ttag: u16, s: Vec) { + // full key gen emulation + let (party_keys_vec, shared_keys_vec, _pk_vec, y, vss_scheme) = keygen_t_n_parties(t, n); + + let private_vec = (0..shared_keys_vec.len()) + .map(|i| PartyPrivate::set_private(party_keys_vec[i].clone(), shared_keys_vec[i].clone())) + .collect::>(); + // make sure that we have t t); + let ttag = ttag as usize; + assert_eq!(s.len(), ttag); + + // each party creates a signing key. This happens in parallel IRL. In this test we + // create a vector of signing keys, one for each party. + // throughout i will index parties + let sign_keys_vec = (0..ttag) + .map(|i| SignKeys::create(&private_vec[usize::from(s[i])], &vss_scheme, s[i], &s)) + .collect::>(); + + // each party computes [Ci,Di] = com(g^gamma_i) and broadcast the commitments + let (bc1_vec, decommit_vec1): (Vec<_>, Vec<_>) = + sign_keys_vec.iter().map(|k| k.phase1_broadcast()).unzip(); + + // each party i sends encryption of k_i under her Paillier key + // m_a_vec = [ma_0;ma_1;,...] + // range proofs are ignored here, as there's no h1, h2, N_tilde setup in this version of GG18 + let m_a_vec: Vec<_> = sign_keys_vec + .iter() + .enumerate() + .map(|(i, k)| MessageA::a(&k.k_i, &party_keys_vec[usize::from(s[i])].ek, &[]).0) + .collect(); + + // each party i sends responses to m_a_vec she received (one response with input gamma_i and one with w_i) + // m_b_gamma_vec_all is a matrix where column i is a vector of message_b's that party i answers to all ma_{j!=i} using paillier key of party j to answer to ma_j + + // aggregation of the n messages of all parties + let mut m_b_gamma_vec_all = Vec::new(); + let mut beta_vec_all = Vec::new(); + let mut m_b_w_vec_all = Vec::new(); + let mut ni_vec_all = Vec::new(); + + for (i, key) in sign_keys_vec.iter().enumerate() { + let mut m_b_gamma_vec = Vec::new(); + let mut beta_vec = Vec::new(); + let mut m_b_w_vec = Vec::new(); + let mut ni_vec = Vec::new(); + + for j in 0..ttag - 1 { + let ind = if j < i { j } else { j + 1 }; + + let (m_b_gamma, beta_gamma, _, _) = MessageB::b( + &key.gamma_i, + &party_keys_vec[usize::from(s[ind])].ek, + m_a_vec[ind].clone(), + &[], + ) + .unwrap(); + let (m_b_w, beta_wi, _, _) = MessageB::b( + &key.w_i, + &party_keys_vec[usize::from(s[ind])].ek, + m_a_vec[ind].clone(), + &[], + ) + .unwrap(); + + m_b_gamma_vec.push(m_b_gamma); + beta_vec.push(beta_gamma); + m_b_w_vec.push(m_b_w); + ni_vec.push(beta_wi); + } + m_b_gamma_vec_all.push(m_b_gamma_vec.clone()); + beta_vec_all.push(beta_vec.clone()); + m_b_w_vec_all.push(m_b_w_vec.clone()); + ni_vec_all.push(ni_vec.clone()); + } + + // Here we complete the MwA protocols by taking the mb matrices and starting with the first column generating the appropriate message + // for example for index i=0 j=0 we need party at index s[1] to answer to mb that party s[0] sent, completing a protocol between s[0] and s[1]. + // for index i=1 j=0 we need party at index s[0] to answer to mb that party s[1]. etc. + // IRL each party i should get only the mb messages that other parties sent in response to the party i ma's. + // TODO: simulate as IRL + let mut alpha_vec_all = Vec::new(); + let mut miu_vec_all = Vec::new(); + + for i in 0..ttag { + let mut alpha_vec = Vec::new(); + let mut miu_vec = Vec::new(); + + let m_b_gamma_vec_i = &m_b_gamma_vec_all[i]; + let m_b_w_vec_i = &m_b_w_vec_all[i]; + + for j in 0..ttag - 1 { + let ind = if j < i { j } else { j + 1 }; + let m_b = m_b_gamma_vec_i[j].clone(); + + let alpha_ij_gamma = m_b + .verify_proofs_get_alpha( + &party_keys_vec[usize::from(s[ind])].dk, + &sign_keys_vec[ind].k_i, + ) + .expect("wrong dlog or m_b"); + let m_b = m_b_w_vec_i[j].clone(); + let alpha_ij_wi = m_b + .verify_proofs_get_alpha( + &party_keys_vec[usize::from(s[ind])].dk, + &sign_keys_vec[ind].k_i, + ) + .expect("wrong dlog or m_b"); + + // since we actually run two MtAwc each party needs to make sure that the values B are the same as the public values + // here for b=w_i the parties already know W_i = g^w_i for each party so this check is done here. for b = gamma_i the check will be later when g^gamma_i will become public + // currently we take the W_i from the other parties signing keys + // TODO: use pk_vec (first change from x_i to w_i) for this check. + assert_eq!(m_b.b_proof.pk, sign_keys_vec[i].g_w_i); + + alpha_vec.push(alpha_ij_gamma); + miu_vec.push(alpha_ij_wi); + } + alpha_vec_all.push(alpha_vec.clone()); + miu_vec_all.push(miu_vec.clone()); + } + + let mut delta_vec = Vec::new(); + let mut sigma_vec = Vec::new(); + + for i in 0..ttag { + let alpha_vec: Vec> = (0..alpha_vec_all[i].len()) + .map(|j| alpha_vec_all[i][j].0.clone()) + .collect(); + let miu_vec: Vec> = (0..miu_vec_all[i].len()) + .map(|j| miu_vec_all[i][j].0.clone()) + .collect(); + + let delta = sign_keys_vec[i].phase2_delta_i(&alpha_vec[..], &beta_vec_all[i]); + let sigma = sign_keys_vec[i].phase2_sigma_i(&miu_vec[..], &ni_vec_all[i]); + delta_vec.push(delta); + sigma_vec.push(sigma); + } + + // all parties broadcast delta_i and compute delta_i ^(-1) + let delta_inv = SignKeys::phase3_reconstruct_delta(&delta_vec); + + // de-commit to g^gamma_i from phase1, test comm correctness, and that it is the same value used in MtA. + // Return R + + let _g_gamma_i_vec = (0..ttag) + .map(|i| sign_keys_vec[i].g_gamma_i.clone()) + .collect::>>(); + + let R_vec = (0..ttag) + .map(|_| { + // each party i tests all B = g^b = g ^ gamma_i she received. + let b_proof_vec = (0..ttag) + .map(|j| { + let b_gamma_vec = &m_b_gamma_vec_all[j]; + &b_gamma_vec[0].b_proof + }) + .collect::>>(); + SignKeys::phase4(&delta_inv, &b_proof_vec, decommit_vec1.clone(), &bc1_vec) + .expect("bad gamma_i decommit") + }) + .collect::>>(); + + let message: [u8; 4] = [79, 77, 69, 82]; + let message_bn = Sha256::new() + .chain_bigint(&BigInt::from_bytes(&message[..])) + .result_bigint(); + let mut local_sig_vec = Vec::new(); + + // each party computes s_i but don't send it yet. we start with phase5 + for i in 0..ttag { + let local_sig = LocalSignature::phase5_local_sig( + &sign_keys_vec[i].k_i, + &message_bn, + &R_vec[i], + &sigma_vec[i], + &y, + ); + local_sig_vec.push(local_sig); + } + + let mut phase5_com_vec: Vec = Vec::new(); + let mut phase_5a_decom_vec: Vec = Vec::new(); + let mut helgamal_proof_vec = Vec::new(); + let mut dlog_proof_rho_vec = Vec::new(); + // we notice that the proof for V= R^sg^l, B = A^l is a general form of homomorphic elgamal. + for sig in &local_sig_vec { + let (phase5_com, phase_5a_decom, helgamal_proof, dlog_proof_rho) = + sig.phase5a_broadcast_5b_zkproof(); + phase5_com_vec.push(phase5_com); + phase_5a_decom_vec.push(phase_5a_decom); + helgamal_proof_vec.push(helgamal_proof); + dlog_proof_rho_vec.push(dlog_proof_rho); + } + + let mut phase5_com2_vec = Vec::new(); + let mut phase_5d_decom2_vec = Vec::new(); + for i in 0..ttag { + let mut phase_5a_decom_vec_clone = phase_5a_decom_vec.clone(); + let mut phase_5a_com_vec_clone = phase5_com_vec.clone(); + let mut phase_5b_elgamal_vec_clone = helgamal_proof_vec.clone(); + + let _decom_i = phase_5a_decom_vec_clone.remove(i); + let _com_i = phase_5a_com_vec_clone.remove(i); + let _elgamal_i = phase_5b_elgamal_vec_clone.remove(i); + // for j in 0..s_minus_i.len() { + let (phase5_com2, phase_5d_decom2) = local_sig_vec[i] + .phase5c( + &phase_5a_decom_vec_clone, + &phase_5a_com_vec_clone, + &phase_5b_elgamal_vec_clone, + &dlog_proof_rho_vec, + &phase_5a_decom_vec[i].V_i, + &R_vec[0], + ) + .expect("error phase5"); + phase5_com2_vec.push(phase5_com2); + phase_5d_decom2_vec.push(phase_5d_decom2); + // } + } + + // assuming phase5 checks passes each party sends s_i and compute sum_i{s_i} + let mut s_vec: Vec> = Vec::new(); + for sig in &local_sig_vec { + let s_i = sig + .phase5d(&phase_5d_decom2_vec, &phase5_com2_vec, &phase_5a_decom_vec) + .expect("bad com 5d"); + s_vec.push(s_i); + } + + // here we compute the signature only of party i=0 to demonstrate correctness. + s_vec.remove(0); + let sig = local_sig_vec[0] + .output_signature(&s_vec) + .expect("verification failed"); + + assert_eq!(local_sig_vec[0].y, y); + verify(&sig, &local_sig_vec[0].y, &local_sig_vec[0].m).unwrap(); + check_sig(&sig.r, &sig.s, &local_sig_vec[0].m, &y); +} + +fn check_sig(r: &Scalar, s: &Scalar, msg: &BigInt, pk: &Point) { + use secp256k1::{Message, PublicKey, Signature, SECP256K1}; + + let raw_msg = BigInt::to_bytes(msg); + let mut msg: Vec = Vec::new(); // padding + msg.extend(vec![0u8; 32 - raw_msg.len()]); + msg.extend(raw_msg.iter()); + + let msg = Message::from_slice(msg.as_slice()).unwrap(); + let slice = pk.to_bytes(false); + let mut raw_pk = Vec::new(); + if slice.len() != 65 { + // after curv's pk_to_key_slice return 65 bytes, this can be removed + raw_pk.insert(0, 4u8); + raw_pk.extend(vec![0u8; 64 - slice.len()]); + raw_pk.extend(slice.as_ref()); + } else { + raw_pk.extend(slice.as_ref()); + } + + assert_eq!(raw_pk.len(), 65); + + let pk = PublicKey::from_slice(&raw_pk).unwrap(); + + let mut compact: Vec = Vec::new(); + let bytes_r = &r.to_bytes()[..]; + compact.extend(vec![0u8; 32 - bytes_r.len()]); + compact.extend(bytes_r.iter()); + + let bytes_s = &s.to_bytes()[..]; + compact.extend(vec![0u8; 32 - bytes_s.len()]); + compact.extend(bytes_s.iter()); + + let secp_sig = Signature::from_compact(compact.as_slice()).unwrap(); + + let is_correct = SECP256K1.verify(&msg, &secp_sig, &pk).is_ok(); + assert!(is_correct); +} + +#[test] +fn test_serialize_deserialize() { + use serde_json; + + let k = Keys::create(0); + let (commit, decommit) = k.phase1_broadcast_phase3_proof_of_correct_key(); + + let encoded = serde_json::to_string(&commit).unwrap(); + let decoded: KeyGenBroadcastMessage1 = serde_json::from_str(&encoded).unwrap(); + assert_eq!(commit.com, decoded.com); + + let encoded = serde_json::to_string(&decommit).unwrap(); + let decoded: KeyGenDecommitMessage1 = serde_json::from_str(&encoded).unwrap(); + assert_eq!(decommit.y_i, decoded.y_i); +} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs new file mode 100644 index 00000000..f468b616 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs @@ -0,0 +1,458 @@ +#![allow(non_snake_case)] +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ +use crate::protocols::multi_party_ecdsa::gg_2020::ErrorType; +use crate::utilities::mta::{MessageA, MessageB}; +use curv::cryptographic_primitives::proofs::sigma_ec_ddh::ECDDHProof; +use curv::cryptographic_primitives::proofs::sigma_ec_ddh::ECDDHStatement; +use curv::cryptographic_primitives::proofs::sigma_ec_ddh::ECDDHWitness; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use paillier::traits::EncryptWithChosenRandomness; +use paillier::traits::Open; +use paillier::DecryptionKey; +use paillier::Paillier; +use paillier::{EncryptionKey, Randomness, RawCiphertext, RawPlaintext}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LocalStatePhase5 { + pub k: Scalar, + pub k_randomness: BigInt, + pub gamma: Scalar, + pub beta_randomness: Vec, + pub beta_tag: Vec, + pub encryption_key: EncryptionKey, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GlobalStatePhase5 { + pub k_vec: Vec>, + pub k_randomness_vec: Vec, + pub gamma_vec: Vec>, + pub beta_randomness_vec: Vec>, + pub beta_tag_vec: Vec>, + pub encryption_key_vec: Vec, + // stuff to check against + pub delta_vec: Vec>, + pub g_gamma_vec: Vec>, + pub m_a_vec: Vec, + pub m_b_mat: Vec>, +} + +// TODO: check all parties submitted inputs +// TODO: if not - abort gracefully with list of parties that did not produce inputs +impl GlobalStatePhase5 { + pub fn local_state_to_global_state( + encryption_key_vec: &[EncryptionKey], + delta_vec: &[Scalar], //to test against delta_vec + g_gamma_vec: &[Point], // to test against the opened commitment for g_gamma + m_a_vec: &[MessageA], // to test against broadcast message A + m_b_mat: Vec>, // to test against broadcast message B + local_state_vec: &[LocalStatePhase5], + ) -> Self { + let len = local_state_vec.len(); + let k_vec = (0..len) + .map(|i| local_state_vec[i].k.clone()) + .collect::>>(); + let k_randomness_vec = (0..len) + .map(|i| local_state_vec[i].k_randomness.clone()) + .collect::>(); + let gamma_vec = (0..len) + .map(|i| local_state_vec[i].gamma.clone()) + .collect::>>(); + let beta_randomness_vec = (0..len) + .map(|i| { + (0..len - 1) + .map(|j| { + let ind1 = if j < i { j } else { j + 1 }; + let ind2 = if j < i { i - 1 } else { i }; + local_state_vec[ind1].beta_randomness[ind2].clone() + }) + .collect::>() + }) + .collect::>>(); + let beta_tag_vec = (0..len) + .map(|i| { + (0..len - 1) + .map(|j| { + let ind1 = if j < i { j } else { j + 1 }; + let ind2 = if j < i { i - 1 } else { i }; + local_state_vec[ind1].beta_tag[ind2].clone() + }) + .collect::>() + }) + .collect::>>(); + + // let encryption_key_vec = (0..len).map(|i| local_state_vec[i].encryption_key.clone() ).collect::>(); + GlobalStatePhase5 { + k_vec, + k_randomness_vec, + gamma_vec, + beta_randomness_vec, + beta_tag_vec, + encryption_key_vec: encryption_key_vec.to_vec(), + delta_vec: delta_vec.to_vec(), + g_gamma_vec: g_gamma_vec.to_vec(), + m_a_vec: m_a_vec.to_vec(), + m_b_mat, + } + } + + pub fn phase5_blame(&self) -> Result<(), ErrorType> { + let len = self.delta_vec.len(); + let mut bad_signers_vec = Vec::new(); + + // check commitment to g_gamma + for i in 0..len { + if self.g_gamma_vec[i] != Point::generator() * &self.gamma_vec[i] { + bad_signers_vec.push(i) + } + } + + let alpha_beta_matrix = (0..len) + .map(|i| { + let message_a = MessageA::a_with_predefined_randomness( + &self.k_vec[i], + &self.encryption_key_vec[i], + &self.k_randomness_vec[i], + &[], + ); + + // check message a + if message_a.c != self.m_a_vec[i].c { + bad_signers_vec.push(i) + } + + if bad_signers_vec.is_empty() { + (0..len - 1) + .map(|j| { + let ind = if j < i { j } else { j + 1 }; + let (message_b, beta) = MessageB::b_with_predefined_randomness( + &self.gamma_vec[ind], + &self.encryption_key_vec[i], + message_a.clone(), + &self.beta_randomness_vec[i][j], + &self.beta_tag_vec[i][j], + &[], + ) + .unwrap(); + // check message_b + if message_b.c != self.m_b_mat[i][j].c { + bad_signers_vec.push(ind) + } + + let k_i_gamma_j = &self.k_vec[i] * &self.gamma_vec[ind]; + let alpha = k_i_gamma_j - β + + (alpha, beta) + }) + .collect::, Scalar)>>() + } else { + vec![] + } + }) + .collect::, Scalar)>>>(); + + // The matrix we got: + // [P2, P1, P1, P1 ...] + // [P3, P3, P2, P2, ...] + // [P4, P4, P4, P3, ...] + // [..., ...] + // [Pn, Pn, Pn, Pn, ...] + // We have n columns, one for each party for all the times the party played alice. + // The Pi's indicate the counter party that played bob + + // we only proceed to check the blame if everyone opened values that are + // consistent with publicly known commitments and ciphertexts + if bad_signers_vec.is_empty() { + //reconstruct delta's + let delta_vec_reconstruct = (0..len) + .map(|i| { + let k_i_gamma_i = &self.k_vec[i] * &self.gamma_vec[i]; + + let alpha_sum = alpha_beta_matrix[i] + .iter() + .fold(Scalar::::zero(), |acc, x| acc + &x.0); + let beta_vec = (0..len - 1) + .map(|j| { + let ind1 = if j < i { j } else { j + 1 }; + let ind2 = if j < i { i - 1 } else { i }; + alpha_beta_matrix[ind1][ind2].1.clone() + }) + .collect::>>(); + + let beta_sum = beta_vec + .iter() + .fold(Scalar::::zero(), |acc, x| acc + x); + + k_i_gamma_i + alpha_sum + beta_sum + }) + .collect::>>(); + + // compare delta vec to reconstructed delta vec + + #[allow(clippy::needless_range_loop)] + for i in 0..len { + if self.delta_vec[i] != delta_vec_reconstruct[i] { + bad_signers_vec.push(i) + } + } + } + + bad_signers_vec.sort_unstable(); + bad_signers_vec.dedup(); + let err_type = ErrorType { + error_type: "phase6_blame".to_string(), + bad_actors: bad_signers_vec, + data: Vec::new(), + }; + Err(err_type) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LocalStatePhase6 { + pub k: Scalar, + pub k_randomness: BigInt, + pub miu: Vec, // we need the value before reduction + pub miu_randomness: Vec, + pub proof_of_eq_dlog: ECDDHProof, +} + +// It is assumed the second message of MtAwc (ciphertext from b to a) is broadcasted in the original protocol +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GlobalStatePhase6 { + pub k_vec: Vec>, + pub k_randomness_vec: Vec, + pub miu_vec: Vec>, + pub miu_randomness_vec: Vec>, + pub g_w_vec: Vec>, + pub encryption_key_vec: Vec, + pub proof_vec: Vec>, + pub S_vec: Vec>, + pub m_a_vec: Vec, + pub m_b_mat: Vec>, +} + +impl GlobalStatePhase6 { + pub fn extract_paillier_randomness(ciphertext: &BigInt, dk: &DecryptionKey) -> BigInt { + let raw_c = RawCiphertext::from(ciphertext.clone()); + let (_plaintext, randomness) = Paillier::open(dk, raw_c); + randomness.0 + } + + pub fn ecddh_proof( + sigma_i: &Scalar, + R: &Point, + S: &Point, + ) -> ECDDHProof { + let delta = ECDDHStatement { + g1: Point::generator().to_point(), + g2: R.clone(), + h1: Point::generator() * sigma_i, + h2: S.clone(), + }; + let w = ECDDHWitness { x: sigma_i.clone() }; + ECDDHProof::prove(&w, &delta) + } + + // TODO: check all parties submitted inputs + // TODO: if not - abort gracefully with list of parties that did not produce inputs + pub fn local_state_to_global_state( + encryption_key_vec: &[EncryptionKey], + S_vec: &[Point], + g_w_vec: &[Point], + m_a_vec: &[MessageA], // to test against broadcast message A + m_b_mat: Vec>, // to test against broadcast message B + local_state_vec: &[LocalStatePhase6], + ) -> Self { + let len = local_state_vec.len(); + let k_vec = (0..len) + .map(|i| local_state_vec[i].k.clone()) + .collect::>>(); + let k_randomness_vec = (0..len) + .map(|i| local_state_vec[i].k_randomness.clone()) + .collect::>(); + let proof_vec = (0..len) + .map(|i| local_state_vec[i].proof_of_eq_dlog.clone()) + .collect::>>(); + let miu_randomness_vec = (0..len) + .map(|i| { + (0..len - 1) + .map(|j| local_state_vec[i].miu_randomness[j].clone()) + .collect::>() + }) + .collect::>>(); + let miu_vec = (0..len) + .map(|i| { + (0..len - 1) + .map(|j| local_state_vec[i].miu[j].clone()) + .collect::>() + }) + .collect::>>(); + + GlobalStatePhase6 { + k_vec, + k_randomness_vec, + miu_vec, + miu_randomness_vec, + g_w_vec: g_w_vec.to_vec(), + encryption_key_vec: encryption_key_vec.to_vec(), + proof_vec, + S_vec: S_vec.to_vec(), + m_a_vec: m_a_vec.to_vec(), + m_b_mat, + } + } + + pub fn phase6_blame(&self, R: &Point) -> Result<(), ErrorType> { + let len = self.k_vec.len(); + let mut bad_signers_vec = Vec::new(); + + // check correctness of miu + for i in 0..len { + for j in 0..len - 1 { + if Paillier::encrypt_with_chosen_randomness( + &self.encryption_key_vec[i], + RawPlaintext::from(self.miu_vec[i][j].clone()), + &Randomness::from(self.miu_randomness_vec[i][j].clone()), + ) != RawCiphertext::from(self.m_b_mat[i][j].c.clone()) + { + bad_signers_vec.push(i) + } + } + } + + // check correctness of k + for i in 0..len { + if MessageA::a_with_predefined_randomness( + &self.k_vec[i], + &self.encryption_key_vec[i], + &self.k_randomness_vec[i], + &[], + ) + .c != self.m_a_vec[i].c + { + bad_signers_vec.push(i) + } + } + + // we only proceed to check the blame if everyone opened values that are + // consistent with publicly known ciphertexts sent during MtA + if bad_signers_vec.is_empty() { + // compute g_ni + let g_ni_mat = (0..len) + .map(|i| { + (0..len - 1) + .map(|j| { + let ind = if j < i { j } else { j + 1 }; + let k_i = &self.k_vec[i]; + let g_w_j = &self.g_w_vec[ind]; + let g_w_j_ki = g_w_j * k_i; + let miu: Scalar = + Scalar::::from(&self.miu_vec[i][j]); + let g_miu = Point::generator() * &miu; + g_w_j_ki - &g_miu + }) + .collect::>>() + }) + .collect::>>>(); + + // compute g_sigma_i + + let mut g_sigma_i_vec = (0..len) + .map(|i| { + let g_wi_ki = &self.g_w_vec[i] * &self.k_vec[i]; + let sum = self.miu_vec[i].iter().fold(g_wi_ki, |acc, x| { + acc + (Point::generator() * &Scalar::::from(&*x)) + }); + sum + }) + .collect::>>(); + + #[allow(clippy::needless_range_loop)] + for i in 0..len { + for j in 0..len - 1 { + let ind1 = if j < i { j } else { j + 1 }; + let ind2 = if j < i { i - 1 } else { i }; + g_sigma_i_vec[i] = &g_sigma_i_vec[i] + &g_ni_mat[ind1][ind2]; + } + } + + // check zero knowledge proof + #[allow(clippy::needless_range_loop)] + for i in 0..len { + let statement = ECDDHStatement { + g1: Point::generator().to_point(), + g2: R.clone(), + h1: g_sigma_i_vec[i].clone(), + h2: self.S_vec[i].clone(), + }; + + let result = self.proof_vec[i].verify(&statement); + if result.is_err() { + bad_signers_vec.push(i) + } + } + } + + bad_signers_vec.sort_unstable(); + bad_signers_vec.dedup(); + let err_type = ErrorType { + error_type: "phase6_blame".to_string(), + bad_actors: bad_signers_vec, + data: Vec::new(), + }; + Err(err_type) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GlobalStatePhase7 { + pub s_vec: Vec>, + pub r: Scalar, + pub R_dash_vec: Vec>, + pub m: BigInt, + pub R: Point, + pub S_vec: Vec>, +} + +impl GlobalStatePhase7 { + pub fn phase7_blame(&self) -> Result<(), ErrorType> { + let len = self.s_vec.len(); //TODO: check bounds + let mut bad_signers_vec = Vec::new(); + + for i in 0..len { + let R_si = &self.R * &self.s_vec[i]; + let R_dash_m = &self.R_dash_vec[i] * &Scalar::::from(&self.m); + let Si_r = &self.S_vec[i] * &self.r; + let right = R_dash_m + Si_r; + let left = R_si; + if left != right { + bad_signers_vec.push(i); + } + } + + let err_type = ErrorType { + error_type: "phase7_blame".to_string(), + bad_actors: bad_signers_vec, + data: Vec::new(), + }; + Err(err_type) + } +} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/mod.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/mod.rs new file mode 100644 index 00000000..5dec3258 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/mod.rs @@ -0,0 +1,28 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +pub mod blame; +pub mod party_i; +pub mod state_machine; +#[cfg(test)] +mod test; + +#[derive(Clone, Debug)] +pub struct ErrorType { + pub error_type: String, + pub bad_actors: Vec, + pub data: Vec, +} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs new file mode 100644 index 00000000..873ab4b6 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs @@ -0,0 +1,990 @@ +#![allow(non_snake_case)] + +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +use std::fmt::Debug; + +use centipede::juggling::proof_system::{Helgamalsegmented, Witness}; +use centipede::juggling::segmentation::Msegmentation; +use curv::arithmetic::traits::*; +use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; +use curv::cryptographic_primitives::commitments::traits::Commitment; +use curv::cryptographic_primitives::proofs::sigma_correct_homomorphic_elgamal_enc::*; +use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Curve, Point, Scalar}; +use curv::BigInt; +use sha2::Sha256; + +use crate::protocols::multi_party_ecdsa::gg_2018::VerifiableSS; +use crate::Error::{self, InvalidSig, Phase5BadSum, Phase6Error}; +use paillier::{ + Decrypt, DecryptionKey, EncryptionKey, KeyGeneration, Paillier, RawCiphertext, RawPlaintext, +}; + +use serde::{Deserialize, Serialize}; +use zk_paillier::zkproofs::NiCorrectKeyProof; +use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; + +use crate::protocols::multi_party_ecdsa::gg_2020::ErrorType; +use crate::utilities::zk_pdl_with_slack::{PDLwSlackProof, PDLwSlackStatement, PDLwSlackWitness}; +use curv::cryptographic_primitives::proofs::sigma_valid_pedersen::PedersenProof; + +use std::convert::TryInto; + +const SECURITY: usize = 256; +const PAILLIER_MIN_BIT_LENGTH: usize = 2047; +const PAILLIER_MAX_BIT_LENGTH: usize = 2048; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Parameters { + pub threshold: u16, //t + pub share_count: u16, //n +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Keys { + pub u_i: Scalar, + pub y_i: Point, + pub dk: DecryptionKey, + pub ek: EncryptionKey, + pub party_index: usize, + pub N_tilde: BigInt, + pub h1: BigInt, + pub h2: BigInt, + pub xhi: BigInt, + pub xhi_inv: BigInt, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PartyPrivate { + u_i: Scalar, + x_i: Scalar, + dk: DecryptionKey, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyGenBroadcastMessage1 { + pub e: EncryptionKey, + pub dlog_statement: DLogStatement, + pub com: BigInt, + pub correct_key_proof: NiCorrectKeyProof, + pub composite_dlog_proof_base_h1: CompositeDLogProof, + pub composite_dlog_proof_base_h2: CompositeDLogProof, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyGenDecommitMessage1 { + pub blind_factor: BigInt, + pub y_i: Point, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SharedKeys { + pub y: Point, + pub x_i: Scalar, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignKeys { + pub w_i: Scalar, + pub g_w_i: Point, + pub k_i: Scalar, + pub gamma_i: Scalar, + pub g_gamma_i: Point, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignBroadcastPhase1 { + pub com: BigInt, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignDecommitPhase1 { + pub blind_factor: BigInt, + pub g_gamma_i: Point, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LocalSignature { + pub r: Scalar, + pub R: Point, + pub s_i: Scalar, + pub m: BigInt, + pub y: Point, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignatureRecid { + pub r: Scalar, + pub s: Scalar, + pub recid: u8, +} + +pub fn generate_h1_h2_N_tilde() -> (BigInt, BigInt, BigInt, BigInt, BigInt) { + // note, should be safe primes: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys();; + let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&ek_tilde.n); + let (mut xhi, mut xhi_inv) = loop { + let xhi_ = BigInt::sample_below(&phi); + match BigInt::mod_inv(&xhi_, &phi) { + Some(inv) => break (xhi_, inv), + None => continue, + } + }; + let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); + xhi = BigInt::sub(&phi, &xhi); + xhi_inv = BigInt::sub(&phi, &xhi_inv); + + (ek_tilde.n, h1, h2, xhi, xhi_inv) +} + +impl Keys { + pub fn create(index: usize) -> Self { + let u = Scalar::::random(); + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair().keys(); + let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); + + Self { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + N_tilde, + h1, + h2, + xhi, + xhi_inv, + } + } + + // we recommend using safe primes if the code is used in production + pub fn create_safe_prime(index: usize) -> Self { + let u = Scalar::::random(); + let y = Point::generator() * &u; + + let (ek, dk) = Paillier::keypair_safe_primes().keys(); + let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); + + Self { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + N_tilde, + h1, + h2, + xhi, + xhi_inv, + } + } + pub fn create_from(u: Scalar, index: usize) -> Self { + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair().keys(); + let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); + + Self { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + N_tilde, + h1, + h2, + xhi, + xhi_inv, + } + } + + pub fn phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2( + &self, + ) -> (KeyGenBroadcastMessage1, KeyGenDecommitMessage1) { + let blind_factor = BigInt::sample(SECURITY); + let correct_key_proof = NiCorrectKeyProof::proof(&self.dk, None); + + let dlog_statement_base_h1 = DLogStatement { + N: self.N_tilde.clone(), + g: self.h1.clone(), + ni: self.h2.clone(), + }; + let dlog_statement_base_h2 = DLogStatement { + N: self.N_tilde.clone(), + g: self.h2.clone(), + ni: self.h1.clone(), + }; + + let composite_dlog_proof_base_h1 = + CompositeDLogProof::prove(&dlog_statement_base_h1, &self.xhi); + let composite_dlog_proof_base_h2 = + CompositeDLogProof::prove(&dlog_statement_base_h2, &self.xhi_inv); + + let com = HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(self.y_i.to_bytes(true).as_ref()), + &blind_factor, + ); + let bcm1 = KeyGenBroadcastMessage1 { + e: self.ek.clone(), + dlog_statement: dlog_statement_base_h1, + com, + correct_key_proof, + composite_dlog_proof_base_h1, + composite_dlog_proof_base_h2, + }; + let decom1 = KeyGenDecommitMessage1 { + blind_factor, + y_i: self.y_i.clone(), + }; + (bcm1, decom1) + } + + pub fn phase1_verify_com_phase3_verify_correct_key_verify_dlog_phase2_distribute( + &self, + params: &Parameters, + decom_vec: &[KeyGenDecommitMessage1], + bc1_vec: &[KeyGenBroadcastMessage1], + ) -> Result<(VerifiableSS, Vec>, usize), ErrorType> { + let mut bad_actors_vec = Vec::new(); + // test length: + assert_eq!(decom_vec.len(), usize::from(params.share_count)); + assert_eq!(bc1_vec.len(), usize::from(params.share_count)); + // test paillier correct key, h1,h2 correct generation and test decommitments + let correct_key_correct_decom_all = (0..bc1_vec.len()) + .map(|i| { + let dlog_statement_base_h2 = DLogStatement { + N: bc1_vec[i].dlog_statement.N.clone(), + g: bc1_vec[i].dlog_statement.ni.clone(), + ni: bc1_vec[i].dlog_statement.g.clone(), + }; + let test_res_1 = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(&decom_vec[i].y_i.to_bytes(true)), + &decom_vec[i].blind_factor, + ) == bc1_vec[i].com; + log::info!("MP-ECDSA : Round 2 : test_res_1 {:?}", test_res_1); + + if !test_res_1 { + // Do a dumb test to search if the right decommit exists + for j in 0..bc1_vec.len() { + if i != j { + let test_res_1_1 = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(&decom_vec[j].y_i.to_bytes(true)), + &decom_vec[j].blind_factor, + ) == bc1_vec[i].com; + if test_res_1_1 { + log::info!("MP-ECDSA : Round 2 RETEST : test_res_1_1 {:?}", test_res_1_1); + log::info!("MP-ECDSA : Round 2 RETEST : i {:?}", i); + log::info!("MP-ECDSA : Round 2 RETEST : j {:?}", j); + log::info!("MP-ECDSA : Round 2 RETEST : decom_vec[i] {:?}", decom_vec[i]); + log::info!("MP-ECDSA : Round 2 RETEST : decom_vec[j] {:?}", decom_vec[j]); + log::info!("MP-ECDSA : Round 2 RETEST : bc1_vec[i] {:?}", bc1_vec[i]); + log::info!("MP-ECDSA : Round 2 RETEST : bc1_vec[j] {:?}", bc1_vec[j]); + } + } + } + } + + let test_res_2 = bc1_vec[i] + .correct_key_proof + .verify(&bc1_vec[i].e, zk_paillier::zkproofs::SALT_STRING) + .is_ok(); + log::info!("MP-ECDSA : Round 2 : test_res_2 {:?}", test_res_2); + + let test_res_3 = bc1_vec[i].e.n.bit_length() >= PAILLIER_MIN_BIT_LENGTH; + log::info!("MP-ECDSA : Round 2 : test_res_3 {:?}", test_res_3); + + let test_res_4 = bc1_vec[i].e.n.bit_length() <= PAILLIER_MAX_BIT_LENGTH; + log::info!("MP-ECDSA : Round 2 : test_res_4 {:?}", test_res_4); + + let test_res_5 = + bc1_vec[i].dlog_statement.N.bit_length() >= PAILLIER_MIN_BIT_LENGTH; + log::info!("MP-ECDSA : Round 2 : test_res_5 {:?}", test_res_5); + + let test_res_6 = + bc1_vec[i].dlog_statement.N.bit_length() <= PAILLIER_MAX_BIT_LENGTH; + log::info!("MP-ECDSA : Round 2 : test_res_6 {:?}", test_res_6); + let test_res_7 = bc1_vec[i] + .composite_dlog_proof_base_h1 + .verify(&bc1_vec[i].dlog_statement) + .is_ok(); + log::info!("MP-ECDSA : Round 2 : test_res_7 {:?}", test_res_7); + let test_res_8 = bc1_vec[i] + .composite_dlog_proof_base_h2 + .verify(&dlog_statement_base_h2) + .is_ok(); + log::info!("MP-ECDSA : Round 2 : test_res_8 {:?}", test_res_8); + + let test_res = test_res_1 + && test_res_2 + && test_res_3 + && test_res_4 + && test_res_5 + && test_res_6 + && test_res_7 + && test_res_8; + + if !test_res { + bad_actors_vec.push(i); + false + } else { + true + } + }) + .all(|x| x); + + let err_type = ErrorType { + error_type: "invalid key".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + + let (vss_scheme, secret_shares) = + VerifiableSS::share(params.threshold, params.share_count, &self.u_i); + if correct_key_correct_decom_all { + Ok((vss_scheme, secret_shares.to_vec(), self.party_index)) + } else { + Err(err_type) + } + } + + pub fn phase2_verify_vss_construct_keypair_phase3_pok_dlog( + &self, + params: &Parameters, + y_vec: &[Point], + secret_shares_vec: &[Scalar], + vss_scheme_vec: &[VerifiableSS], + index: usize, + ) -> Result<(SharedKeys, DLogProof), ErrorType> { + let mut bad_actors_vec = Vec::new(); + assert_eq!(y_vec.len(), usize::from(params.share_count)); + assert_eq!(secret_shares_vec.len(), usize::from(params.share_count)); + assert_eq!(vss_scheme_vec.len(), usize::from(params.share_count)); + + let correct_ss_verify = (0..y_vec.len()) + .map(|i| { + let res = vss_scheme_vec[i] + .validate_share(&secret_shares_vec[i], index.try_into().unwrap()) + .is_ok() + && vss_scheme_vec[i].commitments[0] == y_vec[i]; + if !res { + bad_actors_vec.push(i); + false + } else { + true + } + }) + .all(|x| x); + + let err_type = ErrorType { + error_type: "invalid vss".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + + if correct_ss_verify { + let (head, tail) = y_vec.split_at(1); + let y = tail.iter().fold(head[0].clone(), |acc, x| acc + x); + + let x_i = secret_shares_vec + .iter() + .fold(Scalar::::zero(), |acc, x| acc + x); + let dlog_proof = DLogProof::prove(&x_i); + Ok((SharedKeys { y, x_i }, dlog_proof)) + } else { + Err(err_type) + } + } + + pub fn get_commitments_to_xi( + vss_scheme_vec: &[VerifiableSS], + ) -> Vec> { + let len = vss_scheme_vec.len(); + let (head, tail) = vss_scheme_vec.split_at(1); + let mut global_coefficients = head[0].commitments.clone(); + for vss in tail { + for (i, coefficient_commitment) in vss.commitments.iter().enumerate() { + global_coefficients[i] = &global_coefficients[i] + &*coefficient_commitment; + } + } + + let global_vss = VerifiableSS { + parameters: vss_scheme_vec[0].parameters.clone(), + commitments: global_coefficients, + proof: vss_scheme_vec[0].proof.clone(), + }; + (1..=len) + .map(|i| global_vss.get_point_commitment(i.try_into().unwrap())) + .collect::>>() + } + + pub fn update_commitments_to_xi( + comm: &Point, + vss_scheme: &VerifiableSS, + index: usize, + s: &[usize], + ) -> Point { + let s: Vec = s.iter().map(|&i| i.try_into().unwrap()).collect(); + let li = VerifiableSS::::map_share_to_new_params( + &vss_scheme.parameters, + index.try_into().unwrap(), + s.as_slice(), + ); + comm * &li + } + + pub fn verify_dlog_proofs_check_against_vss( + params: &Parameters, + dlog_proofs_vec: &[DLogProof], + y_vec: &[Point], + vss_vec: &[VerifiableSS], + ) -> Result<(), ErrorType> { + let mut bad_actors_vec = Vec::new(); + assert_eq!(y_vec.len(), usize::from(params.share_count)); + assert_eq!(dlog_proofs_vec.len(), usize::from(params.share_count)); + let xi_commitments = Keys::get_commitments_to_xi(vss_vec); + let xi_dlog_verify = (0..y_vec.len()) + .map(|i| { + let ver_res = DLogProof::verify(&dlog_proofs_vec[i]).is_ok(); + let verify_against_vss = xi_commitments[i] == dlog_proofs_vec[i].pk; + if !ver_res || !verify_against_vss { + bad_actors_vec.push(i); + false + } else { + true + } + }) + .all(|x| x); + + let err_type = ErrorType { + error_type: "bad dlog proof".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + + if xi_dlog_verify { + Ok(()) + } else { + Err(err_type) + } + } +} + +impl PartyPrivate { + pub fn set_private(key: Keys, shared_key: SharedKeys) -> Self { + Self { + u_i: key.u_i, + x_i: shared_key.x_i, + dk: key.dk, + } + } + + pub fn y_i(&self) -> Point { + let g = Point::generator(); + g * &self.u_i + } + + pub fn decrypt(&self, ciphertext: BigInt) -> RawPlaintext { + Paillier::decrypt(&self.dk, &RawCiphertext::from(ciphertext)) + } + + pub fn refresh_private_key(&self, factor: &Scalar, index: usize) -> Keys { + let u: Scalar = &self.u_i + factor; + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair().keys(); + + let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); + + Keys { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + N_tilde, + h1, + h2, + xhi, + xhi_inv, + } + } + + // we recommend using safe primes if the code is used in production + pub fn refresh_private_key_safe_prime(&self, factor: &Scalar, index: usize) -> Keys { + let u: Scalar = &self.u_i + factor; + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair_safe_primes().keys(); + + let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); + + Keys { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + N_tilde, + h1, + h2, + xhi, + xhi_inv, + } + } + + // used for verifiable recovery + pub fn to_encrypted_segment( + &self, + segment_size: usize, + num_of_segments: usize, + pub_ke_y: &Point, + g: &Point, + ) -> (Witness, Helgamalsegmented) { + Msegmentation::to_encrypted_segments(&self.u_i, &segment_size, num_of_segments, pub_ke_y, g) + } + + pub fn update_private_key( + &self, + factor_u_i: &Scalar, + factor_x_i: &Scalar, + ) -> Self { + PartyPrivate { + u_i: &self.u_i + factor_u_i, + x_i: &self.x_i + factor_x_i, + dk: self.dk.clone(), + } + } +} + +impl SignKeys { + pub fn g_w_vec( + pk_vec: &[Point], + s: &[usize], + vss_scheme: &VerifiableSS, + ) -> Vec> { + let s: Vec = s.iter().map(|&i| i.try_into().unwrap()).collect(); + // TODO: check bounds + (0..s.len()) + .map(|i| { + let li = VerifiableSS::::map_share_to_new_params( + &vss_scheme.parameters, + s[i], + s.as_slice(), + ); + &pk_vec[s[i] as usize] * &li + }) + .collect::>>() + } + + pub fn create( + private_x_i: &Scalar, + vss_scheme: &VerifiableSS, + index: usize, + s: &[usize], + ) -> Self { + let s: Vec = s.iter().map(|&i| i.try_into().unwrap()).collect(); + let li = VerifiableSS::::map_share_to_new_params( + &vss_scheme.parameters, + index.try_into().unwrap(), + s.as_slice(), + ); + let w_i = li * private_x_i; + let g = Point::generator(); + let g_w_i = g * &w_i; + let gamma_i = Scalar::::random(); + let g_gamma_i = g * &gamma_i; + let k_i = Scalar::::random(); + Self { + w_i, + g_w_i, + k_i, + gamma_i, + g_gamma_i, + } + } + + pub fn phase1_broadcast(&self) -> (SignBroadcastPhase1, SignDecommitPhase1) { + let blind_factor = BigInt::sample(SECURITY); + let g = Point::generator(); + let g_gamma_i = g * &self.gamma_i; + let com = HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(g_gamma_i.to_bytes(true).as_ref()), + &blind_factor, + ); + + ( + SignBroadcastPhase1 { com }, + SignDecommitPhase1 { + blind_factor, + g_gamma_i: self.g_gamma_i.clone(), + }, + ) + } + + pub fn phase2_delta_i( + &self, + alpha_vec: &[Scalar], + beta_vec: &[Scalar], + ) -> Scalar { + let vec_len = alpha_vec.len(); + assert_eq!(alpha_vec.len(), beta_vec.len()); + // assert_eq!(alpha_vec.len(), self.s.len() - 1); + let ki_gamma_i = &self.k_i * &self.gamma_i; + + (0..vec_len) + .map(|i| &alpha_vec[i] + &beta_vec[i]) + .fold(ki_gamma_i, |acc, x| acc + x) + } + + pub fn phase2_sigma_i( + &self, + miu_vec: &[Scalar], + ni_vec: &[Scalar], + ) -> Scalar { + let vec_len = miu_vec.len(); + assert_eq!(miu_vec.len(), ni_vec.len()); + //assert_eq!(miu_vec.len(), self.s.len() - 1); + let ki_w_i = &self.k_i * &self.w_i; + (0..vec_len) + .map(|i| &miu_vec[i] + &ni_vec[i]) + .fold(ki_w_i, |acc, x| acc + x) + } + + pub fn phase3_compute_t_i( + sigma_i: &Scalar, + ) -> ( + Point, + Scalar, + PedersenProof, + ) { + let g_sigma_i = Point::generator() * sigma_i; + let l = Scalar::::random(); + let h_l = Point::::base_point2() * &l; + let T = g_sigma_i + h_l; + let T_zk_proof = PedersenProof::::prove(sigma_i, &l); + + (T, l, T_zk_proof) + } + pub fn phase3_reconstruct_delta(delta_vec: &[Scalar]) -> Scalar { + let sum = delta_vec + .iter() + .fold(Scalar::::zero(), |acc, x| acc + x); + sum.invert().unwrap() + } + + pub fn phase4( + delta_inv: &Scalar, + b_proof_vec: &[&DLogProof], + phase1_decommit_vec: Vec, + bc1_vec: &[SignBroadcastPhase1], + index: usize, + ) -> Result, ErrorType> { + let mut bad_actors_vec = Vec::new(); + let test_b_vec_and_com = (0..b_proof_vec.len()) + .map(|j| { + let ind = if j < index { j } else { j + 1 }; + let res = b_proof_vec[j].pk == phase1_decommit_vec[ind].g_gamma_i + && HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes( + phase1_decommit_vec[ind].g_gamma_i.to_bytes(true).as_ref(), + ), + &phase1_decommit_vec[ind].blind_factor, + ) == bc1_vec[ind].com; + if !res { + bad_actors_vec.push(ind); + false + } else { + true + } + }) + .all(|x| x); + + let mut g_gamma_i_iter = phase1_decommit_vec.iter(); + let head = g_gamma_i_iter.next().unwrap(); + let tail = g_gamma_i_iter; + + let err_type = ErrorType { + error_type: "bad gamma_i decommit".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + + if test_b_vec_and_com { + Ok({ + let gamma_sum = tail.fold(head.g_gamma_i.clone(), |acc, x| acc + &x.g_gamma_i); + // R + gamma_sum * delta_inv + }) + } else { + Err(err_type) + } + } +} + +impl LocalSignature { + pub fn phase5_proof_pdl( + R_dash: &Point, + R: &Point, + k_ciphertext: &BigInt, + ek: &EncryptionKey, + k_i: &Scalar, + k_enc_randomness: &BigInt, + dlog_statement: &DLogStatement, + ) -> PDLwSlackProof { + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: k_ciphertext.clone(), + ek: ek.clone(), + Q: R_dash.clone(), + G: R.clone(), + h1: dlog_statement.g.clone(), + h2: dlog_statement.ni.clone(), + N_tilde: dlog_statement.N.clone(), + }; + + let pdl_w_slack_witness = PDLwSlackWitness { + x: k_i.clone(), + r: k_enc_randomness.clone(), + }; + + PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement) + } + + pub fn phase5_verify_pdl( + pdl_w_slack_proof_vec: &[PDLwSlackProof], + R_dash: &Point, + R: &Point, + k_ciphertext: &BigInt, + ek: &EncryptionKey, + dlog_statement: &[DLogStatement], + s: &[usize], + i: usize, + ) -> Result<(), ErrorType> { + let mut bad_actors_vec = Vec::new(); + + let num_of_other_participants = s.len() - 1; + if pdl_w_slack_proof_vec.len() != num_of_other_participants { + bad_actors_vec.push(i); + } else { + let proofs_verification = (0..pdl_w_slack_proof_vec.len()) + .map(|j| { + let ind = if j < i { j } else { j + 1 }; + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: k_ciphertext.clone(), + ek: ek.clone(), + Q: R_dash.clone(), + G: R.clone(), + h1: dlog_statement[s[ind]].g.clone(), + h2: dlog_statement[s[ind]].ni.clone(), + N_tilde: dlog_statement[s[ind]].N.clone(), + }; + let ver_res = pdl_w_slack_proof_vec[j].verify(&pdl_w_slack_statement); + if ver_res.is_err() { + bad_actors_vec.push(i); + false + } else { + true + } + }) + .all(|x| x); + if proofs_verification { + return Ok(()); + } + } + + let err_type = ErrorType { + error_type: "Bad PDLwSlack proof".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + Err(err_type) + } + + pub fn phase5_check_R_dash_sum(R_dash_vec: &[Point]) -> Result<(), Error> { + let sum = R_dash_vec + .iter() + .fold(Point::generator().to_point(), |acc, x| acc + x); + match sum - &Point::generator().to_point() == Point::generator().to_point() { + true => Ok(()), + false => Err(Phase5BadSum), + } + } + + pub fn phase6_compute_S_i_and_proof_of_consistency( + R: &Point, + T: &Point, + sigma: &Scalar, + l: &Scalar, + ) -> (Point, HomoELGamalProof) { + let S = R * sigma; + let delta = HomoElGamalStatement { + G: R.clone(), + H: Point::::base_point2().clone(), + Y: Point::generator().to_point(), + D: T.clone(), + E: S.clone(), + }; + let witness = HomoElGamalWitness { + x: l.clone(), + r: sigma.clone(), + }; + let proof = HomoELGamalProof::prove(&witness, &delta); + + (S, proof) + } + + pub fn phase6_verify_proof( + S_vec: &[Point], + proof_vec: &[HomoELGamalProof], + R_vec: &[Point], + T_vec: &[Point], + ) -> Result<(), ErrorType> { + let mut bad_actors_vec = Vec::new(); + let mut verify_proofs = true; + for i in 0..proof_vec.len() { + let delta = HomoElGamalStatement { + G: R_vec[i].clone(), + H: Point::::base_point2().clone(), + Y: Point::generator().to_point(), + D: T_vec[i].clone(), + E: S_vec[i].clone(), + }; + if proof_vec[i].verify(&delta).is_err() { + verify_proofs = false; + bad_actors_vec.push(i); + }; + } + + match verify_proofs { + true => Ok(()), + false => { + let err_type = ErrorType { + error_type: "phase6".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + Err(err_type) + } + } + } + + pub fn phase6_check_S_i_sum( + pubkey_y: &Point, + S_vec: &[Point], + ) -> Result<(), Error> { + let sum_plus_g = S_vec + .iter() + .fold(Point::generator().to_point(), |acc, x| acc + x); + let sum = sum_plus_g - &Point::generator().to_point(); + + match &sum == pubkey_y { + true => Ok(()), + false => Err(Phase6Error), + } + } + + pub fn phase7_local_sig( + k_i: &Scalar, + message: &BigInt, + R: &Point, + sigma_i: &Scalar, + pubkey: &Point, + ) -> Self { + let m_fe = Scalar::::from(message); + let r = Scalar::::from( + &R.x_coord() + .unwrap() + .mod_floor(Scalar::::group_order()), + ); + let s_i = m_fe * k_i + &r * sigma_i; + Self { + r, + R: R.clone(), + s_i, + m: message.clone(), + y: pubkey.clone(), + } + } + + pub fn output_signature(&self, s_vec: &[Scalar]) -> Result { + let mut s = s_vec.iter().fold(self.s_i.clone(), |acc, x| acc + x); + let s_bn = s.to_bigint(); + + let r = Scalar::::from( + &self + .R + .x_coord() + .unwrap() + .mod_floor(Scalar::::group_order()), + ); + let ry: BigInt = self + .R + .y_coord() + .unwrap() + .mod_floor(Scalar::::group_order()); + + /* + Calculate recovery id - it is not possible to compute the public key out of the signature + itself. Recovery id is used to enable extracting the public key uniquely. + 1. id = R.y & 1 + 2. if (s > curve.q / 2) id = id ^ 1 + */ + let is_ry_odd = ry.test_bit(0); + let mut recid = if is_ry_odd { 1 } else { 0 }; + let s_tag_bn = Scalar::::group_order() - &s_bn; + if s_bn > s_tag_bn { + s = Scalar::::from(&s_tag_bn); + recid ^= 1; + } + let sig = SignatureRecid { r, s, recid }; + let ver = verify(&sig, &self.y, &self.m).is_ok(); + if ver { + Ok(sig) + } else { + Err(InvalidSig) + } + } +} + +pub fn verify(sig: &SignatureRecid, y: &Point, message: &BigInt) -> Result<(), Error> { + let b = sig.s.invert().unwrap(); + let a = Scalar::::from(message); + let u1 = a * &b; + let u2 = &sig.r * &b; + + let g = Point::generator(); + let gu1 = g * u1; + let yu2 = y * &u2; + // can be faster using shamir trick + + if sig.r + == Scalar::::from( + &(gu1 + yu2) + .x_coord() + .unwrap() + .mod_floor(Scalar::::group_order()), + ) + { + Ok(()) + } else { + Err(InvalidSig) + } +} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs new file mode 100644 index 00000000..5159e12e --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs @@ -0,0 +1,552 @@ +//! High-level keygen protocol implementation + +use std::fmt; +use std::mem::replace; +use std::time::Duration; + +use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; +use curv::elliptic::curves::secp256_k1::Secp256k1; +use round_based::containers::{ + push::{Push, PushExt}, + *, +}; +use round_based::{IsCritical, Msg, StateMachine}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use thiserror::Error; + +use crate::protocols::multi_party_ecdsa::gg_2018::VerifiableSS; +use crate::protocols::multi_party_ecdsa::gg_2020; + +mod rounds; + +use private::InternalError; +pub use rounds::{LocalKey, ProceedError}; +use rounds::{Round0, Round1, Round2, Round3, Round4}; + +/// Keygen protocol state machine +/// +/// Successfully completed keygen protocol produces [LocalKey] that can be used in further +/// [signing](super::sign) protocol. +pub struct Keygen { + round: R, + + msgs1: Option>>, + msgs2: Option>>, + msgs3: Option, Vec)>>>, + msgs4: Option>>>, + + msgs_queue: Vec>, + + party_i: u16, + party_n: u16, +} + +impl Keygen { + /// Constructs a party of keygen protocol + /// + /// Takes party index `i` (in range `[1; n]`), threshold value `t`, and total number of + /// parties `n`. Party index identifies this party in the protocol, so it must be guaranteed + /// to be unique. + /// + /// Returns error if: + /// * `n` is less than 2, returns [Error::TooFewParties] + /// * `t` is not in range `[1; n-1]`, returns [Error::InvalidThreshold] + /// * `i` is not in range `[1; n]`, returns [Error::InvalidPartyIndex] + pub fn new(i: u16, t: u16, n: u16) -> Result { + if n < 2 { + return Err(Error::TooFewParties); + } + if t == 0 || t >= n { + return Err(Error::InvalidThreshold); + } + if i == 0 || i > n { + return Err(Error::InvalidPartyIndex); + } + let mut state = Self { + round: R::Round0(Round0 { party_i: i, t, n }), + + msgs1: Some(Round1::expects_messages(i, n)), + msgs2: Some(Round2::expects_messages(i, n)), + msgs3: Some(Round3::expects_messages(i, n)), + msgs4: Some(Round4::expects_messages(i, n)), + + msgs_queue: vec![], + party_i: i, + party_n: n, + }; + + state.proceed_round(false)?; + Ok(state) + } + + fn gmap_queue<'a, T, F>(&'a mut self, mut f: F) -> impl Push> + 'a + where + F: FnMut(T) -> M + 'a, + { + (&mut self.msgs_queue).gmap(move |m: Msg| m.map_body(|m| ProtocolMessage(f(m)))) + } + + /// Proceeds round state if it received enough messages and if it's cheap to compute or + /// `may_block == true` + fn proceed_round(&mut self, may_block: bool) -> Result<()> { + let store1_wants_more = self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store3_wants_more = self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store4_wants_more = self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); + + let next_state: R; + let try_again: bool = match replace(&mut self.round, R::Gone) { + R::Round0(round) if !round.is_expensive() || may_block => { + next_state = round + .proceed(self.gmap_queue(M::Round1)) + .map(R::Round1) + .map_err(Error::ProceedRound)?; + true + } + s @ R::Round0(_) => { + next_state = s; + false + } + R::Round1(round) if !store1_wants_more && (!round.is_expensive() || may_block) => { + let store = self.msgs1.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs, self.gmap_queue(M::Round2)) + .map(R::Round2) + .map_err(Error::ProceedRound)?; + true + } + s @ R::Round1(_) => { + next_state = s; + false + } + R::Round2(round) if !store2_wants_more && (!round.is_expensive() || may_block) => { + let store = self.msgs2.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs, self.gmap_queue(M::Round3)) + .map(R::Round3) + .map_err(Error::ProceedRound)?; + true + } + s @ R::Round2(_) => { + next_state = s; + false + } + R::Round3(round) if !store3_wants_more && (!round.is_expensive() || may_block) => { + let store = self.msgs3.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs, self.gmap_queue(M::Round4)) + .map(R::Round4) + .map_err(Error::ProceedRound)?; + true + } + s @ R::Round3(_) => { + next_state = s; + false + } + R::Round4(round) if !store4_wants_more && (!round.is_expensive() || may_block) => { + let store = self.msgs4.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs) + .map(R::Final) + .map_err(Error::ProceedRound)?; + true + } + s @ R::Round4(_) => { + next_state = s; + false + } + s @ R::Final(_) | s @ R::Gone => { + next_state = s; + false + } + }; + + self.round = next_state; + if try_again { + self.proceed_round(may_block) + } else { + Ok(()) + } + } +} + +impl StateMachine for Keygen { + type MessageBody = ProtocolMessage; + type Err = Error; + type Output = LocalKey; + + fn handle_incoming(&mut self, msg: Msg) -> Result<()> { + let current_round = self.current_round(); + + match msg.body { + ProtocolMessage(M::Round1(m)) => { + let store = self + .msgs1 + .as_mut() + .ok_or(Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 1, + })?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + } + ProtocolMessage(M::Round2(m)) => { + let store = self + .msgs2 + .as_mut() + .ok_or(Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 2, + })?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + } + ProtocolMessage(M::Round3(m)) => { + let store = self + .msgs3 + .as_mut() + .ok_or(Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 3, + })?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + } + ProtocolMessage(M::Round4(m)) => { + let store = self + .msgs4 + .as_mut() + .ok_or(Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 4, + })?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + } + } + } + + fn message_queue(&mut self) -> &mut Vec> { + &mut self.msgs_queue + } + + fn wants_to_proceed(&self) -> bool { + let store1_wants_more = self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store3_wants_more = self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store4_wants_more = self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); + + match &self.round { + R::Round0(_) => true, + R::Round1(_) => !store1_wants_more, + R::Round2(_) => !store2_wants_more, + R::Round3(_) => !store3_wants_more, + R::Round4(_) => !store4_wants_more, + R::Final(_) | R::Gone => false, + } + } + + fn proceed(&mut self) -> Result<()> { + self.proceed_round(true) + } + + fn round_timeout(&self) -> Option { + None + } + + fn round_timeout_reached(&mut self) -> Self::Err { + panic!("no timeout was set") + } + + fn is_finished(&self) -> bool { + matches!(self.round, R::Final(_)) + } + + fn pick_output(&mut self) -> Option> { + match self.round { + R::Final(_) => (), + R::Gone => return Some(Err(Error::DoublePickOutput)), + _ => return None, + } + + match replace(&mut self.round, R::Gone) { + R::Final(result) => Some(Ok(result)), + _ => unreachable!("guaranteed by match expression above"), + } + } + + fn current_round(&self) -> u16 { + match &self.round { + R::Round0(_) => 0, + R::Round1(_) => 1, + R::Round2(_) => 2, + R::Round3(_) => 3, + R::Round4(_) => 4, + R::Final(_) | R::Gone => 5, + } + } + + fn total_rounds(&self) -> Option { + Some(4) + } + + fn party_ind(&self) -> u16 { + self.party_i + } + + fn parties(&self) -> u16 { + self.party_n + } +} + +impl super::traits::RoundBlame for Keygen { + /// Returns number of unwilling parties and a vector of their party indexes. + fn round_blame(&self) -> (u16, Vec) { + let store1_blame = self.msgs1.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store2_blame = self.msgs2.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store3_blame = self.msgs3.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store4_blame = self.msgs4.as_ref().map(|s| s.blame()).unwrap_or_default(); + + let default = (0, vec![]); + match &self.round { + R::Round0(_) => default, + R::Round1(_) => store1_blame, + R::Round2(_) => store2_blame, + R::Round3(_) => store3_blame, + R::Round4(_) => store4_blame, + R::Final(_) | R::Gone => default, + } + } +} + +impl fmt::Debug for Keygen { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let current_round = match &self.round { + R::Round0(_) => "0", + R::Round1(_) => "1", + R::Round2(_) => "2", + R::Round3(_) => "3", + R::Round4(_) => "4", + R::Final(_) => "[Final]", + R::Gone => "[Gone]", + }; + let msgs1 = match self.msgs1.as_ref() { + Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), + None => "[None]".into(), + }; + let msgs2 = match self.msgs2.as_ref() { + Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), + None => "[None]".into(), + }; + let msgs3 = match self.msgs3.as_ref() { + Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), + None => "[None]".into(), + }; + let msgs4 = match self.msgs4.as_ref() { + Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), + None => "[None]".into(), + }; + write!( + f, + "{{Keygen at round={} msgs1={} msgs2={} msgs3={} msgs4={} queue=[len={}]}}", + current_round, + msgs1, + msgs2, + msgs3, + msgs4, + self.msgs_queue.len() + ) + } +} + +// Rounds + +enum R { + Round0(Round0), + Round1(Round1), + Round2(Round2), + Round3(Round3), + Round4(Round4), + Final(LocalKey), + Gone, +} + +// Messages + +/// Protocol message which parties send on wire +/// +/// Hides actual messages structure so it could be changed without breaking semver policy. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProtocolMessage(M); + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum M { + Round1(gg_2020::party_i::KeyGenBroadcastMessage1), + Round2(gg_2020::party_i::KeyGenDecommitMessage1), + Round3((VerifiableSS, Vec)), + Round4(DLogProof), +} + +impl crate::MessageRoundID for ProtocolMessage { + fn round_id(&self) -> u16 { + match self.0 { + M::Round1(_) => 1, + M::Round2(_) => 2, + M::Round3(_) => 3, + M::Round4(_) => 4, + } + } +} + +// Error + +type Result = std::result::Result; + +/// Error type of keygen protocol +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum Error { + /// Round proceeding resulted in error + #[error("proceed round: {0}")] + ProceedRound(#[source] ProceedError), + + /// Too few parties (`n < 2`) + #[error("at least 2 parties are required for keygen")] + TooFewParties, + /// Threshold value `t` is not in range `[1; n-1]` + #[error("threshold is not in range [1; n-1]")] + InvalidThreshold, + /// Party index `i` is not in range `[1; n]` + #[error("party index is not in range [1; n]")] + InvalidPartyIndex, + + /// Received message didn't pass pre-validation + #[error("received message didn't pass pre-validation: {0}")] + HandleMessage(#[source] StoreErr), + /// Received message which we didn't expect to receive now (e.g. message from previous round) + #[error( + "didn't expect to receive message from round {msg_round} (being at round {current_round})" + )] + ReceivedOutOfOrderMessage { current_round: u16, msg_round: u16 }, + /// [Keygen::pick_output] called twice + #[error("pick_output called twice")] + DoublePickOutput, + + /// Some internal assertions were failed, which is a bug + #[doc(hidden)] + #[error("internal error: {0:?}")] + InternalError(InternalError), +} + +impl IsCritical for Error { + fn is_critical(&self) -> bool { + match self { + Error::ProceedRound(e) => e.is_critical(), + // These errors are not critical, because they are handled by the protocol + // and don't indicate a bug in the library. + Error::HandleMessage(e) => !matches!( + e, + StoreErr::MsgOverwrite | StoreErr::NotForMe | StoreErr::WantsMoreMessages + ), + Error::ReceivedOutOfOrderMessage { .. } => false, + Error::DoublePickOutput + | Error::TooFewParties + | Error::InvalidThreshold + | Error::InvalidPartyIndex + | Error::InternalError(_) => true, + } + } +} + +impl From for Error { + fn from(err: InternalError) -> Self { + Self::InternalError(err) + } +} + +mod private { + #[derive(Debug)] + #[non_exhaustive] + pub enum InternalError { + /// [Messages store](super::MessageStore) reported that it received all messages it wanted to receive, + /// but refused to return message container + RetrieveRoundMessages(super::StoreErr), + #[doc(hidden)] + StoreGone, + } +} + +#[cfg(test)] +pub mod test { + use round_based::dev::Simulation; + + use super::*; + + pub fn simulate_keygen(t: u16, n: u16) -> Vec> { + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(true); + + for i in 1..=n { + simulation.add_party(Keygen::new(i, t, n).unwrap()); + } + + let keys = simulation.run().unwrap(); + + println!("Benchmark results:"); + println!("{:#?}", simulation.benchmark_results().unwrap()); + + keys + } + + #[test] + fn simulate_keygen_t1_n2() { + simulate_keygen(1, 2); + } + + #[test] + fn simulate_keygen_t1_n3() { + simulate_keygen(1, 3); + } + + #[test] + fn simulate_keygen_t2_n3() { + simulate_keygen(2, 3); + } +} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs new file mode 100644 index 00000000..5529e60d --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs @@ -0,0 +1,373 @@ +use curv::arithmetic::Converter; +use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Curve, Point, Scalar}; +use curv::BigInt; +use sha2::Sha256; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use paillier::Paillier; +use paillier::{Decrypt, Encrypt}; +use paillier::{EncryptionKey, RawCiphertext, RawPlaintext}; +use round_based::containers::push::Push; +use round_based::containers::{self, BroadcastMsgs, MessageStore, P2PMsgs, P2PMsgsStore, Store}; +use round_based::{IsCritical, Msg}; +use zk_paillier::zkproofs::DLogStatement; + +use crate::protocols::multi_party_ecdsa::gg_2018::VerifiableSS; +use crate::protocols::multi_party_ecdsa::gg_2020::party_i::{ + KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Keys, +}; +use crate::protocols::multi_party_ecdsa::gg_2020::{self, ErrorType}; + +pub struct Round0 { + pub party_i: u16, + pub t: u16, + pub n: u16, +} + +impl Round0 { + pub fn proceed(self, mut output: O) -> Result + where + O: Push>, + { + let party_keys = Keys::create(self.party_i as usize); + let (bc1, decom1) = + party_keys.phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2(); + + output.push(Msg { + sender: self.party_i, + receiver: None, + body: bc1.clone(), + }); + Ok(Round1 { + keys: party_keys, + bc1, + decom1, + party_i: self.party_i, + t: self.t, + n: self.n, + }) + } + pub fn is_expensive(&self) -> bool { + true + } +} + +pub struct Round1 { + keys: Keys, + bc1: KeyGenBroadcastMessage1, + decom1: KeyGenDecommitMessage1, + party_i: u16, + t: u16, + n: u16, +} + +impl Round1 { + pub fn proceed( + self, + input: BroadcastMsgs, + mut output: O, + ) -> Result + where + O: Push>, + { + output.push(Msg { + sender: self.party_i, + receiver: None, + body: self.decom1.clone(), + }); + Ok(Round2 { + keys: self.keys, + received_comm: input.into_vec_including_me(self.bc1), + decom: self.decom1, + + party_i: self.party_i, + t: self.t, + n: self.n, + }) + } + pub fn is_expensive(&self) -> bool { + false + } + pub fn expects_messages(i: u16, n: u16) -> Store> { + containers::BroadcastMsgsStore::new(i, n) + } +} + +pub struct Round2 { + keys: gg_2020::party_i::Keys, + received_comm: Vec, + decom: KeyGenDecommitMessage1, + + party_i: u16, + t: u16, + n: u16, +} + +impl Round2 { + pub fn proceed( + self, + input: BroadcastMsgs, + mut output: O, + ) -> Result + where + O: Push, Vec)>>, + { + let params = gg_2020::party_i::Parameters { + threshold: self.t, + share_count: self.n, + }; + let received_decom = input.into_vec_including_me(self.decom); + + let vss_result = self + .keys + .phase1_verify_com_phase3_verify_correct_key_verify_dlog_phase2_distribute( + ¶ms, + &received_decom, + &self.received_comm, + ) + .map_err(ProceedError::Round2VerifyCommitments)?; + + for (i, share) in vss_result.1.iter().enumerate() { + if i + 1 == usize::from(self.party_i) { + continue; + } + + let enc_key_for_recipient = &self.received_comm[i].e; + let encrypted_share = + Paillier::encrypt(enc_key_for_recipient, RawPlaintext::from(share.to_bigint())); + output.push(Msg { + sender: self.party_i, + receiver: Some(i as u16 + 1), + body: (vss_result.0.clone(), encrypted_share.0.to_bytes()), + }) + } + + Ok(Round3 { + keys: self.keys, + + y_vec: received_decom.into_iter().map(|d| d.y_i).collect(), + bc_vec: self.received_comm, + + own_vss: vss_result.0.clone(), + own_share: vss_result.1[usize::from(self.party_i - 1)].clone(), + + party_i: self.party_i, + t: self.t, + n: self.n, + }) + } + pub fn is_expensive(&self) -> bool { + true + } + pub fn expects_messages(i: u16, n: u16) -> Store> { + containers::BroadcastMsgsStore::new(i, n) + } +} + +pub struct Round3 { + keys: gg_2020::party_i::Keys, + + y_vec: Vec>, + bc_vec: Vec, + + own_vss: VerifiableSS, + own_share: Scalar, + + party_i: u16, + t: u16, + n: u16, +} + +impl Round3 { + pub fn proceed( + self, + input: P2PMsgs<(VerifiableSS, Vec)>, + mut output: O, + ) -> Result + where + O: Push>>, + { + let params = gg_2020::party_i::Parameters { + threshold: self.t, + share_count: self.n, + }; + let input: P2PMsgs<(VerifiableSS, Scalar)> = { + let encrypted_input = input.into_iter_indexed(); + let mut decrypted_input = P2PMsgsStore::new(self.party_i, self.n); + for (i, (vss, encrypted_share)) in encrypted_input { + let v = BigInt::from_bytes(&encrypted_share); + let c = RawCiphertext::from(v); + let raw_share: RawPlaintext<'_> = Paillier::decrypt(&self.keys.dk, c); + let share = Scalar::from_bigint(&raw_share.0.into_owned()); + let _ = decrypted_input.push_msg(Msg { + sender: i, + receiver: Some(self.party_i), + body: (vss, share), + }); + } + decrypted_input.finish().unwrap() + }; + + let (vss_schemes, party_shares): (Vec<_>, Vec<_>) = input + .into_vec_including_me((self.own_vss, self.own_share)) + .into_iter() + .unzip(); + let (shared_keys, dlog_proof) = self + .keys + .phase2_verify_vss_construct_keypair_phase3_pok_dlog( + ¶ms, + &self.y_vec, + &party_shares, + &vss_schemes, + self.party_i.into(), + ) + .map_err(ProceedError::Round3VerifyVssConstruct)?; + + output.push(Msg { + sender: self.party_i, + receiver: None, + body: dlog_proof.clone(), + }); + + Ok(Round4 { + keys: self.keys.clone(), + y_vec: self.y_vec.clone(), + bc_vec: self.bc_vec, + shared_keys, + own_dlog_proof: dlog_proof, + vss_vec: vss_schemes, + + party_i: self.party_i, + t: self.t, + n: self.n, + }) + } + pub fn is_expensive(&self) -> bool { + true + } + pub fn expects_messages(i: u16, n: u16) -> Store, Vec)>> { + containers::P2PMsgsStore::new(i, n) + } +} + +pub struct Round4 { + keys: gg_2020::party_i::Keys, + y_vec: Vec>, + bc_vec: Vec, + shared_keys: gg_2020::party_i::SharedKeys, + own_dlog_proof: DLogProof, + vss_vec: Vec>, + + party_i: u16, + t: u16, + n: u16, +} + +impl Round4 { + pub fn proceed( + self, + input: BroadcastMsgs>, + ) -> Result> { + let params = gg_2020::party_i::Parameters { + threshold: self.t, + share_count: self.n, + }; + let dlog_proofs = input.into_vec_including_me(self.own_dlog_proof.clone()); + + Keys::verify_dlog_proofs_check_against_vss( + ¶ms, + &dlog_proofs, + &self.y_vec, + &self.vss_vec, + ) + .map_err(ProceedError::Round4VerifyDLogProof)?; + let pk_vec = (0..params.share_count as usize) + .map(|i| dlog_proofs[i].pk.clone()) + .collect::>>(); + + let paillier_key_vec = (0..params.share_count) + .map(|i| self.bc_vec[i as usize].e.clone()) + .collect::>(); + let h1_h2_n_tilde_vec = self + .bc_vec + .iter() + .map(|bc1| bc1.dlog_statement.clone()) + .collect::>(); + + let (head, tail) = self.y_vec.split_at(1); + let y_sum = tail.iter().fold(head[0].clone(), |acc, x| acc + x); + + let local_key = LocalKey { + paillier_dk: self.keys.dk, + pk_vec, + + keys_linear: self.shared_keys.clone(), + paillier_key_vec, + y_sum_s: y_sum, + h1_h2_n_tilde_vec, + + vss_scheme: self.vss_vec[usize::from(self.party_i - 1)].clone(), + + i: self.party_i, + t: self.t, + n: self.n, + }; + + Ok(local_key) + } + pub fn is_expensive(&self) -> bool { + true + } + pub fn expects_messages(i: u16, n: u16) -> Store>> { + containers::BroadcastMsgsStore::new(i, n) + } +} + +/// Local secret obtained by party after [keygen](super::Keygen) protocol is completed +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct LocalKey { + pub paillier_dk: paillier::DecryptionKey, + pub pk_vec: Vec>, + pub keys_linear: gg_2020::party_i::SharedKeys, + pub paillier_key_vec: Vec, + pub y_sum_s: Point, + pub h1_h2_n_tilde_vec: Vec, + pub vss_scheme: VerifiableSS, + pub i: u16, + pub t: u16, + pub n: u16, +} + +impl LocalKey { + /// Public key of secret shared between parties + pub fn public_key(&self) -> Point { + self.y_sum_s.clone() + } +} + +// Errors + +type Result = std::result::Result; + +/// Proceeding protocol error +/// +/// Subset of [keygen errors](enum@super::Error) that can occur at protocol proceeding (i.e. after +/// every message was received and pre-validated). +#[derive(Debug, Error)] +pub enum ProceedError { + #[error("round 2: verify commitments: {0:?}")] + Round2VerifyCommitments(ErrorType), + #[error("round 3: verify vss construction: {0:?}")] + Round3VerifyVssConstruct(ErrorType), + #[error("round 4: verify dlog proof: {0:?}")] + Round4VerifyDLogProof(ErrorType), +} + +impl IsCritical for ProceedError { + fn is_critical(&self) -> bool { + true + } +} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/mod.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/mod.rs new file mode 100644 index 00000000..59585288 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/mod.rs @@ -0,0 +1,3 @@ +pub mod keygen; +pub mod sign; +pub mod traits; diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign.rs new file mode 100644 index 00000000..e44b80d0 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign.rs @@ -0,0 +1,777 @@ +//! # High-level threshold signing protocol implementation +//! +//! Key feature of GG20 protocol is one-round online signing, meaning that every party needs to +//! broadcast just a single message to sign a data. However, it still requires completing an offline +//! computation for fixed set of parties +//! +//! ## How to get things work +//! +//! First of all, parties need to carry out distributed key generation protocol (see [keygen module]). +//! After DKG is successfully completed, it outputs [LocalKey] — a party local secret share. +//! Then you fix a set of parties who will participate in threshold signing, and they run +//! [OfflineStage] protocol. `OfflineStage` implements [StateMachine] and can be executed in the same +//! way as [Keygen]. `OfflineStage` outputs a [CompletedOfflineStage]. [SignManual] takes a +//! `CompletedOfflineStage` and allows you to perform one-round signing. It doesn't implement +//! `StateMachine`, but rather provides methods to construct messages and final signature manually +//! (refer to [SignManual] documentation to see how to use it). +//! +//! [keygen module]: super::keygen +//! [Keygen]: super::keygen::Keygen +//! [LocalKey]: super::keygen::LocalKey +//! [StateMachine]: round_based::StateMachine + +use std::convert::TryFrom; +use std::mem::replace; +use std::time::Duration; + +use round_based::containers::{push::Push, BroadcastMsgs, MessageStore, P2PMsgs, Store, StoreErr}; +use round_based::{IsCritical, Msg, StateMachine}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::utilities::mta::MessageA; + +use crate::protocols::multi_party_ecdsa::gg_2020 as gg20; +use curv::elliptic::curves::secp256_k1::Secp256k1; +use gg20::party_i::{SignBroadcastPhase1, SignDecommitPhase1, SignatureRecid}; +use gg20::state_machine::keygen::LocalKey; + +mod fmt; +pub mod rounds; + +use crate::utilities::zk_pdl_with_slack::PDLwSlackProof; +use curv::BigInt; +use rounds::*; +pub use rounds::{CompletedOfflineStage, Error as ProceedError, PartialSignature}; + +/// Offline Stage of GG20 signing +/// +/// Successfully carried out Offline Stage will produce [CompletedOfflineStage] that can +/// be used for one-round signing multiple times. +pub struct OfflineStage { + round: OfflineR, + + msgs1: Option>>, + msgs2: Option>>, + msgs3: Option>>, + msgs4: Option>>, + msgs5: Option)>>>, + msgs6: Option>>, + + msgs_queue: MsgQueue, + + party_i: u16, + party_n: u16, +} + +impl OfflineStage { + /// Construct a party of offline stage of threshold signing protocol + /// + /// Once offline stage is finished, parties can do one-round threshold signing (i.e. they only + /// need to exchange a single set of messages). + /// + /// Takes party index `i` (in range `[1; n]`), list `s_l` of parties' indexes from keygen protocol + /// (`s_l[i]` must be an index of party `i` that was used by this party in keygen protocol), and + /// party local secret share `local_key`. + /// + /// Returns error if given arguments are contradicting. + pub fn new(i: u16, s_l: Vec, local_key: LocalKey) -> Result { + if s_l.len() < 2 { + return Err(Error::TooFewParties); + } + if i == 0 || usize::from(i) > s_l.len() { + return Err(Error::InvalidPartyIndex); + } + + let keygen_n = local_key.n; + if s_l.iter().any(|&i| i == 0 || i > keygen_n) { + return Err(Error::InvalidSl); + } + { + // Check if s_l has duplicates + let mut s_l_sorted = s_l.clone(); + s_l_sorted.sort_unstable(); + let mut s_l_sorted_deduped = s_l_sorted.clone(); + s_l_sorted_deduped.dedup(); + + if s_l_sorted != s_l_sorted_deduped { + return Err(Error::InvalidSl); + } + } + + let n = u16::try_from(s_l.len()).map_err(|_| Error::TooManyParties { n: s_l.len() })?; + + Ok(Self { + round: OfflineR::R0(Round0 { i, s_l, local_key }), + + msgs1: Some(Round1::expects_messages(i, n)), + msgs2: Some(Round2::expects_messages(i, n)), + msgs3: Some(Round3::expects_messages(i, n)), + msgs4: Some(Round4::expects_messages(i, n)), + msgs5: Some(Round5::expects_messages(i, n)), + msgs6: Some(Round6::expects_messages(i, n)), + + msgs_queue: MsgQueue(vec![]), + + party_i: i, + party_n: n, + }) + } + + // fn proceed_state(&mut self, may_block: bool) -> Result<()> { + // self.proceed_round(may_block)?; + // self.proceed_decommit_round(may_block) + // } + + fn proceed_round(&mut self, may_block: bool) -> Result<()> { + let store1_wants_more = self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store3_wants_more = self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store4_wants_more = self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store5_wants_more = self.msgs5.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store6_wants_more = self.msgs6.as_ref().map(|s| s.wants_more()).unwrap_or(false); + + let next_state: OfflineR; + let try_again: bool = match replace(&mut self.round, OfflineR::Gone) { + OfflineR::R0(round) if !round.is_expensive() || may_block => { + next_state = round + .proceed(&mut self.msgs_queue) + .map(OfflineR::R1) + .map_err(Error::ProceedRound)?; + true + } + s @ OfflineR::R0(_) => { + next_state = s; + false + } + OfflineR::R1(round) if !store1_wants_more && (!round.is_expensive() || may_block) => { + let store = self.msgs1.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveMessagesFromStore)?; + next_state = round + .proceed(msgs, &mut self.msgs_queue) + .map(OfflineR::R2) + .map_err(Error::ProceedRound)?; + true + } + s @ OfflineR::R1(_) => { + next_state = s; + false + } + OfflineR::R2(round) if !store2_wants_more && (!round.is_expensive() || may_block) => { + let store = self.msgs2.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveMessagesFromStore)?; + next_state = round + .proceed(msgs, &mut self.msgs_queue) + .map(OfflineR::R3) + .map_err(Error::ProceedRound)?; + true + } + s @ OfflineR::R2(_) => { + next_state = s; + false + } + OfflineR::R3(round) if !store3_wants_more && (!round.is_expensive() || may_block) => { + let store = self.msgs3.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveMessagesFromStore)?; + next_state = round + .proceed(msgs, &mut self.msgs_queue) + .map(OfflineR::R4) + .map_err(Error::ProceedRound)?; + true + } + s @ OfflineR::R3(_) => { + next_state = s; + false + } + OfflineR::R4(round) if !store4_wants_more && (!round.is_expensive() || may_block) => { + let store = self.msgs4.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveMessagesFromStore)?; + next_state = round + .proceed(msgs, &mut self.msgs_queue) + .map(OfflineR::R5) + .map_err(Error::ProceedRound)?; + false + } + s @ OfflineR::R4(_) => { + next_state = s; + false + } + OfflineR::R5(round) if !store5_wants_more && (!round.is_expensive() || may_block) => { + let store = self.msgs5.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveMessagesFromStore)?; + next_state = round + .proceed(msgs, &mut self.msgs_queue) + .map(OfflineR::R6) + .map_err(Error::ProceedRound)?; + false + } + s @ OfflineR::R5(_) => { + next_state = s; + false + } + OfflineR::R6(round) if !store6_wants_more && (!round.is_expensive() || may_block) => { + let store = self.msgs6.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveMessagesFromStore)?; + next_state = round + .proceed(msgs) + .map(OfflineR::Finished) + .map_err(Error::ProceedRound)?; + false + } + s @ OfflineR::R6(_) => { + next_state = s; + false + } + s @ OfflineR::Finished(_) | s @ OfflineR::Gone => { + next_state = s; + false + } + }; + + self.round = next_state; + if try_again { + self.proceed_round(may_block) + } else { + Ok(()) + } + } +} + +impl StateMachine for OfflineStage { + type MessageBody = OfflineProtocolMessage; + type Err = Error; + type Output = CompletedOfflineStage; + + fn handle_incoming(&mut self, msg: Msg) -> Result<(), Self::Err> { + let current_round = self.current_round(); + + match msg.body { + OfflineProtocolMessage(OfflineM::M1(m)) => { + let store = self + .msgs1 + .as_mut() + .ok_or(Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 1, + })?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + } + OfflineProtocolMessage(OfflineM::M2(m)) => { + let store = self + .msgs2 + .as_mut() + .ok_or(Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 2, + })?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + } + OfflineProtocolMessage(OfflineM::M3(m)) => { + let store = self + .msgs3 + .as_mut() + .ok_or(Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 2, + })?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + } + OfflineProtocolMessage(OfflineM::M4(m)) => { + let store = self + .msgs4 + .as_mut() + .ok_or(Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 2, + })?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + } + OfflineProtocolMessage(OfflineM::M5(m)) => { + let store = self + .msgs5 + .as_mut() + .ok_or(Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 2, + })?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + } + OfflineProtocolMessage(OfflineM::M6(m)) => { + let store = self + .msgs6 + .as_mut() + .ok_or(Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 2, + })?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + } + } + self.proceed_round(false) + } + + fn message_queue(&mut self) -> &mut Vec> { + &mut self.msgs_queue.0 + } + + fn wants_to_proceed(&self) -> bool { + let store1_wants_more = self.msgs1.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = self.msgs2.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store3_wants_more = self.msgs3.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store4_wants_more = self.msgs4.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store5_wants_more = self.msgs5.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store6_wants_more = self.msgs6.as_ref().map(|s| s.wants_more()).unwrap_or(false); + + match &self.round { + OfflineR::R0(_) => true, + OfflineR::R1(_) => !store1_wants_more, + OfflineR::R2(_) => !store2_wants_more, + OfflineR::R3(_) => !store3_wants_more, + OfflineR::R4(_) => !store4_wants_more, + OfflineR::R5(_) => !store5_wants_more, + OfflineR::R6(_) => !store6_wants_more, + OfflineR::Finished(_) | OfflineR::Gone => false, + } + } + + fn proceed(&mut self) -> Result<(), Self::Err> { + self.proceed_round(true) + } + + fn round_timeout(&self) -> Option { + None + } + + fn round_timeout_reached(&mut self) -> Self::Err { + panic!("no timeout was set") + } + + fn is_finished(&self) -> bool { + matches!(&self.round, OfflineR::Finished(_)) + } + + fn pick_output(&mut self) -> Option> { + match self.round { + OfflineR::Finished(_) => (), + OfflineR::Gone => return Some(Err(Error::DoublePickOutput)), + _ => return None, + } + + match replace(&mut self.round, OfflineR::Gone) { + OfflineR::Finished(result) => Some(Ok(result)), + _ => unreachable!("guaranteed by match expression above"), + } + } + + fn current_round(&self) -> u16 { + match &self.round { + OfflineR::R0(_) => 0, + OfflineR::R1(_) => 1, + OfflineR::R2(_) => 2, + OfflineR::R3(_) => 3, + OfflineR::R4(_) => 4, + OfflineR::R5(_) => 5, + OfflineR::R6(_) => 6, + OfflineR::Finished(_) | OfflineR::Gone => 7, + } + } + + fn total_rounds(&self) -> Option { + Some(6) + } + + fn party_ind(&self) -> u16 { + self.party_i + } + + fn parties(&self) -> u16 { + self.party_n + } +} + +impl super::traits::RoundBlame for OfflineStage { + /// RoundBlame returns number of unwilling parties and a vector of their party indexes. + fn round_blame(&self) -> (u16, Vec) { + let store1_blame = self.msgs1.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store2_blame = self.msgs2.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store3_blame = self.msgs3.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store4_blame = self.msgs4.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store5_blame = self.msgs5.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store6_blame = self.msgs6.as_ref().map(|s| s.blame()).unwrap_or_default(); + + let default = (0, vec![]); + match &self.round { + OfflineR::R0(_) => default, + OfflineR::R1(_) => store1_blame, + OfflineR::R2(_) => store2_blame, + OfflineR::R3(_) => store3_blame, + OfflineR::R4(_) => store4_blame, + OfflineR::R5(_) => store5_blame, + OfflineR::R6(_) => store6_blame, + OfflineR::Finished(_) => store6_blame, + OfflineR::Gone => default, + } + } +} + +#[allow(clippy::large_enum_variant)] +enum OfflineR { + R0(Round0), + R1(Round1), + R2(Round2), + R3(Round3), + R4(Round4), + R5(Round5), + R6(Round6), + Finished(CompletedOfflineStage), + Gone, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct OfflineProtocolMessage(OfflineM); + +impl crate::MessageRoundID for OfflineProtocolMessage { + fn round_id(&self) -> u16 { + match &self.0 { + OfflineM::M1(_) => 1, + OfflineM::M2(_) => 2, + OfflineM::M3(_) => 3, + OfflineM::M4(_) => 4, + OfflineM::M5(_) => 5, + OfflineM::M6(_) => 6, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[allow(clippy::large_enum_variant)] +enum OfflineM { + M1((MessageA, SignBroadcastPhase1)), + M2((GammaI, WI)), + M3((DeltaI, TI, TIProof)), + M4(SignDecommitPhase1), + M5((RDash, Vec)), + M6((SI, HEGProof)), +} + +struct MsgQueue(Vec>); + +macro_rules! make_pushable { + ($($constructor:ident $t:ty),*$(,)?) => { + $( + impl Push> for MsgQueue { + fn push(&mut self, m: Msg<$t>) { + Vec::push(&mut self.0, Msg{ + sender: m.sender, + receiver: m.receiver, + body: OfflineProtocolMessage(OfflineM::$constructor(m.body)) + }) + } + } + )* + }; +} + +make_pushable! { + M1 (MessageA, SignBroadcastPhase1), + M2 (GammaI, WI), + M3 (DeltaI, TI, TIProof), + M4 SignDecommitPhase1, + M5 (RDash, Vec), + M6 (SI, HEGProof), +} + +type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + /// Too few parties (`n < 2`) + #[error("at least 2 parties are required for signing")] + TooFewParties, + /// Too many parties. `n` must fit into `u16`, so only `n < u16::MAX` values are supported. + #[error("too many parties: n={n}, n must be less than 2^16")] + TooManyParties { n: usize }, + /// Party index `i` is not in range `[1; n]` + #[error("party index is not in range [1; n]")] + InvalidPartyIndex, + /// List `s_l` is invalid. Either it contains duplicates (`exist i j. i != j && s_l[i] = s_l[j]`), + /// or contains index that is not in the range `[1; keygen_n]`, `keygen_n` — number of parties + /// participated in DKG (`exist i. s_l[i] = 0 || s_l[i] > keygen_n`). + #[error("invalid s_l")] + InvalidSl, + + /// Round proceeding resulted in protocol error + #[error("proceeding round: {0}")] + ProceedRound(rounds::Error), + + /// Received message which we didn't expect to receive now (e.g. message from previous round) + #[error( + "didn't expect to receive message from round {msg_round} (being at round {current_round})" + )] + ReceivedOutOfOrderMessage { current_round: u16, msg_round: u16 }, + /// Received message didn't pass pre-validation + #[error("received message didn't pass pre-validation: {0}")] + HandleMessage(#[source] StoreErr), + + /// [OfflineStage::pick_output] called twice + #[error("pick_output called twice")] + DoublePickOutput, + + /// A bug in protocol implementation + #[error("offline stage protocol bug: {0}")] + Bug(InternalError), +} + +#[derive(Debug, Error)] +pub enum InternalError { + #[error("store gone")] + StoreGone, + #[error("store reported that it's collected all the messages it needed, but refused to give received messages")] + RetrieveMessagesFromStore(StoreErr), + #[error("decommit round expected to be in NotStarted state")] + DecommitRoundWasntInInitialState, +} + +impl From for Error { + fn from(err: InternalError) -> Self { + Error::Bug(err) + } +} + +impl IsCritical for Error { + fn is_critical(&self) -> bool { + match self { + Error::TooFewParties => true, + Error::TooManyParties { .. } => true, + Error::InvalidPartyIndex => true, + Error::InvalidSl => true, + Error::ProceedRound(_) => true, + Error::ReceivedOutOfOrderMessage { .. } => false, + Error::HandleMessage(_) => false, + Error::DoublePickOutput => true, + Error::Bug(_) => true, + } + } +} + +/// Manual GG20 signing +/// +/// After you completed [OfflineStage] and got [CompletedOfflineStage], parties can perform signing +/// simply by broadcasting a single message. +/// +/// ## Example +/// ```no_run +/// # use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::{ +/// # state_machine::sign::{CompletedOfflineStage, SignManual, PartialSignature}, +/// # party_i::{LocalSignature, verify}, +/// # }; +/// # use curv::arithmetic::{BigInt, Converter}; +/// # type Result = std::result::Result>; +/// # fn broadcast(msg: PartialSignature) -> Result<()> { panic!() } +/// # fn wait_messages() -> Result> { panic!() } +/// # fn main() -> Result<()> { +/// # let completed_offline_stage: CompletedOfflineStage = panic!(); +/// let data = BigInt::from_bytes(b"a message"); +/// +/// // Sign a message locally +/// let (sign, msg) = SignManual::new(data.clone(), completed_offline_stage)?; +/// // Broadcast local partial signature +/// broadcast(msg)?; +/// // Collect partial signatures from other parties +/// let sigs: Vec = wait_messages()?; +/// // Complete signing +/// let signature = sign.complete(&sigs)?; +/// // Verify that signature matches joint public key +/// assert!(verify(&signature, completed_offline_stage.public_key(), &data).is_ok()); +/// # Ok(()) +/// # } +/// ``` +#[derive(Clone)] +pub struct SignManual { + state: Round7, +} + +impl SignManual { + pub fn new( + message: BigInt, + completed_offline_stage: CompletedOfflineStage, + ) -> Result<(Self, PartialSignature), SignError> { + Round7::new(&message, completed_offline_stage) + .map(|(state, m)| (Self { state }, m)) + .map_err(SignError::LocalSigning) + } + + /// `sigs` must not include partial signature produced by local party (only partial signatures produced + /// by other parties) + pub fn complete(self, sigs: &[PartialSignature]) -> Result { + self.state + .proceed_manual(sigs) + .map_err(SignError::CompleteSigning) + } +} + +#[derive(Debug, Error)] +pub enum SignError { + #[error("signing message locally: {0}")] + LocalSigning(rounds::Error), + #[error("couldn't complete signing: {0}")] + CompleteSigning(rounds::Error), +} + +#[cfg(test)] +mod test { + use curv::arithmetic::Converter; + use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; + use round_based::dev::Simulation; + use sha2::Sha256; + + use super::*; + use gg20::party_i::verify; + use gg20::state_machine::keygen::test::simulate_keygen; + + fn simulate_offline_stage( + local_keys: Vec>, + s_l: &[u16], + ) -> Vec { + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(true); + + for (i, &keygen_i) in (1..).zip(s_l) { + simulation.add_party( + OfflineStage::new( + i, + s_l.to_vec(), + local_keys[usize::from(keygen_i - 1)].clone(), + ) + .unwrap(), + ); + } + + let stages = simulation.run().unwrap(); + + println!("Benchmark results:"); + println!("{:#?}", simulation.benchmark_results().unwrap()); + + stages + } + + fn simulate_signing(offline: Vec, message: &[u8]) { + let message = Sha256::new() + .chain_bigint(&BigInt::from_bytes(message)) + .result_bigint(); + let pk = offline[0].public_key().clone(); + + let parties = offline + .iter() + .map(|o| SignManual::new(message.clone(), o.clone())) + .collect::, _>>() + .unwrap(); + let (parties, local_sigs): (Vec<_>, Vec<_>) = parties.into_iter().unzip(); + // parties.remove(0).complete(&local_sigs[1..]).unwrap(); + let local_sigs_except = |i: usize| { + let mut v = vec![]; + v.extend_from_slice(&local_sigs[..i]); + if i + 1 < local_sigs.len() { + v.extend_from_slice(&local_sigs[i + 1..]); + } + v + }; + + assert!(parties + .into_iter() + .enumerate() + .map(|(i, p)| p.complete(&local_sigs_except(i)).unwrap()) + .all(|signature| verify(&signature, &pk, &message).is_ok())); + } + + #[test] + fn simulate_offline_stage_t1_n2_s2() { + let local_keys = simulate_keygen(1, 2); + simulate_offline_stage(local_keys, &[1, 2]); + } + + #[test] + fn simulate_offline_stage_t1_n3_s2() { + let local_keys = simulate_keygen(1, 3); + simulate_offline_stage(local_keys, &[1, 3]); + } + + #[test] + fn simulate_offline_stage_t2_n3_s3() { + let local_keys = simulate_keygen(2, 3); + simulate_offline_stage(local_keys, &[1, 2, 3]); + } + + #[test] + fn simulate_signing_t1_n2_s2() { + let local_keys = simulate_keygen(1, 2); + let offline_stage = simulate_offline_stage(local_keys, &[1, 2]); + simulate_signing(offline_stage, b"ZenGo") + } + + #[test] + fn simulate_signing_t1_n3_s2() { + let local_keys = simulate_keygen(1, 3); + let offline_stage = simulate_offline_stage(local_keys.clone(), &[1, 2]); + simulate_signing(offline_stage, b"ZenGo"); + let offline_stage = simulate_offline_stage(local_keys.clone(), &[1, 3]); + simulate_signing(offline_stage, b"ZenGo"); + let offline_stage = simulate_offline_stage(local_keys, &[2, 3]); + simulate_signing(offline_stage, b"ZenGo"); + } + + #[test] + fn simulate_signing_t2_n3_s3() { + let local_keys = simulate_keygen(2, 3); + let offline_stage = simulate_offline_stage(local_keys, &[1, 2, 3]); + simulate_signing(offline_stage, b"ZenGo") + } +} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/fmt.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/fmt.rs new file mode 100644 index 00000000..c8bf31db --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/fmt.rs @@ -0,0 +1,127 @@ +use std::fmt; + +use round_based::containers::{BroadcastMsgsStore, MessageStore, P2PMsgsStore}; + +impl fmt::Debug for super::OfflineStage { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + OfflineStageProgress::from(self).fmt(f) + } +} + +#[derive(Debug)] +#[allow(dead_code)] +pub struct OfflineStageProgress { + round: OfflineR, + + round1_msgs: ReceivedMessages, + round2_msgs: ReceivedMessages, + round3_msgs: ReceivedMessages, + round4_msgs: ReceivedMessages, + round5_msgs: ReceivedMessages, + + msgs_queue: OutgoingMessages, +} + +impl From<&super::OfflineStage> for OfflineStageProgress { + fn from(state: &super::OfflineStage) -> Self { + Self { + round: match &state.round { + super::OfflineR::R0(_) => OfflineR::R0, + super::OfflineR::R1(_) => OfflineR::R1, + super::OfflineR::R2(_) => OfflineR::R2, + super::OfflineR::R3(_) => OfflineR::R3, + super::OfflineR::R4(_) => OfflineR::R4, + super::OfflineR::R5(_) => OfflineR::R5, + super::OfflineR::R6(_) => OfflineR::R6, + super::OfflineR::Finished(_) => OfflineR::Finished, + super::OfflineR::Gone => OfflineR::Gone, + }, + + round1_msgs: ReceivedMessages::from_broadcast(state.msgs1.as_ref()), + round2_msgs: ReceivedMessages::from_p2p(state.msgs2.as_ref()), + round3_msgs: ReceivedMessages::from_broadcast(state.msgs3.as_ref()), + round4_msgs: ReceivedMessages::from_broadcast(state.msgs4.as_ref()), + round5_msgs: ReceivedMessages::from_broadcast(state.msgs5.as_ref()), + + msgs_queue: OutgoingMessages { + len: state.msgs_queue.0.len(), + }, + } + } +} + +#[derive(Debug)] +pub enum OfflineR { + R0, + R1, + R2, + R3, + R4, + R5, + R6, + Finished, + Gone, +} + +pub enum ContainerType { + P2P, + Broadcast, +} + +pub struct ReceivedMessages(Option); + +pub struct MessagesContainer { + ty: ContainerType, + total: usize, + waiting_for: Vec, +} + +impl ReceivedMessages { + fn from_broadcast(store: Option<&BroadcastMsgsStore>) -> Self { + match store { + Some(store) => ReceivedMessages(Some(MessagesContainer { + ty: ContainerType::Broadcast, + total: store.messages_total(), + waiting_for: store.blame().1, + })), + None => ReceivedMessages(None), + } + } + fn from_p2p(store: Option<&P2PMsgsStore>) -> Self { + match store { + Some(store) => ReceivedMessages(Some(MessagesContainer { + ty: ContainerType::P2P, + total: store.messages_total(), + waiting_for: store.blame().1, + })), + None => ReceivedMessages(None), + } + } +} + +impl fmt::Debug for ReceivedMessages { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.0 { + Some(container) => { + let ty = match container.ty { + ContainerType::Broadcast => "bc", + ContainerType::P2P => "p2p", + }; + write!( + f, + "[{} {}/{}]", + ty, + container.total - container.waiting_for.len(), + container.total + ) + } + None => write!(f, "[gone]"), + } + } +} + +#[derive(Debug)] +#[allow(dead_code)] +pub struct OutgoingMessages { + len: usize, +} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs new file mode 100644 index 00000000..3d681b7f --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs @@ -0,0 +1,737 @@ +#![allow(non_snake_case)] + +use std::convert::TryFrom; +use std::iter; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use sha2::Sha256; + +use round_based::containers::push::Push; +use round_based::containers::{self, BroadcastMsgs, P2PMsgs, Store}; +use round_based::Msg; + +use crate::utilities::mta::{MessageA, MessageB}; + +use crate::protocols::multi_party_ecdsa::gg_2020 as gg20; +use crate::utilities::zk_pdl_with_slack::PDLwSlackProof; +use curv::cryptographic_primitives::proofs::sigma_correct_homomorphic_elgamal_enc::HomoELGamalProof; +use curv::cryptographic_primitives::proofs::sigma_valid_pedersen::PedersenProof; +use gg20::party_i::{ + LocalSignature, SignBroadcastPhase1, SignDecommitPhase1, SignKeys, SignatureRecid, +}; +use gg20::state_machine::keygen::LocalKey; +use gg20::ErrorType; + +type Result = std::result::Result; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[allow(clippy::upper_case_acronyms)] +pub struct GWI(pub Point); +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GammaI(pub MessageB); +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct WI(pub MessageB); +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DeltaI(Scalar); +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TI(pub Point); +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TIProof(pub PedersenProof); +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RDash(Point); +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SI(pub Point); +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct HEGProof(pub HomoELGamalProof); + +pub struct Round0 { + /// Index of this party + /// + /// Must be in range `[0; n)` where `n` is number of parties involved in signing. + pub i: u16, + + /// List of parties' indexes from keygen protocol + /// + /// I.e. `s_l[i]` must be an index of party `i` that was used by this party in keygen protocol. + // s_l.len()` equals to `n` (number of parties involved in signing) + pub s_l: Vec, + + /// Party local secret share + pub local_key: LocalKey, +} + +impl Round0 { + pub fn proceed(self, mut output: O) -> Result + where + O: Push>, + { + let sign_keys = SignKeys::create( + &self.local_key.keys_linear.x_i, + &self.local_key.vss_scheme.clone(), + usize::from(self.s_l[usize::from(self.i - 1)]) - 1, + &self + .s_l + .iter() + .map(|&i| usize::from(i) - 1) + .collect::>(), + ); + let (bc1, decom1) = sign_keys.phase1_broadcast(); + + let party_ek = self.local_key.paillier_key_vec[usize::from(self.local_key.i - 1)].clone(); + let m_a = MessageA::a(&sign_keys.k_i, &party_ek, &self.local_key.h1_h2_n_tilde_vec); + + output.push(Msg { + sender: self.i, + receiver: None, + body: (m_a.0.clone(), bc1.clone()), + }); + + let round1 = Round1 { + i: self.i, + s_l: self.s_l.clone(), + local_key: self.local_key, + m_a, + sign_keys, + phase1_com: bc1, + phase1_decom: decom1, + }; + + Ok(round1) + } + + pub fn is_expensive(&self) -> bool { + true + } +} + +pub struct Round1 { + i: u16, + s_l: Vec, + local_key: LocalKey, + m_a: (MessageA, BigInt), + sign_keys: SignKeys, + phase1_com: SignBroadcastPhase1, + phase1_decom: SignDecommitPhase1, +} + +impl Round1 { + pub fn proceed( + self, + input: BroadcastMsgs<(MessageA, SignBroadcastPhase1)>, + mut output: O, + ) -> Result + where + O: Push>, + { + let (m_a_vec, bc_vec): (Vec<_>, Vec<_>) = input + .into_vec_including_me((self.m_a.0.clone(), self.phase1_com.clone())) + .into_iter() + .unzip(); + + let mut m_b_gamma_vec = Vec::new(); + let mut beta_vec = Vec::new(); + let mut m_b_w_vec = Vec::new(); + let mut ni_vec = Vec::new(); + + let ttag = self.s_l.len(); + let l_s: Vec<_> = self + .s_l + .iter() + .cloned() + .map(|i| usize::from(i) - 1) + .collect(); + let i = usize::from(self.i - 1); + for j in 0..ttag - 1 { + let ind = if j < i { j } else { j + 1 }; + + let (m_b_gamma, beta_gamma, _beta_randomness, _beta_tag) = MessageB::b( + &self.sign_keys.gamma_i, + &self.local_key.paillier_key_vec[l_s[ind]], + m_a_vec[ind].clone(), + &self.local_key.h1_h2_n_tilde_vec, + ) + .map_err(|e| { + Error::Round1(ErrorType { + error_type: e.to_string(), + bad_actors: vec![ind], + data: vec![], + }) + })?; + + let (m_b_w, beta_wi, _, _) = MessageB::b( + &self.sign_keys.w_i, + &self.local_key.paillier_key_vec[l_s[ind]], + m_a_vec[ind].clone(), + &self.local_key.h1_h2_n_tilde_vec, + ) + .map_err(|e| { + Error::Round1(ErrorType { + error_type: e.to_string(), + bad_actors: vec![ind], + data: vec![], + }) + })?; + + m_b_gamma_vec.push(m_b_gamma); + beta_vec.push(beta_gamma); + m_b_w_vec.push(m_b_w); + ni_vec.push(beta_wi); + } + + let party_indices = (1..=self.s_l.len()) + .map(|j| u16::try_from(j).unwrap()) + .filter(|&j| j != self.i); + for ((j, gamma_i), w_i) in party_indices.zip(m_b_gamma_vec).zip(m_b_w_vec) { + output.push(Msg { + sender: self.i, + receiver: Some(j), + body: (GammaI(gamma_i.clone()), WI(w_i.clone())), + }); + } + + Ok(Round2 { + i: self.i, + s_l: self.s_l, + local_key: self.local_key, + sign_keys: self.sign_keys, + m_a: self.m_a, + beta_vec, + ni_vec, + bc_vec, + m_a_vec, + phase1_decom: self.phase1_decom, + }) + } + + pub fn expects_messages( + i: u16, + n: u16, + ) -> Store> { + containers::BroadcastMsgsStore::new(i, n) + } + + pub fn is_expensive(&self) -> bool { + true + } +} + +pub struct Round2 { + i: u16, + s_l: Vec, + local_key: LocalKey, + sign_keys: SignKeys, + m_a: (MessageA, BigInt), + beta_vec: Vec>, + ni_vec: Vec>, + bc_vec: Vec, + m_a_vec: Vec, + phase1_decom: SignDecommitPhase1, +} + +impl Round2 { + pub fn proceed(self, input_p2p: P2PMsgs<(GammaI, WI)>, mut output: O) -> Result + where + O: Push>, // TODO: unify TI and TIProof + { + let (m_b_gamma_s, m_b_w_s): (Vec<_>, Vec<_>) = input_p2p + .into_vec() + .into_iter() + .map(|(gamma_i, w_i)| (gamma_i.0, w_i.0)) + .unzip(); + + let mut alpha_vec = Vec::new(); + let mut miu_vec = Vec::new(); + + let ttag = self.s_l.len(); + let index = usize::from(self.i) - 1; + let l_s: Vec<_> = self + .s_l + .iter() + .cloned() + .map(|i| usize::from(i) - 1) + .collect(); + let g_w_vec = SignKeys::g_w_vec( + &self.local_key.pk_vec[..], + &l_s[..], + &self.local_key.vss_scheme, + ); + for j in 0..ttag - 1 { + let ind = if j < index { j } else { j + 1 }; + let m_b = m_b_gamma_s[j].clone(); + + let alpha_ij_gamma = m_b + .verify_proofs_get_alpha(&self.local_key.paillier_dk, &self.sign_keys.k_i) + .map_err(|e| { + Error::Round3(ErrorType { + error_type: e.to_string(), + bad_actors: vec![ind], + data: vec![], + }) + })?; + let m_b = m_b_w_s[j].clone(); + let alpha_ij_wi = m_b + .verify_proofs_get_alpha(&self.local_key.paillier_dk, &self.sign_keys.k_i) + .map_err(|e| { + Error::Round3(ErrorType { + error_type: e.to_string(), + bad_actors: vec![ind], + data: vec![], + }) + })?; + assert_eq!(m_b.b_proof.pk, g_w_vec[ind]); //TODO: return error + + alpha_vec.push(alpha_ij_gamma.0); + miu_vec.push(alpha_ij_wi.0); + } + + let delta_i = self.sign_keys.phase2_delta_i(&alpha_vec, &self.beta_vec); + + let sigma_i = self.sign_keys.phase2_sigma_i(&miu_vec, &self.ni_vec); + let (t_i, l_i, t_i_proof) = SignKeys::phase3_compute_t_i(&sigma_i); + output.push(Msg { + sender: self.i, + receiver: None, + body: ( + DeltaI(delta_i.clone()), + TI(t_i.clone()), + TIProof(t_i_proof.clone()), + ), + }); + + Ok(Round3 { + i: self.i, + s_l: self.s_l, + local_key: self.local_key, + sign_keys: self.sign_keys, + m_a: self.m_a, + mb_gamma_s: m_b_gamma_s, + bc_vec: self.bc_vec, + m_a_vec: self.m_a_vec, + delta_i, + t_i, + l_i, + sigma_i, + t_i_proof, + phase1_decom: self.phase1_decom, + }) + } + + pub fn expects_messages(i: u16, n: u16) -> Store> { + containers::P2PMsgsStore::new(i, n) + } + + pub fn is_expensive(&self) -> bool { + true + } +} + +pub struct Round3 { + i: u16, + s_l: Vec, + local_key: LocalKey, + sign_keys: SignKeys, + m_a: (MessageA, BigInt), + mb_gamma_s: Vec, + bc_vec: Vec, + m_a_vec: Vec, + delta_i: Scalar, + t_i: Point, + l_i: Scalar, + sigma_i: Scalar, + t_i_proof: PedersenProof, + + phase1_decom: SignDecommitPhase1, +} + +impl Round3 { + pub fn proceed( + self, + input: BroadcastMsgs<(DeltaI, TI, TIProof)>, + mut output: O, + ) -> Result + where + O: Push>, + { + let (delta_vec, t_vec, t_proof_vec) = input + .into_vec_including_me(( + DeltaI(self.delta_i), + TI(self.t_i.clone()), + TIProof(self.t_i_proof), + )) + .into_iter() + .map(|(delta_i, t_i, t_i_proof)| (delta_i.0, t_i.0, t_i_proof.0)) + .unzip3(); + + for i in 0..t_vec.len() { + assert_eq!(t_vec[i], t_proof_vec[i].com); + } + + let delta_inv = SignKeys::phase3_reconstruct_delta(&delta_vec); + let ttag = self.s_l.len(); + for (idx, proof) in t_proof_vec.iter().enumerate().take(ttag) { + PedersenProof::verify(proof).map_err(|e| { + Error::Round3(ErrorType { + error_type: e.to_string(), + bad_actors: vec![idx + 1], + data: vec![], + }) + })?; + } + + output.push(Msg { + sender: self.i, + receiver: None, + body: self.phase1_decom.clone(), + }); + + Ok(Round4 { + i: self.i, + s_l: self.s_l, + local_key: self.local_key, + sign_keys: self.sign_keys, + m_a: self.m_a, + mb_gamma_s: self.mb_gamma_s, + bc_vec: self.bc_vec, + m_a_vec: self.m_a_vec, + t_i: self.t_i, + l_i: self.l_i, + sigma_i: self.sigma_i, + phase1_decom: self.phase1_decom, + delta_inv, + t_vec, + }) + } + + pub fn expects_messages(i: u16, n: u16) -> Store> { + containers::BroadcastMsgsStore::new(i, n) + } + + pub fn is_expensive(&self) -> bool { + true + } +} + +pub struct Round4 { + i: u16, + s_l: Vec, + local_key: LocalKey, + sign_keys: SignKeys, + m_a: (MessageA, BigInt), + mb_gamma_s: Vec, + bc_vec: Vec, + m_a_vec: Vec, + t_i: Point, + l_i: Scalar, + sigma_i: Scalar, + delta_inv: Scalar, + t_vec: Vec>, + phase1_decom: SignDecommitPhase1, +} + +impl Round4 { + pub fn proceed( + self, + decommit_round1: BroadcastMsgs, + mut output: O, + ) -> Result + where + O: Push)>>, + { + let decom_vec: Vec<_> = decommit_round1.into_vec_including_me(self.phase1_decom.clone()); + + let ttag = self.s_l.len(); + let b_proof_vec: Vec<_> = (0..ttag - 1).map(|i| &self.mb_gamma_s[i].b_proof).collect(); + let R = SignKeys::phase4( + &self.delta_inv, + &b_proof_vec[..], + decom_vec, + &self.bc_vec, + usize::from(self.i - 1), + ) + .map_err(|e| Error::Round5(e))?; + + let R_dash = &R * &self.sign_keys.k_i; + + // each party sends first message to all other parties + let mut phase5_proofs_vec = Vec::new(); + let l_s: Vec<_> = self + .s_l + .iter() + .cloned() + .map(|i| usize::from(i) - 1) + .collect(); + let index = usize::from(self.i - 1); + for j in 0..ttag - 1 { + let ind = if j < index { j } else { j + 1 }; + let proof = LocalSignature::phase5_proof_pdl( + &R_dash, + &R, + &self.m_a.0.c, + &self.local_key.paillier_key_vec[l_s[index]], + &self.sign_keys.k_i, + &self.m_a.1, + &self.local_key.h1_h2_n_tilde_vec[l_s[ind]], + ); + + phase5_proofs_vec.push(proof); + } + + output.push(Msg { + sender: self.i, + receiver: None, + body: (RDash(R_dash.clone()), phase5_proofs_vec.clone()), + }); + + Ok(Round5 { + i: self.i, + s_l: self.s_l, + local_key: self.local_key, + sign_keys: self.sign_keys, + t_vec: self.t_vec, + m_a_vec: self.m_a_vec, + t_i: self.t_i, + l_i: self.l_i, + sigma_i: self.sigma_i, + R, + R_dash, + phase5_proofs_vec, + }) + } + + pub fn expects_messages(i: u16, n: u16) -> Store> { + containers::BroadcastMsgsStore::new(i, n) + } + + pub fn is_expensive(&self) -> bool { + true + } +} + +pub struct Round5 { + i: u16, + s_l: Vec, + local_key: LocalKey, + sign_keys: SignKeys, + t_vec: Vec>, + m_a_vec: Vec, + t_i: Point, + l_i: Scalar, + sigma_i: Scalar, + R: Point, + R_dash: Point, + phase5_proofs_vec: Vec, +} + +impl Round5 { + pub fn proceed( + self, + input: BroadcastMsgs<(RDash, Vec)>, + mut output: O, + ) -> Result + where + O: Push>, + { + let (r_dash_vec, pdl_proof_mat_inc_me): (Vec<_>, Vec<_>) = input + .into_vec_including_me((RDash(self.R_dash), self.phase5_proofs_vec)) + .into_iter() + .map(|(r_dash, pdl_proof)| (r_dash.0, pdl_proof)) + .unzip(); + + let l_s: Vec<_> = self + .s_l + .iter() + .cloned() + .map(|i| usize::from(i) - 1) + .collect(); + let ttag = self.s_l.len(); + for i in 0..ttag { + LocalSignature::phase5_verify_pdl( + &pdl_proof_mat_inc_me[i], + &r_dash_vec[i], + &self.R, + &self.m_a_vec[i].c, + &self.local_key.paillier_key_vec[l_s[i]], + &self.local_key.h1_h2_n_tilde_vec, + &l_s, + i, + ) + .map_err(|e| Error::Round5(e))?; + } + LocalSignature::phase5_check_R_dash_sum(&r_dash_vec).map_err(|e| { + Error::Round5(ErrorType { + error_type: e.to_string(), + bad_actors: vec![], + data: vec![], + }) + })?; + + let (S_i, homo_elgamal_proof) = LocalSignature::phase6_compute_S_i_and_proof_of_consistency( + &self.R, + &self.t_i, + &self.sigma_i, + &self.l_i, + ); + + output.push(Msg { + sender: self.i, + receiver: None, + body: (SI(S_i.clone()), HEGProof(homo_elgamal_proof.clone())), + }); + + Ok(Round6 { + S_i, + homo_elgamal_proof, + s_l: self.s_l, + protocol_output: CompletedOfflineStage { + i: self.i, + local_key: self.local_key, + sign_keys: self.sign_keys, + t_vec: self.t_vec, + R: self.R, + sigma_i: self.sigma_i, + }, + }) + } + + pub fn expects_messages(i: u16, n: u16) -> Store)>> { + containers::BroadcastMsgsStore::new(i, n) + } + + pub fn is_expensive(&self) -> bool { + true + } +} + +pub struct Round6 { + S_i: Point, + homo_elgamal_proof: HomoELGamalProof, + s_l: Vec, + /// Round 6 guards protocol output until final checks are taken the place + protocol_output: CompletedOfflineStage, +} + +impl Round6 { + pub fn proceed( + self, + input: BroadcastMsgs<(SI, HEGProof)>, + ) -> Result { + let (S_i_vec, hegp_vec): (Vec<_>, Vec<_>) = input + .into_vec_including_me((SI(self.S_i), HEGProof(self.homo_elgamal_proof))) + .into_iter() + .map(|(s_i, hegp_i)| (s_i.0, hegp_i.0)) + .unzip(); + let R_vec: Vec<_> = iter::repeat(self.protocol_output.R.clone()) + .take(self.s_l.len()) + .collect(); + + LocalSignature::phase6_verify_proof( + &S_i_vec, + &hegp_vec, + &R_vec, + &self.protocol_output.t_vec, + ) + .map_err(Error::Round6VerifyProof)?; + LocalSignature::phase6_check_S_i_sum(&self.protocol_output.local_key.y_sum_s, &S_i_vec) + .map_err(Error::Round6CheckSig)?; + + Ok(self.protocol_output) + } + + pub fn expects_messages(i: u16, n: u16) -> Store> { + containers::BroadcastMsgsStore::new(i, n) + } + + pub fn is_expensive(&self) -> bool { + true + } +} + +#[derive(Clone)] +#[allow(dead_code)] +pub struct CompletedOfflineStage { + i: u16, + local_key: LocalKey, + sign_keys: SignKeys, + t_vec: Vec>, + R: Point, + sigma_i: Scalar, +} + +impl CompletedOfflineStage { + pub fn public_key(&self) -> &Point { + &self.local_key.y_sum_s + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PartialSignature(Scalar); + +#[derive(Clone)] +pub struct Round7 { + local_signature: LocalSignature, +} + +impl Round7 { + pub fn new( + message: &BigInt, + completed_offline_stage: CompletedOfflineStage, + ) -> Result<(Self, PartialSignature)> { + let local_signature = LocalSignature::phase7_local_sig( + &completed_offline_stage.sign_keys.k_i, + message, + &completed_offline_stage.R, + &completed_offline_stage.sigma_i, + &completed_offline_stage.local_key.y_sum_s, + ); + let partial = PartialSignature(local_signature.s_i.clone()); + Ok((Self { local_signature }, partial)) + } + + pub fn proceed_manual(self, sigs: &[PartialSignature]) -> Result { + let sigs = sigs.iter().map(|s_i| s_i.0.clone()).collect::>(); + self.local_signature + .output_signature(&sigs) + .map_err(Error::Round7) + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("round 1: {0:?}")] + Round1(ErrorType), + #[error("round 2 stage 3: {0:?}")] + Round2Stage3(crate::Error), + #[error("round 2 stage 4: {0:?}")] + Round2Stage4(ErrorType), + #[error("round 3: {0:?}")] + Round3(ErrorType), + #[error("round 5: {0:?}")] + Round5(ErrorType), + #[error("round 6: verify proof: {0:?}")] + Round6VerifyProof(ErrorType), + #[error("round 6: check sig: {0:?}")] + Round6CheckSig(crate::Error), + #[error("round 7: {0:?}")] + Round7(crate::Error), +} + +trait IteratorExt: Iterator { + fn unzip3(self) -> (Vec, Vec, Vec) + where + Self: Iterator + Sized, + { + let (mut a, mut b, mut c) = (vec![], vec![], vec![]); + for (a_i, b_i, c_i) in self { + a.push(a_i); + b.push(b_i); + c.push(c_i); + } + (a, b, c) + } +} + +impl IteratorExt for I where I: Iterator {} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/traits.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/traits.rs new file mode 100644 index 00000000..2377a8ce --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/traits.rs @@ -0,0 +1,11 @@ +pub trait RoundBlame { + /// Retrieves a list of uncorporative parties + /// + /// Returns a numbers of messages yet to recieve and list of parties to send messages for the current round + fn round_blame(&self) -> (u16, Vec); +} + +pub trait MessageRoundID: Clone { + /// Returns the round id of the message + fn round_id(&self) -> u16; +} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs new file mode 100644 index 00000000..9edc567e --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs @@ -0,0 +1,783 @@ +#![allow(non_snake_case)] + +use crate::protocols::multi_party_ecdsa::gg_2018::VerifiableSS; +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ +use crate::protocols::multi_party_ecdsa::gg_2020::blame::{ + GlobalStatePhase5, GlobalStatePhase6, GlobalStatePhase7, LocalStatePhase5, LocalStatePhase6, +}; +use crate::protocols::multi_party_ecdsa::gg_2020::party_i::SignatureRecid; +use crate::protocols::multi_party_ecdsa::gg_2020::party_i::{ + KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Keys, LocalSignature, Parameters, SharedKeys, + SignKeys, +}; +use crate::utilities::mta::{MessageA, MessageB}; +use curv::arithmetic::traits::Converter; + +use crate::protocols::multi_party_ecdsa::gg_2020::ErrorType; +use crate::utilities::zk_pdl_with_slack::PDLwSlackProof; +use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; +use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; +use curv::cryptographic_primitives::proofs::sigma_valid_pedersen::PedersenProof; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use paillier::*; +use sha2::Sha256; +use zk_paillier::zkproofs::DLogStatement; + +#[test] +fn test_keygen_t1_n2() { + assert!(keygen_t_n_parties(1, 2).is_ok()); +} + +#[test] +fn test_keygen_t2_n3() { + assert!(keygen_t_n_parties(2, 3).is_ok()); +} + +#[test] +fn test_keygen_t2_n4() { + assert!(keygen_t_n_parties(2, 4).is_ok()); +} + +#[test] +fn test_sign_n2_t1_ttag1() { + let _ = sign(1, 2, 2, vec![0, 1], 0, &[0]); +} + +#[test] +fn test_sign_n5_t2_ttag4() { + let _ = sign(2, 5, 4, vec![0, 2, 3, 4], 0, &[0]); +} +#[test] +fn test_sign_n8_t4_ttag6() { + let _ = sign(4, 8, 6, vec![0, 1, 2, 4, 6, 7], 0, &[0]); +} + +// party 1 is corrupting step 5 +#[test] +fn test_sign_n2_t1_ttag1_corrupt_step5_party1() { + let res = sign(1, 2, 2, vec![0, 1], 5, &[0]); + assert!(&res.err().unwrap().bad_actors[..] == &[0]) +} + +// party 2 is corrupting step 5 +#[test] +fn test_sign_n2_t1_ttag1_corrupt_step5_party2() { + let res = sign(1, 2, 2, vec![0, 1], 5, &[1]); + assert!(&res.err().unwrap().bad_actors[..] == &[1]) +} + +// both parties are corrupting step 5 +#[test] +fn test_sign_n2_t1_ttag1_corrupt_step5_party12() { + let res = sign(1, 2, 2, vec![0, 1], 5, &[0, 1]); + assert!(&res.err().unwrap().bad_actors[..] == &[0, 1]) +} +// party 1 is corrupted +#[test] +fn test_sign_n5_t2_ttag4_corrupt_step5_party1() { + let res = sign(2, 5, 4, vec![0, 2, 3, 4], 5, &[0]); + assert!(&res.err().unwrap().bad_actors[..] == &[0]) +} + +// party 1,4 are corrupted +#[test] +fn test_sign_n5_t2_ttag4_corrupt_step5_party14() { + let res = sign(2, 5, 4, vec![0, 2, 3, 4], 5, &[0, 3]); + assert!(&res.err().unwrap().bad_actors[..] == &[0, 3]) +} + +// party 1 is corrupting step 6 +#[test] +fn test_sign_n2_t1_ttag1_corrupt_step6_party1() { + let res = sign(1, 2, 2, vec![0, 1], 6, &[0]); + assert!(&res.err().unwrap().bad_actors[..] == &[0]) +} +// party 2 is corrupting step 6 +#[test] +fn test_sign_n2_t1_ttag1_corrupt_step6_party2() { + let res = sign(1, 2, 2, vec![0, 1], 6, &[1]); + assert!(&res.err().unwrap().bad_actors[..] == &[1]) +} + +// both parties are corrupting step 6 +#[test] +fn test_sign_n2_t1_ttag1_corrupt_step6_party12() { + let res = sign(1, 2, 2, vec![0, 1], 6, &[0, 1]); + assert!(&res.err().unwrap().bad_actors[..] == &[0, 1]) +} +// party 1 is corrupted +#[test] +fn test_sign_n5_t2_ttag4_corrupt_step6_party1() { + let res = sign(2, 5, 4, vec![0, 2, 3, 4], 6, &[0]); + assert!(&res.err().unwrap().bad_actors[..] == &[0]) +} + +// party 1,4 are corrupted +#[test] +fn test_sign_n5_t2_ttag4_corrupt_step6_party14() { + let res = sign(2, 5, 4, vec![0, 2, 3, 4], 6, &[0, 3]); + assert!(&res.err().unwrap().bad_actors[..] == &[0, 3]) +} + +// party 1 is corrupting step 5 +#[test] +fn test_sign_n2_t1_ttag1_corrupt_step7_party2() { + let res = sign(1, 2, 2, vec![0, 1], 7, &[1]); + assert!(&res.err().unwrap().bad_actors[..] == &[1]) +} + +// party 2,4 are corrupted +#[test] +fn test_sign_n5_t2_ttag4_corrupt_step7_party24() { + let res = sign(2, 5, 4, vec![0, 2, 3, 4], 7, &[1, 3]); + assert!(&res.err().unwrap().bad_actors[..] == &[1, 3]) +} + +fn keygen_t_n_parties( + t: u16, + n: u16, +) -> Result< + ( + Vec, + Vec>, + Vec>, + Point, + VerifiableSS, + Vec, + Vec, + ), + ErrorType, +> { + let params = Parameters { + threshold: t, + share_count: n, + }; + let (t, n) = (t as usize, n as usize); + let party_keys_vec = (0..n).map(Keys::create).collect::>(); + + let (bc1_vec, decom_vec): (Vec<_>, Vec<_>) = party_keys_vec + .iter() + .map(|k| k.phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2()) + .unzip(); + + let e_vec = bc1_vec + .iter() + .map(|bc1| bc1.e.clone()) + .collect::>(); + let h1_h2_N_tilde_vec = bc1_vec + .iter() + .map(|bc1| bc1.dlog_statement.clone()) + .collect::>(); + let y_vec = (0..n) + .map(|i| decom_vec[i].y_i.clone()) + .collect::>>(); + let mut y_vec_iter = y_vec.iter(); + let head = y_vec_iter.next().unwrap(); + let tail = y_vec_iter; + let y_sum = tail.fold(head.clone(), |acc, x| acc + x); + + let mut vss_scheme_vec = Vec::new(); + let mut secret_shares_vec = Vec::new(); + let mut index_vec = Vec::new(); + + // TODO: find way to propagate the error and bad actors list properly + let vss_result: Vec<_> = party_keys_vec + .iter() + .map(|k| { + k.phase1_verify_com_phase3_verify_correct_key_verify_dlog_phase2_distribute( + ¶ms, &decom_vec, &bc1_vec, + ) + .expect("") + }) + .collect(); + + for (vss_scheme, secret_shares, index) in vss_result { + vss_scheme_vec.push(vss_scheme); + secret_shares_vec.push(secret_shares); // cannot unzip + index_vec.push(index as u16); + } + + let vss_scheme_for_test = vss_scheme_vec.clone(); + + let party_shares = (0..n) + .map(|i| { + (0..n) + .map(|j| { + let vec_j = &secret_shares_vec[j]; + vec_j[i].clone() + }) + .collect::>>() + }) + .collect::>>>(); + + let mut shared_keys_vec = Vec::new(); + let mut dlog_proof_vec = Vec::new(); + for (i, key) in party_keys_vec.iter().enumerate() { + let res = key.phase2_verify_vss_construct_keypair_phase3_pok_dlog( + ¶ms, + &y_vec, + &party_shares[i], + &vss_scheme_vec, + (&index_vec[i] + 1).into(), + ); + if res.is_err() { + return Err(res.err().unwrap()); + } + let (shared_keys, dlog_proof) = res.unwrap(); + shared_keys_vec.push(shared_keys); + dlog_proof_vec.push(dlog_proof); + } + + let pk_vec = (0..n) + .map(|i| dlog_proof_vec[i].pk.clone()) + .collect::>>(); + + let dlog_verification = Keys::verify_dlog_proofs_check_against_vss( + ¶ms, + &dlog_proof_vec, + &y_vec, + &vss_scheme_vec, + ); + + if dlog_verification.is_err() { + return Err(dlog_verification.err().unwrap()); + } + //test + let xi_vec = (0..=t) + .map(|i| shared_keys_vec[i].x_i.clone()) + .collect::>>(); + let x = vss_scheme_for_test[0] + .clone() + .reconstruct(&index_vec[0..=t], &xi_vec); + let sum_u_i = party_keys_vec + .iter() + .fold(Scalar::::zero(), |acc, x| acc + &x.u_i); + assert_eq!(x, sum_u_i); + + Ok(( + party_keys_vec, // Paillier keys, keypair, N, h1, h2 + shared_keys_vec, // Private shares for this MPC keypair + pk_vec, // dlog proof for x_i + y_sum, // public key for this MPC keypair. + vss_scheme_for_test[0].clone(), // This contains the commitments for each initial share and the shares itself + e_vec, // paillier encryption keys. Why separate ? + h1_h2_N_tilde_vec, + )) +} + +fn sign( + t: u16, + n: u16, + ttag: u16, //number of participants + s: Vec, //participant list indexed from zero + corrupt_step: usize, + corrupted_parties: &[usize], +) -> Result { + // full key gen emulation + let (party_keys_vec, shared_keys_vec, pk_vec, y, vss_scheme, ek_vec, dlog_statement_vec) = + keygen_t_n_parties(t, n).unwrap(); + + // transform the t,n share to t,t+1 share. Get the public keys for the same. + let g_w_vec = SignKeys::g_w_vec(&pk_vec, &s[..], &vss_scheme); + + let private_vec = (0..shared_keys_vec.len()) + .map(|i| shared_keys_vec[i].x_i.clone()) + .collect::>(); + // make sure that we have t t); + let ttag = ttag as usize; + assert_eq!(s.len(), ttag); + + // each party creates a signing key. This happens in parallel IRL. In this test we + // create a vector of signing keys, one for each party. + // throughout i will index parties + let sign_keys_vec = (0..ttag) + .map(|i| SignKeys::create(&private_vec[s[i]], &vss_scheme, s[i], &s)) + .collect::>(); + + // each party computes [Ci,Di] = com(g^gamma_i) and broadcast the commitments + let (bc1_vec, decommit_vec1): (Vec<_>, Vec<_>) = + sign_keys_vec.iter().map(|k| k.phase1_broadcast()).unzip(); + + // each signer's dlog statement. in reality, parties prove statements + // using only other parties' h1,h2,N_tilde. here we also use own parameters for simplicity + let signers_dlog_statements = (0..ttag) + .map(|i| dlog_statement_vec[s[i]].clone()) + .collect::>(); + + // each party i BROADCASTS encryption of k_i under her Paillier key + // m_a_vec = [ma_0;ma_1;,...] + // we assume here that party sends the same encryption to all other parties. + // It should be changed to different encryption (randomness) to each counter party + let m_a_vec: Vec<_> = sign_keys_vec + .iter() + .enumerate() + .map(|(i, k)| MessageA::a(&k.k_i, &party_keys_vec[s[i]].ek, &signers_dlog_statements)) + .collect(); + + // #each party i sends responses to m_a_vec she received (one response with input gamma_i and one with w_i) + // #m_b_gamma_vec_all is a matrix where column i is a vector of message_b's that were sent to party i + + // aggregation of the n messages of all parties + let mut m_b_gamma_vec_all = Vec::new(); + let mut beta_vec_all = Vec::new(); + let mut m_b_w_vec_all = Vec::new(); + let mut ni_vec_all = Vec::new(); + let mut beta_randomness_vec_all = Vec::new(); //should be accessible in case of blame + let mut beta_tag_vec_all = Vec::new(); //should be accessible in case of blame + + // m_b_gamma and m_b_w are BROADCAST + for i in 0..ttag { + let mut m_b_gamma_vec = Vec::new(); + let mut beta_vec = Vec::new(); + let mut beta_randomness_vec = Vec::new(); + let mut beta_tag_vec = Vec::new(); + let mut m_b_w_vec = Vec::new(); + let mut ni_vec = Vec::new(); + + for j in 0..ttag - 1 { + let ind = if j < i { j } else { j + 1 }; + let (m_b_gamma, beta_gamma, beta_randomness, beta_tag) = MessageB::b( + &sign_keys_vec[ind].gamma_i, + &ek_vec[s[i]], + m_a_vec[i].0.clone(), + &signers_dlog_statements, + ) + .expect("Alice's range proofs in MtA failed"); + let (m_b_w, beta_wi, _, _) = MessageB::b( + &sign_keys_vec[ind].w_i, + &ek_vec[s[i]], + m_a_vec[i].0.clone(), + &signers_dlog_statements, + ) + .expect("Alice's range proofs in MtA failed"); + + m_b_gamma_vec.push(m_b_gamma); + beta_vec.push(beta_gamma); + beta_randomness_vec.push(beta_randomness); + beta_tag_vec.push(beta_tag); + m_b_w_vec.push(m_b_w); + ni_vec.push(beta_wi); + } + m_b_gamma_vec_all.push(m_b_gamma_vec.clone()); + beta_vec_all.push(beta_vec.clone()); + beta_tag_vec_all.push(beta_tag_vec.clone()); + beta_randomness_vec_all.push(beta_randomness_vec.clone()); + m_b_w_vec_all.push(m_b_w_vec.clone()); + ni_vec_all.push(ni_vec.clone()); + } + + // Here we complete the MwA protocols by taking the mb matrices and starting with the first column, + // generating the appropriate message. The first column is the answers of party 1 to mb sent from other parties. + // The second column is the answers that party 2 is sending and so on. + + // TODO: simulate as IRL + let mut alpha_vec_all = Vec::new(); + let mut miu_vec_all = Vec::new(); + let mut miu_bigint_vec_all = Vec::new(); //required for the phase6 IA sub protocol + + for i in 0..ttag { + let mut alpha_vec = Vec::new(); + let mut miu_vec = Vec::new(); + let mut miu_bigint_vec = Vec::new(); //required for the phase6 IA sub protocol + + let m_b_gamma_vec_i = &m_b_gamma_vec_all[i]; + let m_b_w_vec_i = &m_b_w_vec_all[i]; + + // in case + for j in 0..ttag - 1 { + let ind = if j < i { j } else { j + 1 }; + let m_b = m_b_gamma_vec_i[j].clone(); + + // TODO: identify these errors + let alpha_ij_gamma = m_b + .verify_proofs_get_alpha(&party_keys_vec[s[i]].dk, &sign_keys_vec[i].k_i) + .expect("wrong dlog or m_b"); + let m_b = m_b_w_vec_i[j].clone(); + let alpha_ij_wi = m_b + .verify_proofs_get_alpha(&party_keys_vec[s[i]].dk, &sign_keys_vec[i].k_i) + .expect("wrong dlog or m_b"); + + // since we actually run two MtAwc each party needs to make sure that the values B are the same as the public values + // here for b=w_i the parties already know W_i = g^w_i for each party so this check is done here. for b = gamma_i the check will be later when g^gamma_i will become public + // currently we take the W_i from the other parties signing keys + // TODO: use pk_vec (first change from x_i to w_i) for this check. + assert_eq!(m_b.b_proof.pk, sign_keys_vec[ind].g_w_i); + + alpha_vec.push(alpha_ij_gamma.0); + miu_vec.push(alpha_ij_wi.0); + miu_bigint_vec.push(alpha_ij_wi.1); + } + alpha_vec_all.push(alpha_vec.clone()); + miu_vec_all.push(miu_vec.clone()); + miu_bigint_vec_all.push(miu_bigint_vec.clone()); + } + + let mut delta_vec = Vec::new(); + let mut sigma_vec = Vec::new(); + + for i in 0..ttag { + // prepare beta_vec of party_i: + let beta_vec = (0..ttag - 1) + .map(|j| { + let ind1 = if j < i { j } else { j + 1 }; + let ind2 = if j < i { i - 1 } else { i }; + + beta_vec_all[ind1][ind2].clone() + }) + .collect::>>(); + + // prepare ni_vec of party_i: + let ni_vec = (0..ttag - 1) + .map(|j| { + let ind1 = if j < i { j } else { j + 1 }; + let ind2 = if j < i { i - 1 } else { i }; + ni_vec_all[ind1][ind2].clone() + }) + .collect::>>(); + + let mut delta = sign_keys_vec[i].phase2_delta_i(&alpha_vec_all[i], &beta_vec); + + let mut sigma = sign_keys_vec[i].phase2_sigma_i(&miu_vec_all[i], &ni_vec); + // test wrong delta corruption + if corrupt_step == 5 && corrupted_parties.iter().any(|&x| x == i) { + delta = &delta + δ + } + // test wrong sigma corruption + if corrupt_step == 6 && corrupted_parties.iter().any(|&x| x == i) { + sigma = &sigma + σ + } + delta_vec.push(delta); + sigma_vec.push(sigma); + } + + // all parties broadcast delta_i and compute delta_i ^(-1) + let delta_inv = SignKeys::phase3_reconstruct_delta(&delta_vec); + // all parties broadcast T_i: + let mut T_vec = Vec::new(); + let mut l_vec = Vec::new(); + let mut T_proof_vec = Vec::new(); + for i in 0..ttag { + let (T_i, l_i, T_proof_i) = SignKeys::phase3_compute_t_i(&sigma_vec[i]); + T_vec.push(T_i); + l_vec.push(l_i); + T_proof_vec.push(T_proof_i); + } + // verify T_proof_vec + for i in 0..ttag { + assert_eq!(T_vec[i], T_proof_vec[i].com.clone()); + PedersenProof::verify(&T_proof_vec[i]).expect("error T proof"); + } + // de-commit to g^gamma_i from phase1, test comm correctness, and that it is the same value used in MtA. + // Return R + + let R_vec = (0..ttag) + .map(|i| { + // each party i tests all B = g^b = g ^ gamma_i she received. + let m_b_gamma_vec = &m_b_gamma_vec_all[i]; + let b_proof_vec = (0..ttag - 1) + .map(|j| &m_b_gamma_vec[j].b_proof) + .collect::>>(); + SignKeys::phase4(&delta_inv, &b_proof_vec, decommit_vec1.clone(), &bc1_vec, i) + .expect("") //TODO: propagate the error + }) + .collect::>>(); + + //new phase 5 + // all parties broadcast R_dash = k_i * R. + let R_dash_vec = (0..ttag) + .map(|i| &R_vec[i] * &sign_keys_vec[i].k_i) + .collect::>>(); + + // each party sends first message to all other parties + let mut phase5_proofs_vec: Vec> = vec![Vec::new(); ttag]; + for i in 0..ttag { + for j in 0..ttag - 1 { + let ind = if j < i { j } else { j + 1 }; + let proof = LocalSignature::phase5_proof_pdl( + &R_dash_vec[i], + &R_vec[i], + &m_a_vec[i].0.c, + &ek_vec[s[i]], + &sign_keys_vec[i].k_i, + &m_a_vec[i].1, + &dlog_statement_vec[s[ind]], + ); + + phase5_proofs_vec[i].push(proof); + } + } + + for i in 0..ttag { + let phase5_verify_zk = LocalSignature::phase5_verify_pdl( + &phase5_proofs_vec[i], + &R_dash_vec[i], + &R_vec[i], + &m_a_vec[i].0.c, + &ek_vec[s[i]], + &dlog_statement_vec[..], + &s, + i, + ); + if phase5_verify_zk.is_err() { + return Err(phase5_verify_zk.err().unwrap()); + } + } + + //each party must run the test + let phase5_check = LocalSignature::phase5_check_R_dash_sum(&R_dash_vec); + if phase5_check.is_err() { + // initiate phase 5 blame protocol to learn which parties acted maliciously. + // each party generates local state and share with other parties. + // assuming sync communication - if a message was failed to arrive from a party - + // this party should automatically be blamed + let mut local_state_vec = Vec::new(); + for i in 0..ttag { + // compose beta tag vector: + let mut beta_tag_vec_to_test = Vec::new(); + let mut beta_randomness_vec_to_test = Vec::new(); + for j in 0..ttag - 1 { + let ind1 = if j < i { j } else { j + 1 }; + let ind2 = if j < i { i - 1 } else { i }; + beta_tag_vec_to_test.push(beta_tag_vec_all[ind1][ind2].clone()); + beta_randomness_vec_to_test.push(beta_randomness_vec_all[ind1][ind2].clone()); + } + + let local_state = LocalStatePhase5 { + k: sign_keys_vec[i].k_i.clone(), + k_randomness: m_a_vec[i].1.clone(), + gamma: sign_keys_vec[i].gamma_i.clone(), + beta_randomness: beta_randomness_vec_to_test, + beta_tag: beta_tag_vec_to_test, + encryption_key: ek_vec[s[i]].clone(), + }; + local_state_vec.push(local_state); + } + //g_gamma_vec: + let g_gamma_vec = (0..decommit_vec1.len()) + .map(|i| decommit_vec1[i].g_gamma_i.clone()) + .collect::>>(); + //m_a_vec + let m_a_vec = (0..m_a_vec.len()) + .map(|i| m_a_vec[i].0.clone()) + .collect::>(); + + // reduce ek vec to only ek of participants : + let ek_vec = (0..ttag) + .map(|k| ek_vec[s[k]].clone()) + .collect::>(); + let global_state = GlobalStatePhase5::local_state_to_global_state( + &ek_vec[..], + &delta_vec, + &g_gamma_vec[..], + &m_a_vec[..], + m_b_gamma_vec_all, + &local_state_vec[..], + ); + global_state.phase5_blame()?; + } + + let mut S_vec = Vec::new(); + let mut homo_elgamal_proof_vec = Vec::new(); + for i in 0..ttag { + let (S_i, homo_elgamal_proof) = LocalSignature::phase6_compute_S_i_and_proof_of_consistency( + &R_vec[i], + &T_vec[i], + &sigma_vec[i], + &l_vec[i], + ); + S_vec.push(S_i); + homo_elgamal_proof_vec.push(homo_elgamal_proof); + } + + LocalSignature::phase6_verify_proof(&S_vec, &homo_elgamal_proof_vec, &R_vec, &T_vec)?; + + let phase6_check = LocalSignature::phase6_check_S_i_sum(&y, &S_vec); + if phase6_check.is_err() { + // initiate phase 6 blame protocol to learn which parties acted maliciously. + // each party generates local state and share with other parties. + // assuming sync communication - if a message was failed to arrive from a party - + // this party should automatically be blamed + + let mut local_state_vec = Vec::new(); + for i in 0..ttag { + let mut miu_randomness_vec = Vec::new(); + for j in 0..ttag - 1 { + let rand = GlobalStatePhase6::extract_paillier_randomness( + &m_b_w_vec_all[i][j].c, + &party_keys_vec[s[i]].dk, + ); + miu_randomness_vec.push(rand); + } + let proof = GlobalStatePhase6::ecddh_proof(&sigma_vec[i], &R_vec[i], &S_vec[i]); + let local_state = LocalStatePhase6 { + k: sign_keys_vec[i].k_i.clone(), + k_randomness: m_a_vec[i].1.clone(), + miu: miu_bigint_vec_all[i].clone(), + miu_randomness: miu_randomness_vec, + proof_of_eq_dlog: proof, + }; + local_state_vec.push(local_state); + } + + //m_a_vec + let m_a_vec = (0..m_a_vec.len()) + .map(|i| m_a_vec[i].0.clone()) + .collect::>(); + + // reduce ek vec to only ek of participants : + let ek_vec = (0..ttag) + .map(|k| ek_vec[s[k]].clone()) + .collect::>(); + + let global_state = GlobalStatePhase6::local_state_to_global_state( + &ek_vec[..], + &S_vec[..], + &g_w_vec[..], + &m_a_vec[..], + m_b_w_vec_all, + &local_state_vec[..], + ); + global_state.phase6_blame(&R_vec[0])?; + } + + let message: [u8; 4] = [79, 77, 69, 82]; + let message_bn = Sha256::new() + .chain_bigint(&BigInt::from_bytes(&message[..])) + .result_bigint(); + let mut local_sig_vec = Vec::new(); + let mut s_vec = Vec::new(); + // each party computes s_i + for i in 0..ttag { + let local_sig = LocalSignature::phase7_local_sig( + &sign_keys_vec[i].k_i, + &message_bn, + &R_vec[i], + &sigma_vec[i], + &y, + ); + s_vec.push(local_sig.s_i.clone()); + local_sig_vec.push(local_sig); + } + + // test corrupted local s + if corrupt_step == 7 { + for i in 0..s_vec.len() { + if corrupted_parties.iter().any(|&x| x == i) { + s_vec[i] = &s_vec[i] + &s_vec[i]; + } + } + } + + let sig = local_sig_vec[0].output_signature(&s_vec[1..]); + + // test + assert_eq!(local_sig_vec[0].y, y); + //error in phase 7: + if sig.is_err() { + let global_state = GlobalStatePhase7 { + s_vec, + r: local_sig_vec[0].r.clone(), + R_dash_vec, + m: local_sig_vec[0].m.clone(), + R: local_sig_vec[0].R.clone(), + S_vec, + }; + global_state.phase7_blame()?; + } + //for testing purposes: checking with a second verifier: + + let sig = sig.unwrap(); + check_sig(&sig.r, &sig.s, &local_sig_vec[0].m, &y); + Ok(sig) +} + +fn check_sig(r: &Scalar, s: &Scalar, msg: &BigInt, pk: &Point) { + use secp256k1::{Message, PublicKey, Signature, SECP256K1}; + + let raw_msg = BigInt::to_bytes(msg); + let mut msg: Vec = Vec::new(); // padding + msg.extend(vec![0u8; 32 - raw_msg.len()]); + msg.extend(raw_msg.iter()); + + let msg = Message::from_slice(msg.as_slice()).unwrap(); + let slice = pk.to_bytes(false); + let mut raw_pk = Vec::new(); + if slice.len() != 65 { + // after curv's pk_to_key_slice return 65 bytes, this can be removed + raw_pk.insert(0, 4u8); + raw_pk.extend(vec![0u8; 64 - slice.len()]); + raw_pk.extend(slice.as_ref()); + } else { + raw_pk.extend(slice.as_ref()); + } + + assert_eq!(raw_pk.len(), 65); + + let pk = PublicKey::from_slice(&raw_pk).unwrap(); + + let mut compact: Vec = Vec::new(); + let bytes_r = &r.to_bytes()[..]; + compact.extend(vec![0u8; 32 - bytes_r.len()]); + compact.extend(bytes_r.iter()); + + let bytes_s = &s.to_bytes()[..]; + compact.extend(vec![0u8; 32 - bytes_s.len()]); + compact.extend(bytes_s.iter()); + + let secp_sig = Signature::from_compact(compact.as_slice()).unwrap(); + + let is_correct = SECP256K1.verify(&msg, &secp_sig, &pk).is_ok(); + assert!(is_correct); +} +#[test] +fn test_serialize_deserialize() { + use serde_json; + + let k = Keys::create(0); + let (commit, decommit) = k.phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2(); + + let encoded = serde_json::to_string(&commit).unwrap(); + let decoded: KeyGenBroadcastMessage1 = serde_json::from_str(&encoded).unwrap(); + assert_eq!(commit.com, decoded.com); + + let encoded = serde_json::to_string(&decommit).unwrap(); + let decoded: KeyGenDecommitMessage1 = serde_json::from_str(&encoded).unwrap(); + assert_eq!(decommit.y_i, decoded.y_i); +} +#[test] +fn test_small_paillier() { + // parties shouldn't be able to choose small Paillier modulus + let mut k = Keys::create(0); + // creating 2046-bit Paillier + let (ek, dk) = Paillier::keypair_with_modulus_size(2046).keys(); + k.dk = dk; + k.ek = ek; + let (commit, decommit) = k.phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2(); + assert!(k + .phase1_verify_com_phase3_verify_correct_key_verify_dlog_phase2_distribute( + &Parameters { + threshold: 0, + share_count: 1 + }, + &[decommit], + &[commit], + ) + .is_err()); +} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs new file mode 100644 index 00000000..43ded3aa --- /dev/null +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs @@ -0,0 +1,18 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +pub mod gg_2018; +pub mod gg_2020; diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/mod.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/mod.rs new file mode 100644 index 00000000..001cd979 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/mod.rs @@ -0,0 +1,23 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +const SECURITY_BITS: usize = 256; + +pub mod party_one; +pub mod party_two; + +#[cfg(test)] +mod test; diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_one.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_one.rs new file mode 100644 index 00000000..7ee1d573 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_one.rs @@ -0,0 +1,419 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ +use std::cmp; + +use class_group::primitives::cl_dl_public_setup::{ + decrypt, verifiably_encrypt, CLDLProof, CLGroup, Ciphertext as CLCiphertext, PK, SK, +}; + +use curv::arithmetic::*; +use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; +use curv::cryptographic_primitives::commitments::traits::Commitment; +use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; +use curv::cryptographic_primitives::proofs::sigma_dlog::*; +use curv::cryptographic_primitives::proofs::sigma_ec_ddh::*; +use curv::cryptographic_primitives::proofs::ProofError; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use subtle::ConstantTimeEq; + +use super::party_two::EphKeyGenFirstMsg as Party2EphKeyGenFirstMessage; +use super::party_two::EphKeyGenSecondMsg as Party2EphKeyGenSecondMessage; +use super::SECURITY_BITS; +use crate::Error::{self, InvalidSig}; + +//****************** Begin: Party One structs ******************// +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EcKeyPair { + pub public_share: Point, + secret_share: Scalar, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CommWitness { + pub pk_commitment_blind_factor: BigInt, + pub zk_pok_blind_factor: BigInt, + pub public_share: Point, + pub d_log_proof: DLogProof, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyGenFirstMsg { + pub pk_commitment: BigInt, + pub zk_pok_commitment: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeyGenSecondMsg { + pub comm_witness: CommWitness, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HSMCL { + pub public: PK, + pub secret: SK, + pub encrypted_share: CLCiphertext, + pub cl_group: CLGroup, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HSMCLPublic { + pub cl_pub_key: PK, + pub proof: CLDLProof, + pub encrypted_share: CLCiphertext, + pub cl_group: CLGroup, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SignatureRecid { + pub s: BigInt, + pub r: BigInt, + pub recid: u8, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Signature { + pub s: BigInt, + pub r: BigInt, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Party1Private { + x1: Scalar, + hsmcl_pub: PK, + hsmcl_priv: SK, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLFirstMessage { + pub c_hat: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLdecommit { + pub q_hat: Point, + pub blindness: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLSecondMessage { + pub decommit: PDLdecommit, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EphEcKeyPair { + pub public_share: Point, + secret_share: Scalar, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EphKeyGenFirstMsg { + pub d_log_proof: ECDDHProof, + pub public_share: Point, + pub c: Point, //c = secret_share * base_point2 +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EphKeyGenSecondMsg {} + +//****************** End: Party One structs ******************// + +impl KeyGenFirstMsg { + pub fn create_commitments() -> (KeyGenFirstMsg, CommWitness, EcKeyPair) { + let base = Point::generator(); + + let secret_share = Scalar::::random(); + //in Lindell's protocol range proof works only for x1 = + Scalar::::from(&secret_share.to_bigint().div_floor(&BigInt::from(3))); + + let public_share = base * &secret_share; + + let d_log_proof = DLogProof::prove(&secret_share); + // we use hash based commitment + let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); + let pk_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), + &pk_commitment_blind_factor, + ); + + let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); + let zk_pok_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(d_log_proof.pk_t_rand_commitment.to_bytes(true).as_ref()), + &zk_pok_blind_factor, + ); + let ec_key_pair = EcKeyPair { + public_share, + secret_share, + }; + ( + KeyGenFirstMsg { + pk_commitment, + zk_pok_commitment, + }, + CommWitness { + pk_commitment_blind_factor, + zk_pok_blind_factor, + public_share: ec_key_pair.public_share.clone(), + d_log_proof, + }, + ec_key_pair, + ) + } + + pub fn create_commitments_with_fixed_secret_share( + secret_share: Scalar, + ) -> (KeyGenFirstMsg, CommWitness, EcKeyPair) { + let base = Point::generator(); + let public_share = base * &secret_share; + + let d_log_proof = DLogProof::prove(&secret_share); + + let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); + let pk_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), + &pk_commitment_blind_factor, + ); + + let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); + let zk_pok_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(d_log_proof.pk_t_rand_commitment.to_bytes(true).as_ref()), + &zk_pok_blind_factor, + ); + + let ec_key_pair = EcKeyPair { + public_share, + secret_share, + }; + ( + KeyGenFirstMsg { + pk_commitment, + zk_pok_commitment, + }, + CommWitness { + pk_commitment_blind_factor, + zk_pok_blind_factor, + public_share: ec_key_pair.public_share.clone(), + d_log_proof, + }, + ec_key_pair, + ) + } +} + +impl KeyGenSecondMsg { + pub fn verify_and_decommit( + comm_witness: CommWitness, + proof: &DLogProof, + ) -> Result { + DLogProof::verify(proof)?; + Ok(KeyGenSecondMsg { comm_witness }) + } +} + +pub fn compute_pubkey( + party_one_private: &Party1Private, + other_share_public_share: &Point, +) -> Point { + other_share_public_share * &party_one_private.x1 +} + +impl Party1Private { + pub fn set_private_key(ec_key: &EcKeyPair, hsmcl: &HSMCL) -> Party1Private { + Party1Private { + x1: ec_key.secret_share.clone(), + hsmcl_pub: hsmcl.public.clone(), + hsmcl_priv: hsmcl.secret.clone(), + } + } +} + +impl HSMCL { + pub fn generate_keypair_and_encrypted_share_and_proof( + keygen: &EcKeyPair, + seed: &BigInt, + ) -> (HSMCL, HSMCLPublic) { + let cl_group = CLGroup::new_from_setup(&1348, seed); + let (secret_key, public_key) = cl_group.keygen(); + let (ciphertext, proof) = verifiably_encrypt( + &cl_group, + &public_key, + (&keygen.secret_share, &keygen.public_share), + ); + + ( + HSMCL { + cl_group: cl_group.clone(), + public: public_key.clone(), + secret: secret_key, + encrypted_share: ciphertext.clone(), + }, + HSMCLPublic { + cl_pub_key: public_key, + proof, + encrypted_share: ciphertext, + cl_group, + }, + ) + } +} + +impl EphKeyGenFirstMsg { + pub fn create() -> (EphKeyGenFirstMsg, EphEcKeyPair) { + let base = Point::generator(); + let secret_share = Scalar::::random(); + let public_share = base * &secret_share; + let h = Point::::base_point2(); + let w = ECDDHWitness { + x: secret_share.clone(), + }; + let c = h * &secret_share; + let delta = ECDDHStatement { + g1: base.to_point(), + h1: public_share.clone(), + g2: h.clone(), + h2: c.clone(), + }; + let d_log_proof = ECDDHProof::prove(&w, &delta); + let ec_key_pair = EphEcKeyPair { + public_share: public_share.clone(), + secret_share, + }; + ( + EphKeyGenFirstMsg { + d_log_proof, + public_share, + c, + }, + ec_key_pair, + ) + } +} + +impl EphKeyGenSecondMsg { + pub fn verify_commitments_and_dlog_proof( + party_two_first_message: &Party2EphKeyGenFirstMessage, + party_two_second_message: &Party2EphKeyGenSecondMessage, + ) -> Result { + let party_two_pk_commitment = &party_two_first_message.pk_commitment; + let party_two_zk_pok_commitment = &party_two_first_message.zk_pok_commitment; + let party_two_zk_pok_blind_factor = + &party_two_second_message.comm_witness.zk_pok_blind_factor; + let party_two_public_share = &party_two_second_message.comm_witness.public_share; + let party_two_pk_commitment_blind_factor = &party_two_second_message + .comm_witness + .pk_commitment_blind_factor; + let party_two_d_log_proof = &party_two_second_message.comm_witness.d_log_proof; + let mut flag = true; + if party_two_pk_commitment + != &HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(party_two_public_share.to_bytes(true).as_ref()), + party_two_pk_commitment_blind_factor, + ) + { + flag = false + } + if party_two_zk_pok_commitment + != &HashCommitment::::create_commitment_with_user_defined_randomness( + &Sha256::new() + .chain_points([&party_two_d_log_proof.a1, &party_two_d_log_proof.a2]) + .result_bigint(), + party_two_zk_pok_blind_factor, + ) + { + flag = false + } + if !flag { + return Err(ProofError); + } + let delta = ECDDHStatement { + g1: Point::generator().to_point(), + h1: party_two_public_share.clone(), + g2: Point::::base_point2().clone(), + h2: party_two_second_message.comm_witness.c.clone(), + }; + party_two_d_log_proof.verify(&delta)?; + Ok(EphKeyGenSecondMsg {}) + } +} + +impl Signature { + pub fn compute( + hsmcl: &HSMCL, + party_one_private: &Party1Private, + partial_sig_c3: CLCiphertext, + ephemeral_local_share: &EphEcKeyPair, + ephemeral_other_public_share: &Point, + ) -> Signature { + //compute r = k2* R1 + let r = ephemeral_other_public_share * &ephemeral_local_share.secret_share; + + let rx = r + .x_coord() + .unwrap() + .mod_floor(Scalar::::group_order()); + let k1 = &ephemeral_local_share.secret_share.to_bigint(); + let k1_inv = BigInt::mod_inv(k1, Scalar::::group_order()).unwrap(); + let s_tag = decrypt( + &hsmcl.cl_group, + &party_one_private.hsmcl_priv, + &partial_sig_c3, + ); + let s_tag_tag = BigInt::mod_mul( + &k1_inv, + &s_tag.to_bigint(), + Scalar::::group_order(), + ); + let s = cmp::min( + s_tag_tag.clone(), + Scalar::::group_order().clone() - s_tag_tag, + ); + Signature { s, r: rx } + } +} + +pub fn verify( + signature: &Signature, + pubkey: &Point, + message: &BigInt, +) -> Result<(), Error> { + let s_fe = Scalar::::from(&signature.s); + let rx_fe = Scalar::::from(&signature.r); + + let s_inv_fe = s_fe.invert().ok_or(Error::InvalidSig)?; + let e_fe: Scalar = + Scalar::::from(&message.mod_floor(Scalar::::group_order())); + let u1 = Point::generator() * e_fe * &s_inv_fe; + let u2 = pubkey * rx_fe * &s_inv_fe; + + // second condition is against malleability + let rx_bytes = &BigInt::to_bytes(&signature.r)[..]; + let u1_plus_u2_bytes = &BigInt::to_bytes(&(u1 + u2).x_coord().unwrap())[..]; + + if rx_bytes.ct_eq(u1_plus_u2_bytes).unwrap_u8() == 1 + && signature.s < Scalar::::group_order() - signature.s.clone() + { + Ok(()) + } else { + Err(InvalidSig) + } +} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_two.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_two.rs new file mode 100644 index 00000000..55f2aeef --- /dev/null +++ b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_two.rs @@ -0,0 +1,356 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ +use super::party_one::HSMCLPublic; +use class_group::primitives::cl_dl_public_setup::PK as HSMCLPK; +use class_group::primitives::cl_dl_public_setup::{ + encrypt, eval_scal, eval_sum, CLGroup, Ciphertext as CLCiphertext, +}; +use curv::arithmetic::traits::*; +use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; +use curv::cryptographic_primitives::commitments::traits::Commitment; +use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; +use curv::cryptographic_primitives::proofs::sigma_dlog::*; +use curv::cryptographic_primitives::proofs::sigma_ec_ddh::*; +use curv::cryptographic_primitives::proofs::ProofError; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; + +use super::party_one::EphKeyGenFirstMsg as Party1EphKeyGenFirstMsg; +use super::party_one::KeyGenFirstMsg as Party1KeyGenFirstMessage; +use super::party_one::KeyGenSecondMsg as Party1KeyGenSecondMessage; +use super::SECURITY_BITS; + +//****************** Begin: Party Two structs ******************// + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EcKeyPair { + pub public_share: Point, + secret_share: Scalar, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyGenFirstMsg { + pub d_log_proof: DLogProof, + pub public_share: Point, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeyGenSecondMsg {} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Party2Public { + pub group: CLGroup, + pub ek: HSMCLPK, + pub encrypted_secret_share: CLCiphertext, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PartialSig { + pub c3: CLCiphertext, +} + +#[derive(Serialize, Deserialize)] +pub struct Party2Private { + x2: Scalar, +} +#[derive(Debug)] +pub struct PDLchallenge { + pub c_tag: BigInt, + pub c_tag_tag: BigInt, + a: BigInt, + b: BigInt, + blindness: BigInt, + q_tag: Point, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLFirstMessage { + pub c_tag: BigInt, + pub c_tag_tag: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLdecommit { + pub a: BigInt, + pub b: BigInt, + pub blindness: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLSecondMessage { + pub decommit: PDLdecommit, +} +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EphEcKeyPair { + pub public_share: Point, + secret_share: Scalar, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EphCommWitness { + pub pk_commitment_blind_factor: BigInt, + pub zk_pok_blind_factor: BigInt, + pub public_share: Point, + pub d_log_proof: ECDDHProof, + pub c: Point, //c = secret_share * base_point2 +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EphKeyGenFirstMsg { + pub pk_commitment: BigInt, + pub zk_pok_commitment: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EphKeyGenSecondMsg { + pub comm_witness: EphCommWitness, +} + +//****************** End: Party Two structs ******************// + +impl KeyGenFirstMsg { + pub fn create() -> (KeyGenFirstMsg, EcKeyPair) { + let base = Point::generator(); + let secret_share = Scalar::::random(); + let public_share = base * &secret_share; + let d_log_proof = DLogProof::prove(&secret_share); + let ec_key_pair = EcKeyPair { + public_share: public_share.clone(), + secret_share, + }; + ( + KeyGenFirstMsg { + d_log_proof, + public_share, + }, + ec_key_pair, + ) + } + + pub fn create_with_fixed_secret_share( + secret_share: Scalar, + ) -> (KeyGenFirstMsg, EcKeyPair) { + let base = Point::generator(); + let public_share = base * &secret_share; + let d_log_proof = DLogProof::prove(&secret_share); + let ec_key_pair = EcKeyPair { + public_share: public_share.clone(), + secret_share, + }; + ( + KeyGenFirstMsg { + d_log_proof, + public_share, + }, + ec_key_pair, + ) + } +} + +impl KeyGenSecondMsg { + pub fn verify_commitments_and_dlog_proof( + party_one_first_message: &Party1KeyGenFirstMessage, + party_one_second_message: &Party1KeyGenSecondMessage, + ) -> Result { + let party_one_pk_commitment = &party_one_first_message.pk_commitment; + let party_one_zk_pok_commitment = &party_one_first_message.zk_pok_commitment; + let party_one_zk_pok_blind_factor = + &party_one_second_message.comm_witness.zk_pok_blind_factor; + let party_one_public_share = &party_one_second_message.comm_witness.public_share; + let party_one_pk_commitment_blind_factor = &party_one_second_message + .comm_witness + .pk_commitment_blind_factor; + let party_one_d_log_proof = &party_one_second_message.comm_witness.d_log_proof; + + let mut flag = true; + if party_one_pk_commitment + != &HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(party_one_public_share.to_bytes(true).as_ref()), + party_one_pk_commitment_blind_factor, + ) + { + flag = false + } + if party_one_zk_pok_commitment + != &HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes( + party_one_d_log_proof + .pk_t_rand_commitment + .to_bytes(true) + .as_ref(), + ), + party_one_zk_pok_blind_factor, + ) + { + flag = false + } + + if !flag { + return Err(ProofError); + } + + DLogProof::verify(party_one_d_log_proof)?; + Ok(KeyGenSecondMsg {}) + } +} + +pub fn compute_pubkey( + local_share: &EcKeyPair, + other_share_public_share: &Point, +) -> Point { + other_share_public_share * &local_share.secret_share +} + +impl Party2Private { + pub fn set_private_key(ec_key: &EcKeyPair) -> Party2Private { + Party2Private { + x2: ec_key.secret_share.clone(), + } + } +} + +impl Party2Public { + pub fn verify_setup_and_zkcldl_proof( + hsmcl_public: &HSMCLPublic, + seed: &BigInt, + party1_ec_pubkey: &Point, + ) -> Result { + let setup_verify = hsmcl_public.cl_group.setup_verify(seed); + + let proof_verify = hsmcl_public.proof.verify( + &hsmcl_public.cl_group, + &hsmcl_public.cl_pub_key, + &hsmcl_public.encrypted_share, + party1_ec_pubkey, + ); + if proof_verify.is_ok() && setup_verify.is_ok() { + Ok(Party2Public { + group: hsmcl_public.cl_group.clone(), + ek: hsmcl_public.cl_pub_key.clone(), + encrypted_secret_share: hsmcl_public.encrypted_share.clone(), + }) + } else { + Err(ProofError) + } + } +} + +impl EphKeyGenFirstMsg { + pub fn create_commitments() -> (EphKeyGenFirstMsg, EphCommWitness, EphEcKeyPair) { + let base = Point::generator(); + + let secret_share = Scalar::::random(); + + let public_share = base * &secret_share; + + let h = Point::base_point2(); + let w = ECDDHWitness { + x: secret_share.clone(), + }; + let c = h * &secret_share; + let delta = ECDDHStatement { + g1: base.to_point(), + h1: public_share.clone(), + g2: h.clone(), + h2: c.clone(), + }; + let d_log_proof = ECDDHProof::prove(&w, &delta); + + // we use hash based commitment + let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); + let pk_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), + &pk_commitment_blind_factor, + ); + + let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); + let zk_pok_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &Sha256::new() + .chain_points([&d_log_proof.a1, &d_log_proof.a2]) + .result_bigint(), + &zk_pok_blind_factor, + ); + + let ec_key_pair = EphEcKeyPair { + public_share, + secret_share, + }; + ( + EphKeyGenFirstMsg { + pk_commitment, + zk_pok_commitment, + }, + EphCommWitness { + pk_commitment_blind_factor, + zk_pok_blind_factor, + public_share: ec_key_pair.public_share.clone(), + d_log_proof, + c, + }, + ec_key_pair, + ) + } +} + +impl EphKeyGenSecondMsg { + pub fn verify_and_decommit( + comm_witness: EphCommWitness, + party_one_first_message: &Party1EphKeyGenFirstMsg, + ) -> Result { + let delta = ECDDHStatement { + g1: Point::generator().to_point(), + h1: party_one_first_message.public_share.clone(), + g2: Point::::base_point2().clone(), + h2: party_one_first_message.c.clone(), + }; + party_one_first_message.d_log_proof.verify(&delta)?; + Ok(EphKeyGenSecondMsg { comm_witness }) + } +} + +impl PartialSig { + pub fn compute( + party_two_public: Party2Public, + local_share: &Party2Private, + ephemeral_local_share: &EphEcKeyPair, + ephemeral_other_public_share: &Point, + message: &BigInt, + ) -> PartialSig { + let q = Scalar::::group_order(); + //compute r = k2* R1 + let r: Point = + ephemeral_other_public_share * &ephemeral_local_share.secret_share; + + let rx = r.x_coord().unwrap().mod_floor(q); + let k2 = &ephemeral_local_share.secret_share.to_bigint(); + let k2_inv = BigInt::mod_inv(k2, q).unwrap(); + let k2_inv_m = BigInt::mod_mul(&k2_inv, message, q); + let k2_inv_m_fe = Scalar::::from(&k2_inv_m); + let c1 = encrypt(&party_two_public.group, &party_two_public.ek, &k2_inv_m_fe); + let v = BigInt::mod_mul(&k2_inv, &local_share.x2.to_bigint(), q); + let v = BigInt::mod_mul(&v, &rx, q); + + let c2 = eval_scal(&party_two_public.encrypted_secret_share, &v); + let c3 = eval_sum(&c1.0, &c2); + + //c3: + PartialSig { c3 } + } +} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/test.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/test.rs new file mode 100644 index 00000000..87aad0d0 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/test.rs @@ -0,0 +1,142 @@ +// For integration tests, please add your tests in /tests instead + +use super::*; +use curv::arithmetic::Converter; +use curv::elliptic::curves::*; +use curv::BigInt; + +#[test] +fn test_d_log_proof_party_two_party_one() { + let (party_one_first_message, comm_witness, _ec_key_pair_party1) = + party_one::KeyGenFirstMsg::create_commitments(); + let (party_two_first_message, _ec_key_pair_party2) = party_two::KeyGenFirstMsg::create(); + let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( + comm_witness, + &party_two_first_message.d_log_proof, + ) + .expect("failed to verify and decommit"); + + let _party_two_second_message = party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( + &party_one_first_message, + &party_one_second_message, + ) + .expect("failed to verify commitments and DLog proof"); +} + +#[test] +fn test_full_key_gen() { + let (party_one_first_message, comm_witness, ec_key_pair_party1) = + party_one::KeyGenFirstMsg::create_commitments_with_fixed_secret_share( + Scalar::::random(), + ); + let (party_two_first_message, _ec_key_pair_party2) = + party_two::KeyGenFirstMsg::create_with_fixed_secret_share(Scalar::::from( + &BigInt::from(10), + )); + let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( + comm_witness, + &party_two_first_message.d_log_proof, + ) + .expect("failed to verify and decommit"); + + let _party_two_second_message = party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( + &party_one_first_message, + &party_one_second_message, + ) + .expect("failed to verify commitments and DLog proof"); + + // init HSMCL keypair: + let seed: BigInt = BigInt::from_str_radix( + "314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848", + 10, + ).unwrap(); + let (hsmcl, hsmcl_public) = party_one::HSMCL::generate_keypair_and_encrypted_share_and_proof( + &ec_key_pair_party1, + &seed, + ); + + //P1 sends P2 hsmcl_public + let _party_one_private = party_one::Party1Private::set_private_key(&ec_key_pair_party1, &hsmcl); + + let _party_two_hsmcl_pub = party_two::Party2Public::verify_setup_and_zkcldl_proof( + &hsmcl_public, + &seed, + &party_one_second_message.comm_witness.public_share, + ) + .expect("proof error"); +} + +#[test] +fn test_two_party_sign() { + ////////// Simulate KeyGen ///////////////// + // assume party1 and party2 engaged with KeyGen in the past resulting in + // party1 owning private share and HSMCL key-pair + // party2 owning private share and HSMCL encryption of party1 share + let (_party_one_private_share_gen, comm_witness, ec_key_pair_party1) = + party_one::KeyGenFirstMsg::create_commitments(); + let (party_two_private_share_gen, ec_key_pair_party2) = party_two::KeyGenFirstMsg::create(); + + //pi (nothing up my sleeve) + let seed: BigInt = BigInt::from_str_radix( + "314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848", + 10, + ).unwrap(); + + let (party_one_hsmcl, hsmcl_public) = + party_one::HSMCL::generate_keypair_and_encrypted_share_and_proof( + &ec_key_pair_party1, + &seed, + ); + + let party1_private = + party_one::Party1Private::set_private_key(&ec_key_pair_party1, &party_one_hsmcl); + + let party_two_hsmcl_pub = party_two::Party2Public::verify_setup_and_zkcldl_proof( + &hsmcl_public, + &seed, + &comm_witness.public_share, + ) + .expect("proof error"); + + ////////// Start Signing ///////////////// + // creating the ephemeral private shares: + + let (eph_party_two_first_message, eph_comm_witness, eph_ec_key_pair_party2) = + party_two::EphKeyGenFirstMsg::create_commitments(); + let (eph_party_one_first_message, eph_ec_key_pair_party1) = + party_one::EphKeyGenFirstMsg::create(); + let eph_party_two_second_message = party_two::EphKeyGenSecondMsg::verify_and_decommit( + eph_comm_witness, + &eph_party_one_first_message, + ) + .expect("party1 DLog proof failed"); + + let _eph_party_one_second_message = + party_one::EphKeyGenSecondMsg::verify_commitments_and_dlog_proof( + &eph_party_two_first_message, + &eph_party_two_second_message, + ) + .expect("failed to verify commitments and DLog proof"); + let party2_private = party_two::Party2Private::set_private_key(&ec_key_pair_party2); + let message = BigInt::from(1234); + + let partial_sig = party_two::PartialSig::compute( + party_two_hsmcl_pub, + &party2_private, + &eph_ec_key_pair_party2, + &eph_party_one_first_message.public_share, + &message, + ); + + let signature = party_one::Signature::compute( + &party_one_hsmcl, + &party1_private, + partial_sig.c3, + &eph_ec_key_pair_party1, + &eph_party_two_second_message.comm_witness.public_share, + ); + + let pubkey = + party_one::compute_pubkey(&party1_private, &party_two_private_share_gen.public_share); + party_one::verify(&signature, &pubkey, &message).expect("Invalid signature") +} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/mod.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/mod.rs new file mode 100644 index 00000000..001cd979 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/mod.rs @@ -0,0 +1,23 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +const SECURITY_BITS: usize = 256; + +pub mod party_one; +pub mod party_two; + +#[cfg(test)] +mod test; diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs new file mode 100644 index 00000000..072d32f1 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs @@ -0,0 +1,607 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ +use std::cmp; + +use centipede::juggling::proof_system::{Helgamalsegmented, Witness}; +use centipede::juggling::segmentation::Msegmentation; +use curv::arithmetic::traits::*; +use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; +use curv::cryptographic_primitives::commitments::traits::Commitment; +use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; +use curv::cryptographic_primitives::proofs::sigma_dlog::*; +use curv::cryptographic_primitives::proofs::sigma_ec_ddh::*; +use curv::cryptographic_primitives::proofs::ProofError; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use paillier::Paillier; +use paillier::{Decrypt, EncryptWithChosenRandomness, KeyGeneration}; +use paillier::{DecryptionKey, EncryptionKey, Randomness, RawCiphertext, RawPlaintext}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use subtle::ConstantTimeEq; +use zk_paillier::zkproofs::NiCorrectKeyProof; + +use super::party_two::EphKeyGenFirstMsg as Party2EphKeyGenFirstMessage; +use super::party_two::EphKeyGenSecondMsg as Party2EphKeyGenSecondMessage; +use super::SECURITY_BITS; + +use crate::utilities::mta::MessageB; +use crate::Error; + +use crate::utilities::zk_pdl_with_slack::PDLwSlackProof; +use crate::utilities::zk_pdl_with_slack::PDLwSlackStatement; +use crate::utilities::zk_pdl_with_slack::PDLwSlackWitness; +use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; + +//****************** Begin: Party One structs ******************// +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EcKeyPair { + pub public_share: Point, + secret_share: Scalar, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CommWitness { + pub pk_commitment_blind_factor: BigInt, + pub zk_pok_blind_factor: BigInt, + pub public_share: Point, + pub d_log_proof: DLogProof, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyGenFirstMsg { + pub pk_commitment: BigInt, + pub zk_pok_commitment: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeyGenSecondMsg { + pub comm_witness: CommWitness, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PaillierKeyPair { + pub ek: EncryptionKey, + dk: DecryptionKey, + pub encrypted_share: BigInt, + randomness: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SignatureRecid { + pub s: BigInt, + pub r: BigInt, + pub recid: u8, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Signature { + pub s: BigInt, + pub r: BigInt, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Party1Private { + x1: Scalar, + paillier_priv: DecryptionKey, + c_key_randomness: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLFirstMessage { + pub c_hat: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLdecommit { + pub q_hat: Point, + pub blindness: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLSecondMessage { + pub decommit: PDLdecommit, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EphEcKeyPair { + pub public_share: Point, + secret_share: Scalar, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EphKeyGenFirstMsg { + pub d_log_proof: ECDDHProof, + pub public_share: Point, + pub c: Point, //c = secret_share * base_point2 +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EphKeyGenSecondMsg {} + +//****************** End: Party One structs ******************// + +impl KeyGenFirstMsg { + pub fn create_commitments() -> (KeyGenFirstMsg, CommWitness, EcKeyPair) { + let base = Point::generator(); + + let secret_share = Scalar::::random(); + + let public_share = base * &secret_share; + + let d_log_proof = DLogProof::::prove(&secret_share); + // we use hash based commitment + let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); + let pk_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), + &pk_commitment_blind_factor, + ); + + let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); + let zk_pok_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(d_log_proof.pk_t_rand_commitment.to_bytes(true).as_ref()), + &zk_pok_blind_factor, + ); + let ec_key_pair = EcKeyPair { + public_share, + secret_share, + }; + ( + KeyGenFirstMsg { + pk_commitment, + zk_pok_commitment, + }, + CommWitness { + pk_commitment_blind_factor, + zk_pok_blind_factor, + public_share: ec_key_pair.public_share.clone(), + d_log_proof, + }, + ec_key_pair, + ) + } + + pub fn create_commitments_with_fixed_secret_share( + secret_share: Scalar, + ) -> (KeyGenFirstMsg, CommWitness, EcKeyPair) { + let base = Point::generator(); + let public_share = base * &secret_share; + + let d_log_proof = DLogProof::::prove(&secret_share); + + let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); + let pk_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), + &pk_commitment_blind_factor, + ); + + let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); + let zk_pok_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(d_log_proof.pk_t_rand_commitment.to_bytes(true).as_ref()), + &zk_pok_blind_factor, + ); + + let ec_key_pair = EcKeyPair { + public_share, + secret_share, + }; + ( + KeyGenFirstMsg { + pk_commitment, + zk_pok_commitment, + }, + CommWitness { + pk_commitment_blind_factor, + zk_pok_blind_factor, + public_share: ec_key_pair.public_share.clone(), + d_log_proof, + }, + ec_key_pair, + ) + } +} + +impl KeyGenSecondMsg { + pub fn verify_and_decommit( + comm_witness: CommWitness, + proof: &DLogProof, + ) -> Result { + DLogProof::verify(proof)?; + Ok(KeyGenSecondMsg { comm_witness }) + } +} + +pub fn compute_pubkey( + party_one_private: &Party1Private, + other_share_public_share: &Point, +) -> Point { + other_share_public_share * &party_one_private.x1 +} + +impl Party1Private { + pub fn set_private_key(ec_key: &EcKeyPair, paillier_key: &PaillierKeyPair) -> Party1Private { + Party1Private { + x1: ec_key.secret_share.clone(), + paillier_priv: paillier_key.dk.clone(), + c_key_randomness: paillier_key.randomness.clone(), + } + } + pub fn refresh_private_key( + party_one_private: &Party1Private, + factor: &BigInt, + ) -> ( + EncryptionKey, + BigInt, + Party1Private, + NiCorrectKeyProof, + PDLwSlackStatement, + PDLwSlackProof, + CompositeDLogProof, + ) { + let (ek_new, dk_new) = Paillier::keypair().keys(); + let randomness = Randomness::sample(&ek_new); + let factor_fe = Scalar::::from(&*factor); + let x1_new = &party_one_private.x1 * factor_fe; + let c_key_new = Paillier::encrypt_with_chosen_randomness( + &ek_new, + RawPlaintext::from(x1_new.to_bigint()), + &randomness, + ) + .0 + .into_owned(); + let correct_key_proof_new = NiCorrectKeyProof::proof(&dk_new, None); + + let paillier_key_pair = PaillierKeyPair { + ek: ek_new.clone(), + dk: dk_new.clone(), + encrypted_share: c_key_new.clone(), + randomness: randomness.0.clone(), + }; + + let party_one_private_new = Party1Private { + x1: x1_new, + paillier_priv: dk_new, + c_key_randomness: randomness.0, + }; + + let (pdl_statement, pdl_proof, composite_dlog_proof) = + PaillierKeyPair::pdl_proof(&party_one_private_new, &paillier_key_pair); + + ( + ek_new, + c_key_new, + party_one_private_new, + correct_key_proof_new, + pdl_statement, + pdl_proof, + composite_dlog_proof, + ) + } + + // used for verifiable recovery + pub fn to_encrypted_segment( + &self, + segment_size: usize, + num_of_segments: usize, + pub_ke_y: &Point, + g: &Point, + ) -> (Witness, Helgamalsegmented) { + Msegmentation::to_encrypted_segments(&self.x1, &segment_size, num_of_segments, pub_ke_y, g) + } + + // used to transform lindell master key to gg18 master key + pub fn to_mta_message_b( + &self, + message_b: MessageB, + ) -> Result<(Scalar, BigInt), Error> { + message_b.verify_proofs_get_alpha(&self.paillier_priv, &self.x1) + } +} + +impl PaillierKeyPair { + pub fn generate_keypair_and_encrypted_share(keygen: &EcKeyPair) -> PaillierKeyPair { + let (ek, dk) = Paillier::keypair().keys(); + let randomness = Randomness::sample(&ek); + + let encrypted_share = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(keygen.secret_share.to_bigint()), + &randomness, + ) + .0 + .into_owned(); + + PaillierKeyPair { + ek, + dk, + encrypted_share, + randomness: randomness.0, + } + } + + pub fn generate_encrypted_share_from_fixed_paillier_keypair( + ek: &EncryptionKey, + dk: &DecryptionKey, + keygen: &EcKeyPair, + ) -> PaillierKeyPair { + let randomness = Randomness::sample(ek); + + let encrypted_share = Paillier::encrypt_with_chosen_randomness( + ek, + RawPlaintext::from(keygen.secret_share.to_bigint()), + &randomness, + ) + .0 + .into_owned(); + + PaillierKeyPair { + ek: ek.clone(), + dk: dk.clone(), + encrypted_share, + randomness: randomness.0, + } + } + + pub fn generate_ni_proof_correct_key(paillier_context: &PaillierKeyPair) -> NiCorrectKeyProof { + NiCorrectKeyProof::proof(&paillier_context.dk, None) + } + + pub fn pdl_proof( + party1_private: &Party1Private, + paillier_key_pair: &PaillierKeyPair, + ) -> (PDLwSlackStatement, PDLwSlackProof, CompositeDLogProof) { + let (n_tilde, h1, h2, xhi) = generate_h1_h2_n_tilde(); + let dlog_statement = DLogStatement { + N: n_tilde, + g: h1, + ni: h2, + }; + let composite_dlog_proof = CompositeDLogProof::prove(&dlog_statement, &xhi); + + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: paillier_key_pair.encrypted_share.clone(), + ek: paillier_key_pair.ek.clone(), + Q: Point::generator() * &party1_private.x1, + G: Point::generator().to_point(), + h1: dlog_statement.g.clone(), + h2: dlog_statement.ni.clone(), + N_tilde: dlog_statement.N, + }; + + let pdl_w_slack_witness = PDLwSlackWitness { + x: party1_private.x1.clone(), + r: party1_private.c_key_randomness.clone(), + }; + + let pdl_w_slack_proof = PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); + ( + pdl_w_slack_statement, + pdl_w_slack_proof, + composite_dlog_proof, + ) + } +} + +impl EphKeyGenFirstMsg { + pub fn create() -> (EphKeyGenFirstMsg, EphEcKeyPair) { + let base = Point::generator(); + let secret_share = Scalar::::random(); + let public_share = &*base * &secret_share; + let h = Point::::base_point2(); + + let c = h * &secret_share; + let w = ECDDHWitness { + x: secret_share.clone(), + }; + let delta = ECDDHStatement { + g1: base.to_point(), + h1: public_share.clone(), + g2: h.clone(), + h2: c.clone(), + }; + let d_log_proof = ECDDHProof::prove(&w, &delta); + let ec_key_pair = EphEcKeyPair { + public_share: public_share.clone(), + secret_share, + }; + ( + EphKeyGenFirstMsg { + d_log_proof, + public_share, + c, + }, + ec_key_pair, + ) + } +} + +impl EphKeyGenSecondMsg { + pub fn verify_commitments_and_dlog_proof( + party_two_first_message: &Party2EphKeyGenFirstMessage, + party_two_second_message: &Party2EphKeyGenSecondMessage, + ) -> Result { + let party_two_pk_commitment = &party_two_first_message.pk_commitment; + let party_two_zk_pok_commitment = &party_two_first_message.zk_pok_commitment; + let party_two_zk_pok_blind_factor = + &party_two_second_message.comm_witness.zk_pok_blind_factor; + let party_two_public_share = &party_two_second_message.comm_witness.public_share; + let party_two_pk_commitment_blind_factor = &party_two_second_message + .comm_witness + .pk_commitment_blind_factor; + let party_two_d_log_proof = &party_two_second_message.comm_witness.d_log_proof; + let mut flag = true; + if party_two_pk_commitment + != &HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(party_two_public_share.to_bytes(true).as_ref()), + party_two_pk_commitment_blind_factor, + ) + { + flag = false + } + if party_two_zk_pok_commitment + != &HashCommitment::::create_commitment_with_user_defined_randomness( + &Sha256::new() + .chain_points([&party_two_d_log_proof.a1, &party_two_d_log_proof.a2]) + .result_bigint(), + party_two_zk_pok_blind_factor, + ) + { + flag = false + } + + if !flag { + return Err(ProofError); + } + + let delta = ECDDHStatement { + g1: Point::generator().to_point(), + h1: party_two_public_share.clone(), + g2: Point::::base_point2().clone(), + h2: party_two_second_message.comm_witness.c.clone(), + }; + party_two_d_log_proof.verify(&delta)?; + Ok(EphKeyGenSecondMsg {}) + } +} + +impl Signature { + pub fn compute( + party_one_private: &Party1Private, + partial_sig_c3: &BigInt, + ephemeral_local_share: &EphEcKeyPair, + ephemeral_other_public_share: &Point, + ) -> Signature { + //compute r = k2* R1 + let r = ephemeral_other_public_share * &ephemeral_local_share.secret_share; + + let rx = r + .x_coord() + .unwrap() + .mod_floor(Scalar::::group_order()); + + let k1_inv = ephemeral_local_share.secret_share.invert().unwrap(); + + let s_tag = Paillier::decrypt( + &party_one_private.paillier_priv, + &RawCiphertext::from(partial_sig_c3), + ) + .0; + let s_tag_fe = Scalar::::from(s_tag.as_ref()); + let s_tag_tag = s_tag_fe * k1_inv; + let s_tag_tag_bn = s_tag_tag.to_bigint(); + + let s = cmp::min( + s_tag_tag_bn.clone(), + Scalar::::group_order().clone() - s_tag_tag_bn, + ); + + Signature { s, r: rx } + } + + pub fn compute_with_recid( + party_one_private: &Party1Private, + partial_sig_c3: &BigInt, + ephemeral_local_share: &EphEcKeyPair, + ephemeral_other_public_share: &Point, + ) -> SignatureRecid { + //compute r = k2* R1 + let r = ephemeral_other_public_share * &ephemeral_local_share.secret_share; + + let rx = r + .x_coord() + .unwrap() + .mod_floor(Scalar::::group_order()); + let ry = r + .y_coord() + .unwrap() + .mod_floor(Scalar::::group_order()); + let k1_inv = ephemeral_local_share.secret_share.invert().unwrap(); + + let s_tag = Paillier::decrypt( + &party_one_private.paillier_priv, + &RawCiphertext::from(partial_sig_c3), + ) + .0; + let s_tag_fe = Scalar::::from(s_tag.as_ref()); + let s_tag_tag = s_tag_fe * k1_inv; + let s_tag_tag_bn = s_tag_tag.to_bigint(); + let s = cmp::min( + s_tag_tag_bn.clone(), + Scalar::::group_order() - &s_tag_tag_bn, + ); + + /* + Calculate recovery id - it is not possible to compute the public key out of the signature + itself. Recovery id is used to enable extracting the public key uniquely. + 1. id = R.y & 1 + 2. if (s > curve.q / 2) id = id ^ 1 + */ + let is_ry_odd = ry.test_bit(0); + let mut recid = if is_ry_odd { 1 } else { 0 }; + if s_tag_tag_bn > Scalar::::group_order() - &s_tag_tag_bn { + recid ^= 1; + } + + SignatureRecid { s, r: rx, recid } + } +} + +pub fn verify( + signature: &Signature, + pubkey: &Point, + message: &BigInt, +) -> Result<(), Error> { + let s_fe = Scalar::::from(&signature.s); + let rx_fe = Scalar::::from(&signature.r); + + let s_inv_fe = s_fe.invert().unwrap(); + let e_fe: Scalar = + Scalar::::from(&message.mod_floor(Scalar::::group_order())); + let u1 = Point::generator() * e_fe * &s_inv_fe; + let u2 = &*pubkey * rx_fe * &s_inv_fe; + + // second condition is against malleability + let rx_bytes = &BigInt::to_bytes(&signature.r)[..]; + let u1_plus_u2_bytes = &BigInt::to_bytes(&(u1 + u2).x_coord().unwrap())[..]; + + if rx_bytes.ct_eq(u1_plus_u2_bytes).unwrap_u8() == 1 + && signature.s < Scalar::::group_order() - signature.s.clone() + { + Ok(()) + } else { + Err(Error::InvalidSig) + } +} + +pub fn generate_h1_h2_n_tilde() -> (BigInt, BigInt, BigInt, BigInt) { + //note, should be safe primes: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys();; + let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&phi); + let s = BigInt::from(2).pow(256_u32); + let xhi = BigInt::sample_below(&s); + let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); + let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); + + (ek_tilde.n, h1, h2, xhi) +} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_two.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_two.rs new file mode 100644 index 00000000..19d3e1f8 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_two.rs @@ -0,0 +1,426 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +use centipede::juggling::proof_system::{Helgamalsegmented, Witness}; +use centipede::juggling::segmentation::Msegmentation; +use curv::arithmetic::traits::*; +use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; +use curv::cryptographic_primitives::commitments::traits::Commitment; +use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; +use curv::cryptographic_primitives::proofs::sigma_dlog::*; +use curv::cryptographic_primitives::proofs::sigma_ec_ddh::*; +use curv::cryptographic_primitives::proofs::ProofError; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use paillier::Paillier; +use paillier::{Add, Encrypt, Mul}; +use paillier::{EncryptionKey, RawCiphertext, RawPlaintext}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use zk_paillier::zkproofs::{IncorrectProof, NiCorrectKeyProof}; + +use super::party_one::EphKeyGenFirstMsg as Party1EphKeyGenFirstMsg; +use super::party_one::KeyGenFirstMsg as Party1KeyGenFirstMessage; +use super::party_one::KeyGenSecondMsg as Party1KeyGenSecondMessage; +use super::SECURITY_BITS; +use crate::utilities::mta::{MessageA, MessageB}; + +use crate::utilities::zk_pdl_with_slack::PDLwSlackProof; +use crate::utilities::zk_pdl_with_slack::PDLwSlackStatement; +use thiserror::Error; +use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; + +#[derive(Error, Debug)] +pub enum PartyTwoError { + #[error("party two pdl verify failed (lindell 2017)")] + PdlVerify, +} + +const PAILLIER_KEY_SIZE: usize = 2048; +//****************** Begin: Party Two structs ******************// + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EcKeyPair { + pub public_share: Point, + secret_share: Scalar, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyGenFirstMsg { + pub d_log_proof: DLogProof, + pub public_share: Point, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeyGenSecondMsg {} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PaillierPublic { + pub ek: EncryptionKey, + pub encrypted_secret_share: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PartialSig { + pub c3: BigInt, +} + +#[derive(Serialize, Deserialize)] +pub struct Party2Private { + x2: Scalar, +} + +#[derive(Debug)] +#[allow(dead_code)] +pub struct PDLchallenge { + pub c_tag: BigInt, + pub c_tag_tag: BigInt, + a: BigInt, + b: BigInt, + blindness: BigInt, + q_tag: Point, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLFirstMessage { + pub c_tag: BigInt, + pub c_tag_tag: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLdecommit { + pub a: BigInt, + pub b: BigInt, + pub blindness: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PDLSecondMessage { + pub decommit: PDLdecommit, +} +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EphEcKeyPair { + pub public_share: Point, + secret_share: Scalar, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EphCommWitness { + pub pk_commitment_blind_factor: BigInt, + pub zk_pok_blind_factor: BigInt, + pub public_share: Point, + pub d_log_proof: ECDDHProof, + pub c: Point, //c = secret_share * base_point2 +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EphKeyGenFirstMsg { + pub pk_commitment: BigInt, + pub zk_pok_commitment: BigInt, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EphKeyGenSecondMsg { + pub comm_witness: EphCommWitness, +} + +//****************** End: Party Two structs ******************// + +impl KeyGenFirstMsg { + pub fn create() -> (KeyGenFirstMsg, EcKeyPair) { + let base = Point::generator(); + let secret_share = Scalar::::random(); + let public_share = base * &secret_share; + let d_log_proof = DLogProof::prove(&secret_share); + let ec_key_pair = EcKeyPair { + public_share: public_share.clone(), + secret_share, + }; + ( + KeyGenFirstMsg { + d_log_proof, + public_share, + }, + ec_key_pair, + ) + } + + pub fn create_with_fixed_secret_share( + secret_share: Scalar, + ) -> (KeyGenFirstMsg, EcKeyPair) { + let base = Point::generator(); + let public_share = base * &secret_share; + let d_log_proof = DLogProof::prove(&secret_share); + let ec_key_pair = EcKeyPair { + public_share: public_share.clone(), + secret_share, + }; + ( + KeyGenFirstMsg { + d_log_proof, + public_share, + }, + ec_key_pair, + ) + } +} + +impl KeyGenSecondMsg { + pub fn verify_commitments_and_dlog_proof( + party_one_first_message: &Party1KeyGenFirstMessage, + party_one_second_message: &Party1KeyGenSecondMessage, + ) -> Result { + let party_one_pk_commitment = &party_one_first_message.pk_commitment; + let party_one_zk_pok_commitment = &party_one_first_message.zk_pok_commitment; + let party_one_zk_pok_blind_factor = + &party_one_second_message.comm_witness.zk_pok_blind_factor; + let party_one_public_share = &party_one_second_message.comm_witness.public_share; + let party_one_pk_commitment_blind_factor = &party_one_second_message + .comm_witness + .pk_commitment_blind_factor; + let party_one_d_log_proof = &party_one_second_message.comm_witness.d_log_proof; + + let mut flag = true; + if party_one_pk_commitment + != &HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(party_one_public_share.to_bytes(true).as_ref()), + party_one_pk_commitment_blind_factor, + ) + { + flag = false + } + if party_one_zk_pok_commitment + != &HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes( + party_one_d_log_proof + .pk_t_rand_commitment + .to_bytes(true) + .as_ref(), + ), + party_one_zk_pok_blind_factor, + ) + { + flag = false + } + + if !flag { + return Err(ProofError); + } + + DLogProof::verify(party_one_d_log_proof)?; + Ok(KeyGenSecondMsg {}) + } +} + +pub fn compute_pubkey( + local_share: &EcKeyPair, + other_share_public_share: &Point, +) -> Point { + let pubkey = other_share_public_share; + pubkey * &local_share.secret_share +} + +impl Party2Private { + pub fn set_private_key(ec_key: &EcKeyPair) -> Party2Private { + Party2Private { + x2: ec_key.secret_share.clone(), + } + } + + pub fn update_private_key(party_two_private: &Party2Private, factor: &BigInt) -> Party2Private { + let factor_fe = Scalar::::from(factor); + Party2Private { + x2: &party_two_private.x2 * &factor_fe, + } + } + + // used for verifiable recovery + pub fn to_encrypted_segment( + &self, + segment_size: usize, + num_of_segments: usize, + pub_ke_y: &Point, + g: &Point, + ) -> (Witness, Helgamalsegmented) { + Msegmentation::to_encrypted_segments(&self.x2, &segment_size, num_of_segments, pub_ke_y, g) + } + + // used to transform lindell master key to gg18 master key + pub fn to_mta_message_b( + &self, + ek: &EncryptionKey, + ciphertext: &BigInt, + ) -> (MessageB, Scalar) { + let message_a = MessageA { + c: ciphertext.clone(), + range_proofs: vec![], + }; + let (a, b, _, _) = MessageB::b(&self.x2, ek, message_a, &[]).unwrap(); + (a, b) + } +} + +impl PaillierPublic { + pub fn pdl_verify( + composite_dlog_proof: &CompositeDLogProof, + pdl_w_slack_statement: &PDLwSlackStatement, + pdl_w_slack_proof: &PDLwSlackProof, + paillier_public: &PaillierPublic, + q1: &Point, + ) -> Result<(), PartyTwoError> { + if pdl_w_slack_statement.ek != paillier_public.ek + || pdl_w_slack_statement.ciphertext != paillier_public.encrypted_secret_share + || &pdl_w_slack_statement.Q != q1 + { + return Err(PartyTwoError::PdlVerify); + } + let dlog_statement = DLogStatement { + N: pdl_w_slack_statement.N_tilde.clone(), + g: pdl_w_slack_statement.h1.clone(), + ni: pdl_w_slack_statement.h2.clone(), + }; + if composite_dlog_proof.verify(&dlog_statement).is_ok() + && pdl_w_slack_proof.verify(pdl_w_slack_statement).is_ok() + { + Ok(()) + } else { + Err(PartyTwoError::PdlVerify) + } + } + + pub fn verify_ni_proof_correct_key( + proof: NiCorrectKeyProof, + ek: &EncryptionKey, + ) -> Result<(), IncorrectProof> { + // + if ek.n.bit_length() < PAILLIER_KEY_SIZE - 1 { + return Err(IncorrectProof); + }; + proof.verify(ek, zk_paillier::zkproofs::SALT_STRING) + } +} + +impl EphKeyGenFirstMsg { + pub fn create_commitments() -> (EphKeyGenFirstMsg, EphCommWitness, EphEcKeyPair) { + let base = Point::generator(); + + let secret_share = Scalar::::random(); + + let public_share = base * &secret_share; + + let h = Point::::base_point2(); + + let c = h * &secret_share; + let w = ECDDHWitness { + x: secret_share.clone(), + }; + let delta = ECDDHStatement { + g1: base.to_point(), + h1: public_share.clone(), + g2: h.clone(), + h2: c.clone(), + }; + let d_log_proof = ECDDHProof::prove(&w, &delta); + + // we use hash based commitment + let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); + let pk_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), + &pk_commitment_blind_factor, + ); + + let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); + let zk_pok_commitment = + HashCommitment::::create_commitment_with_user_defined_randomness( + &Sha256::new() + .chain_points([&d_log_proof.a1, &d_log_proof.a2]) + .result_bigint(), + &zk_pok_blind_factor, + ); + + let ec_key_pair = EphEcKeyPair { + public_share, + secret_share, + }; + ( + EphKeyGenFirstMsg { + pk_commitment, + zk_pok_commitment, + }, + EphCommWitness { + pk_commitment_blind_factor, + zk_pok_blind_factor, + public_share: ec_key_pair.public_share.clone(), + d_log_proof, + c, + }, + ec_key_pair, + ) + } +} + +impl EphKeyGenSecondMsg { + pub fn verify_and_decommit( + comm_witness: EphCommWitness, + party_one_first_message: &Party1EphKeyGenFirstMsg, + ) -> Result { + let delta = ECDDHStatement { + g1: Point::generator().to_point(), + h1: party_one_first_message.public_share.clone(), + g2: Point::::base_point2().clone(), + h2: party_one_first_message.c.clone(), + }; + party_one_first_message.d_log_proof.verify(&delta)?; + Ok(EphKeyGenSecondMsg { comm_witness }) + } +} + +impl PartialSig { + pub fn compute( + ek: &EncryptionKey, + encrypted_secret_share: &BigInt, + local_share: &Party2Private, + ephemeral_local_share: &EphEcKeyPair, + ephemeral_other_public_share: &Point, + message: &BigInt, + ) -> PartialSig { + let q = Scalar::::group_order(); + //compute r = k2* R1 + let r = ephemeral_other_public_share * &ephemeral_local_share.secret_share; + + let rx = r.x_coord().unwrap().mod_floor(q); + let rho = BigInt::sample_below(&q.pow(2)); + let k2_inv = BigInt::mod_inv(&ephemeral_local_share.secret_share.to_bigint(), q).unwrap(); + let partial_sig = rho * q + BigInt::mod_mul(&k2_inv, message, q); + + let c1 = Paillier::encrypt(ek, RawPlaintext::from(partial_sig)); + let v = BigInt::mod_mul( + &k2_inv, + &BigInt::mod_mul(&rx, &local_share.x2.to_bigint(), q), + q, + ); + let c2 = Paillier::mul( + ek, + RawCiphertext::from(encrypted_secret_share.clone()), + RawPlaintext::from(v), + ); + //c3: + PartialSig { + c3: Paillier::add(ek, c2, c1).0.into_owned(), + } + } +} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/test.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/test.rs new file mode 100644 index 00000000..60c142f6 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/test.rs @@ -0,0 +1,137 @@ +// For integration tests, please add your tests in /tests instead + +use crate::protocols::two_party_ecdsa::lindell_2017::{party_one, party_two}; +use curv::arithmetic::traits::Samplable; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Scalar}; +use curv::BigInt; + +#[test] +fn test_d_log_proof_party_two_party_one() { + let (party_one_first_message, comm_witness, _ec_key_pair_party1) = + party_one::KeyGenFirstMsg::create_commitments(); + let (party_two_first_message, _ec_key_pair_party2) = party_two::KeyGenFirstMsg::create(); + let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( + comm_witness, + &party_two_first_message.d_log_proof, + ) + .expect("failed to verify and decommit"); + + let _party_two_second_message = party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( + &party_one_first_message, + &party_one_second_message, + ) + .expect("failed to verify commitments and DLog proof"); +} + +#[test] + +fn test_full_key_gen() { + let (party_one_first_message, comm_witness, ec_key_pair_party1) = + party_one::KeyGenFirstMsg::create_commitments_with_fixed_secret_share( + Scalar::::from(&BigInt::sample(253)), + ); + let (party_two_first_message, _ec_key_pair_party2) = + party_two::KeyGenFirstMsg::create_with_fixed_secret_share(Scalar::::from( + &BigInt::from(10), + )); + let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( + comm_witness, + &party_two_first_message.d_log_proof, + ) + .expect("failed to verify and decommit"); + + let _party_two_second_message = party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( + &party_one_first_message, + &party_one_second_message, + ) + .expect("failed to verify commitments and DLog proof"); + + // init paillier keypair: + let paillier_key_pair = + party_one::PaillierKeyPair::generate_keypair_and_encrypted_share(&ec_key_pair_party1); + + let party_one_private = + party_one::Party1Private::set_private_key(&ec_key_pair_party1, &paillier_key_pair); + + let party_two_paillier = party_two::PaillierPublic { + ek: paillier_key_pair.ek.clone(), + encrypted_secret_share: paillier_key_pair.encrypted_share.clone(), + }; + + // zk proof of correct paillier key + let correct_key_proof = + party_one::PaillierKeyPair::generate_ni_proof_correct_key(&paillier_key_pair); + party_two::PaillierPublic::verify_ni_proof_correct_key( + correct_key_proof, + &party_two_paillier.ek, + ) + .expect("bad paillier key"); + + //zk_pdl + + let (pdl_statement, pdl_proof, composite_dlog_proof) = + party_one::PaillierKeyPair::pdl_proof(&party_one_private, &paillier_key_pair); + party_two::PaillierPublic::pdl_verify( + &composite_dlog_proof, + &pdl_statement, + &pdl_proof, + &party_two_paillier, + &party_one_second_message.comm_witness.public_share, + ) + .expect("PDL error"); +} + +#[test] +fn test_two_party_sign() { + // assume party1 and party2 engaged with KeyGen in the past resulting in + // party1 owning private share and paillier key-pair + // party2 owning private share and paillier encryption of party1 share + let (_party_one_private_share_gen, _comm_witness, ec_key_pair_party1) = + party_one::KeyGenFirstMsg::create_commitments(); + let (party_two_private_share_gen, ec_key_pair_party2) = party_two::KeyGenFirstMsg::create(); + + let keypair = + party_one::PaillierKeyPair::generate_keypair_and_encrypted_share(&ec_key_pair_party1); + + // creating the ephemeral private shares: + + let (eph_party_two_first_message, eph_comm_witness, eph_ec_key_pair_party2) = + party_two::EphKeyGenFirstMsg::create_commitments(); + let (eph_party_one_first_message, eph_ec_key_pair_party1) = + party_one::EphKeyGenFirstMsg::create(); + let eph_party_two_second_message = party_two::EphKeyGenSecondMsg::verify_and_decommit( + eph_comm_witness, + &eph_party_one_first_message, + ) + .expect("party1 DLog proof failed"); + + let _eph_party_one_second_message = + party_one::EphKeyGenSecondMsg::verify_commitments_and_dlog_proof( + &eph_party_two_first_message, + &eph_party_two_second_message, + ) + .expect("failed to verify commitments and DLog proof"); + let party2_private = party_two::Party2Private::set_private_key(&ec_key_pair_party2); + let message = BigInt::from(1234); + let partial_sig = party_two::PartialSig::compute( + &keypair.ek, + &keypair.encrypted_share, + &party2_private, + &eph_ec_key_pair_party2, + &eph_party_one_first_message.public_share, + &message, + ); + + let party1_private = party_one::Party1Private::set_private_key(&ec_key_pair_party1, &keypair); + + let signature = party_one::Signature::compute( + &party1_private, + &partial_sig.c3, + &eph_ec_key_pair_party1, + &eph_party_two_second_message.comm_witness.public_share, + ); + + let pubkey = + party_one::compute_pubkey(&party1_private, &party_two_private_share_gen.public_share); + party_one::verify(&signature, &pubkey, &message).expect("Invalid signature") +} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/mod.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/mod.rs new file mode 100644 index 00000000..d1785d24 --- /dev/null +++ b/multi-party-ecdsa/src/protocols/two_party_ecdsa/mod.rs @@ -0,0 +1,24 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +// Fast Secure Two-Party ECDSA Signing by Yehuda Lindell (https://eprint.iacr.org/2017/552.pdf). + +pub mod lindell_2017; + +// Two-Party ECDSA from Hash Proof Systems and +//Efficient Instantiations (https://eprint.iacr.org/2019/503.pdf) +#[cfg(feature = "cclst")] +pub mod cclst_2019; diff --git a/multi-party-ecdsa/src/utilities/mod.rs b/multi-party-ecdsa/src/utilities/mod.rs new file mode 100644 index 00000000..867fce30 --- /dev/null +++ b/multi-party-ecdsa/src/utilities/mod.rs @@ -0,0 +1,3 @@ +pub mod mta; +pub mod zk_pdl; +pub mod zk_pdl_with_slack; diff --git a/multi-party-ecdsa/src/utilities/mta/mod.rs b/multi-party-ecdsa/src/utilities/mta/mod.rs new file mode 100644 index 00000000..570ac872 --- /dev/null +++ b/multi-party-ecdsa/src/utilities/mta/mod.rs @@ -0,0 +1,215 @@ +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +/// MtA is described in https://eprint.iacr.org/2019/114.pdf section 3 +use curv::arithmetic::traits::Samplable; +use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use paillier::traits::EncryptWithChosenRandomness; +use paillier::{Add, Decrypt, Mul}; +use paillier::{DecryptionKey, EncryptionKey, Paillier, Randomness, RawCiphertext, RawPlaintext}; +use zk_paillier::zkproofs::DLogStatement; + +use serde::{Deserialize, Serialize}; +use sha2::Sha256; + +use crate::protocols::multi_party_ecdsa::gg_2018::party_i::PartyPrivate; +use crate::utilities::mta::range_proofs::AliceProof; +use crate::Error::{self, InvalidKey}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MessageA { + pub c: BigInt, // paillier encryption + pub range_proofs: Vec, // proofs (using other parties' h1,h2,N_tilde) that the plaintext is small +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MessageB { + pub c: BigInt, // paillier encryption + pub b_proof: DLogProof, + pub beta_tag_proof: DLogProof, +} + +impl MessageA { + /// Creates a new `messageA` using Alice's Paillier encryption key and `dlog_statements` + /// - other parties' `h1,h2,N_tilde`s for range proofs. + /// If range proofs are not needed (one example is identification of aborts where we + /// only want to reconstruct a ciphertext), `dlog_statements` can be an empty slice. + pub fn a( + a: &Scalar, + alice_ek: &EncryptionKey, + dlog_statements: &[DLogStatement], + ) -> (Self, BigInt) { + let randomness = BigInt::sample_below(&alice_ek.n); + let m_a = MessageA::a_with_predefined_randomness(a, alice_ek, &randomness, dlog_statements); + (m_a, randomness) + } + + pub fn a_with_predefined_randomness( + a: &Scalar, + alice_ek: &EncryptionKey, + randomness: &BigInt, + dlog_statements: &[DLogStatement], + ) -> Self { + let c_a = Paillier::encrypt_with_chosen_randomness( + alice_ek, + RawPlaintext::from(a.to_bigint()), + &Randomness::from(randomness.clone()), + ) + .0 + .clone() + .into_owned(); + let alice_range_proofs = dlog_statements + .iter() + .map(|dlog_statement| { + AliceProof::generate(&a.to_bigint(), &c_a, alice_ek, dlog_statement, randomness) + }) + .collect::>(); + + Self { + c: c_a, + range_proofs: alice_range_proofs, + } + } +} + +impl MessageB { + pub fn b( + b: &Scalar, + alice_ek: &EncryptionKey, + m_a: MessageA, + dlog_statements: &[DLogStatement], + ) -> Result<(Self, Scalar, BigInt, BigInt), Error> { + let beta_tag = BigInt::sample_below(&alice_ek.n); + let randomness = BigInt::sample_below(&alice_ek.n); + let (m_b, beta) = MessageB::b_with_predefined_randomness( + b, + alice_ek, + m_a, + &randomness, + &beta_tag, + dlog_statements, + )?; + + Ok((m_b, beta, randomness, beta_tag)) + } + + pub fn b_with_predefined_randomness( + b: &Scalar, + alice_ek: &EncryptionKey, + m_a: MessageA, + randomness: &BigInt, + beta_tag: &BigInt, + dlog_statements: &[DLogStatement], + ) -> Result<(Self, Scalar), Error> { + if m_a.range_proofs.len() != dlog_statements.len() { + return Err(InvalidKey); + } + // verify proofs + if !m_a + .range_proofs + .iter() + .zip(dlog_statements) + .map(|(proof, dlog_statement)| proof.verify(&m_a.c, alice_ek, dlog_statement)) + .all(|x| x) + { + log::info!("MP-ECDSA : Proof Mismatch"); + return Err(InvalidKey); + }; + let beta_tag_fe = Scalar::::from(beta_tag); + let c_beta_tag = Paillier::encrypt_with_chosen_randomness( + alice_ek, + RawPlaintext::from(beta_tag), + &Randomness::from(randomness.clone()), + ); + + let b_bn = b.to_bigint(); + let b_c_a = Paillier::mul( + alice_ek, + RawCiphertext::from(m_a.c), + RawPlaintext::from(b_bn), + ); + let c_b = Paillier::add(alice_ek, b_c_a, c_beta_tag); + let beta = Scalar::::zero() - &beta_tag_fe; + let dlog_proof_b = DLogProof::prove(b); + let dlog_proof_beta_tag = DLogProof::prove(&beta_tag_fe); + + Ok(( + Self { + c: c_b.0.clone().into_owned(), + b_proof: dlog_proof_b, + beta_tag_proof: dlog_proof_beta_tag, + }, + beta, + )) + } + + pub fn verify_proofs_get_alpha( + &self, + dk: &DecryptionKey, + a: &Scalar, + ) -> Result<(Scalar, BigInt), Error> { + let alice_share = Paillier::decrypt(dk, &RawCiphertext::from(self.c.clone())); + let g = Point::generator(); + let alpha = Scalar::::from(alice_share.0.as_ref()); + let g_alpha = g * α + let ba_btag = &self.b_proof.pk * a + &self.beta_tag_proof.pk; + if DLogProof::verify(&self.b_proof).is_ok() + && DLogProof::verify(&self.beta_tag_proof).is_ok() + // we prove the correctness of the ciphertext using this check and the proof of knowledge of dlog of beta_tag + && ba_btag == g_alpha + { + Ok((alpha, alice_share.0.into_owned())) + } else { + Err(InvalidKey) + } + } + + // another version, supporting PartyPrivate therefore binding mta to gg18. + // with the regular version mta can be used in general + pub fn verify_proofs_get_alpha_gg18( + &self, + private: &PartyPrivate, + a: &Scalar, + ) -> Result, Error> { + let alice_share = private.decrypt(self.c.clone()); + let g = Point::generator(); + let alpha = Scalar::::from(alice_share.0.as_ref()); + let g_alpha = g * α + let ba_btag = &self.b_proof.pk * a + &self.beta_tag_proof.pk; + + if DLogProof::verify(&self.b_proof).is_ok() + && DLogProof::verify(&self.beta_tag_proof).is_ok() + && ba_btag == g_alpha + { + Ok(alpha) + } else { + Err(InvalidKey) + } + } + + pub fn verify_b_against_public( + public_gb: &Point, + mta_gb: &Point, + ) -> bool { + public_gb == mta_gb + } +} + +pub mod range_proofs; +#[cfg(test)] +mod test; diff --git a/multi-party-ecdsa/src/utilities/mta/range_proofs.rs b/multi-party-ecdsa/src/utilities/mta/range_proofs.rs new file mode 100644 index 00000000..4610a57f --- /dev/null +++ b/multi-party-ecdsa/src/utilities/mta/range_proofs.rs @@ -0,0 +1,710 @@ +#![allow(non_snake_case)] + +//! This file is a modified version of ING bank's range proofs implementation: +//! https://github.com/ing-bank/threshold-signatures/blob/master/src/algorithms/zkp.rs +//! +//! Zero knowledge range proofs for MtA protocol are implemented here. +//! Formal description can be found in Appendix A of https://eprint.iacr.org/2019/114.pdf +//! There are some deviations from the original specification: +//! 1) In Bob's proofs `gamma` is sampled from `[0;q^2 * N]` and `tau` from `[0;q^3 * N_tilde]`. +//! 2) A non-interactive version is implemented, with challenge `e` computed via Fiat-Shamir. + +use curv::arithmetic::traits::*; +use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use sha2::Sha256; + +use paillier::{EncryptionKey, Randomness}; +use zk_paillier::zkproofs::DLogStatement; + +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; +use zeroize::Zeroize; + +/// Represents the first round of the interactive version of the proof +#[derive(Zeroize)] +#[zeroize(drop)] +struct AliceZkpRound1 { + alpha: BigInt, + beta: BigInt, + gamma: BigInt, + ro: BigInt, + z: BigInt, + u: BigInt, + w: BigInt, +} + +impl AliceZkpRound1 { + fn from( + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + a: &BigInt, + q: &BigInt, + ) -> Self { + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let N_tilde = &dlog_statement.N; + let alpha = BigInt::sample_below(&q.pow(3)); + let beta = BigInt::from_paillier_key(alice_ek); + let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let ro = BigInt::sample_below(&(q * N_tilde)); + let z = (BigInt::mod_pow(h1, a, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; + let u = ((alpha.borrow() * &alice_ek.n + 1) + * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) + % &alice_ek.nn; + let w = + (BigInt::mod_pow(h1, &alpha, N_tilde) * BigInt::mod_pow(h2, &gamma, N_tilde)) % N_tilde; + Self { + alpha, + beta, + gamma, + ro, + z, + u, + w, + } + } +} + +/// Represents the second round of the interactive version of the proof +struct AliceZkpRound2 { + s: BigInt, + s1: BigInt, + s2: BigInt, +} + +impl AliceZkpRound2 { + fn from( + alice_ek: &EncryptionKey, + round1: &AliceZkpRound1, + e: &BigInt, + a: &BigInt, + r: &BigInt, + ) -> Self { + Self { + s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, + s1: (e * a) + round1.alpha.borrow(), + s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), + } + } +} + +/// Alice's proof +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AliceProof { + z: BigInt, + e: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, +} + +impl AliceProof { + /// verify Alice's proof using the proof and public keys + pub fn verify( + &self, + cipher: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + ) -> bool { + let N = &alice_ek.n; + let NN = &alice_ek.nn; + let N_tilde = &dlog_statement.N; + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let Gen = alice_ek.n.borrow() + 1; + + if self.s1 > Scalar::::group_order().pow(3) { + return false; + } + + let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); + let z_e_inv = match z_e_inv { + // z must be invertible, yet the check is done here + None => return false, + Some(c) => c, + }; + + let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) + * BigInt::mod_pow(h2, &self.s2, N_tilde) + * z_e_inv) + % N_tilde; + + let gs1 = (self.s1.borrow() * N + 1) % NN; + let cipher_e_inv = BigInt::mod_inv(&BigInt::mod_pow(cipher, &self.e, NN), NN); + let cipher_e_inv = match cipher_e_inv { + None => return false, + Some(c) => c, + }; + + let u = (gs1 * BigInt::mod_pow(&self.s, N, NN) * cipher_e_inv) % NN; + + let e = Sha256::new() + .chain_bigint(N) + .chain_bigint(&Gen) + .chain_bigint(cipher) + .chain_bigint(&self.z) + .chain_bigint(&u) + .chain_bigint(&w) + .result_bigint(); + if e != self.e { + return false; + } + + true + } + /// Create the proof using Alice's Paillier private keys and public ZKP setup. + /// Requires randomness used for encrypting Alice's secret a. + /// It is assumed that secp256k1 curve is used. + pub fn generate( + a: &BigInt, + cipher: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &BigInt, + ) -> Self { + let round1 = AliceZkpRound1::from( + alice_ek, + dlog_statement, + a, + Scalar::::group_order(), + ); + + let Gen = alice_ek.n.borrow() + 1; + let e = Sha256::new() + .chain_bigint(&alice_ek.n) + .chain_bigint(&Gen) + .chain_bigint(cipher) + .chain_bigint(&round1.z) + .chain_bigint(&round1.u) + .chain_bigint(&round1.w) + .result_bigint(); + + let round2 = AliceZkpRound2::from(alice_ek, &round1, &e, a, r); + + Self { + z: round1.z.clone(), + e, + s: round2.s, + s1: round2.s1, + s2: round2.s2, + } + } +} + +/// Represents first round of the interactive version of the proof +#[derive(Zeroize)] +#[zeroize(drop)] +struct BobZkpRound1 { + pub alpha: BigInt, + pub beta: BigInt, + pub gamma: BigInt, + pub ro: BigInt, + pub ro_prim: BigInt, + pub sigma: BigInt, + pub tau: BigInt, + pub z: BigInt, + pub z_prim: BigInt, + pub t: BigInt, + pub w: BigInt, + pub v: BigInt, +} + +impl BobZkpRound1 { + /// `b` - Bob's secret + /// `beta_prim` - randomly chosen in `MtA` by Bob + /// `a_encrypted` - Alice's secret encrypted by Alice + fn from( + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + b: &Scalar, + beta_prim: &BigInt, + a_encrypted: &BigInt, + q: &BigInt, + ) -> Self { + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let N_tilde = &dlog_statement.N; + let b_bn = b.to_bigint(); + + let alpha = BigInt::sample_below(&q.pow(3)); + let beta = BigInt::from_paillier_key(alice_ek); + let gamma = BigInt::sample_below(&(q.pow(2) * &alice_ek.n)); + let ro = BigInt::sample_below(&(q * N_tilde)); + let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let sigma = BigInt::sample_below(&(q * N_tilde)); + let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; + let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) + * BigInt::mod_pow(h2, &ro_prim, N_tilde)) + % N_tilde; + let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * BigInt::mod_pow(h2, &sigma, N_tilde)) + % N_tilde; + let w = + (BigInt::mod_pow(h1, &gamma, N_tilde) * BigInt::mod_pow(h2, &tau, N_tilde)) % N_tilde; + let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) + * (gamma.borrow() * &alice_ek.n + 1) + * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) + % &alice_ek.nn; + Self { + alpha, + beta, + gamma, + ro, + ro_prim, + sigma, + tau, + z, + z_prim, + t, + w, + v, + } + } +} + +/// represents second round of the interactive version of the proof +struct BobZkpRound2 { + pub s: BigInt, + pub s1: BigInt, + pub s2: BigInt, + pub t1: BigInt, + pub t2: BigInt, +} + +impl BobZkpRound2 { + /// `e` - the challenge in interactive ZKP, the hash in non-interactive ZKP + /// `b` - Bob's secret + /// `beta_prim` - randomly chosen in `MtA` by Bob + /// `r` - randomness used by Bob on Alice's public Paillier key to encrypt `beta_prim` in `MtA` + fn from( + alice_ek: &EncryptionKey, + round1: &BobZkpRound1, + e: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + r: &Randomness, + ) -> Self { + let b_bn = b.to_bigint(); + Self { + s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, + s1: (e * b_bn) + round1.alpha.borrow(), + s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), + t1: (e * beta_prim) + round1.gamma.borrow(), + t2: (e * round1.sigma.borrow()) + round1.tau.borrow(), + } + } +} + +/// Additional fields in Bob's proof if MtA is run with check +pub struct BobCheck { + u: Point, + X: Point, +} + +/// Bob's regular proof +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct BobProof { + t: BigInt, + z: BigInt, + e: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, + t1: BigInt, + t2: BigInt, +} + +#[allow(clippy::too_many_arguments)] +impl BobProof { + pub fn verify( + &self, + a_enc: &BigInt, + mta_avc_out: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + check: Option<&BobCheck>, + ) -> bool { + let N = &alice_ek.n; + let NN = &alice_ek.nn; + let N_tilde = &dlog_statement.N; + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + + if self.s1 > Scalar::::group_order().pow(3) { + return false; + } + + let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); + let z_e_inv = match z_e_inv { + // z must be invertible, yet the check is done here + None => return false, + Some(c) => c, + }; + + let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) + * BigInt::mod_pow(h2, &self.s2, N_tilde) + * z_e_inv) + % N_tilde; + + let mta_e_inv = BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); + let mta_e_inv = match mta_e_inv { + None => return false, + Some(c) => c, + }; + + let v = (BigInt::mod_pow(a_enc, &self.s1, NN) + * BigInt::mod_pow(&self.s, N, NN) + * (self.t1.borrow() * N + 1) + * mta_e_inv) + % NN; + + let t_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.t, &self.e, N_tilde), N_tilde); + let t_e_inv = match t_e_inv { + None => return false, + Some(c) => c, + }; + + let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) + * BigInt::mod_pow(h2, &self.t2, N_tilde) + * t_e_inv) + % N_tilde; + + let Gen = alice_ek.n.borrow() + 1; + let mut values_to_hash = vec![ + &alice_ek.n, + &Gen, + a_enc, + mta_avc_out, + &self.z, + &z_prim, + &self.t, + &v, + &w, + ]; + let e = match check { + Some(_) => { + let X_x_coor = check.unwrap().X.x_coord().unwrap(); + values_to_hash.push(&X_x_coor); + let X_y_coor = check.unwrap().X.y_coord().unwrap(); + values_to_hash.push(&X_y_coor); + let u_x_coor = check.unwrap().u.x_coord().unwrap(); + values_to_hash.push(&u_x_coor); + let u_y_coor = check.unwrap().u.y_coord().unwrap(); + values_to_hash.push(&u_y_coor); + values_to_hash + .into_iter() + .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) + .result_bigint() + } + None => values_to_hash + .into_iter() + .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) + .result_bigint(), + }; + + if e != self.e { + return false; + } + + true + } + + pub fn generate( + a_encrypted: &BigInt, + mta_encrypted: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &Randomness, + check: bool, + ) -> (BobProof, Option>) { + let round1 = BobZkpRound1::from( + alice_ek, + dlog_statement, + b, + beta_prim, + a_encrypted, + Scalar::::group_order(), + ); + + let Gen = alice_ek.n.borrow() + 1; + let mut values_to_hash = vec![ + &alice_ek.n, + &Gen, + a_encrypted, + mta_encrypted, + &round1.z, + &round1.z_prim, + &round1.t, + &round1.v, + &round1.w, + ]; + let mut check_u = None; + let e = if check { + let (X, u) = { + let ec_gen = Point::generator(); + let alpha = Scalar::::from(&round1.alpha); + (ec_gen * b, ec_gen * alpha) + }; + check_u = Some(u.clone()); + let X_x_coor = X.x_coord().unwrap(); + values_to_hash.push(&X_x_coor); + let X_y_coor = X.y_coord().unwrap(); + values_to_hash.push(&X_y_coor); + let u_x_coor = u.x_coord().unwrap(); + values_to_hash.push(&u_x_coor); + let u_y_coor = u.y_coord().unwrap(); + values_to_hash.push(&u_y_coor); + values_to_hash + .into_iter() + .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) + .result_bigint() + } else { + values_to_hash + .into_iter() + .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) + .result_bigint() + }; + + let round2 = BobZkpRound2::from(alice_ek, &round1, &e, b, beta_prim, r); + + ( + BobProof { + t: round1.t.clone(), + z: round1.z.clone(), + e, + s: round2.s, + s1: round2.s1, + s2: round2.s2, + t1: round2.t1, + t2: round2.t2, + }, + check_u, + ) + } +} + +/// Bob's extended proof, adds the knowledge of $`B = g^b \in \mathcal{G}`$ +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct BobProofExt { + proof: BobProof, + u: Point, +} + +#[allow(clippy::too_many_arguments)] +impl BobProofExt { + pub fn verify( + &self, + a_enc: &BigInt, + mta_avc_out: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + X: &Point, + ) -> bool { + // check basic proof first + if !self.proof.verify( + a_enc, + mta_avc_out, + alice_ek, + dlog_statement, + Some(&BobCheck { + u: self.u.clone(), + X: X.clone(), + }), + ) { + return false; + } + + // fiddle with EC points + let (x1, x2) = { + let ec_gen = Point::generator(); + let s1 = Scalar::::from(&self.proof.s1); + let e = Scalar::::from(&self.proof.e); + (ec_gen * s1, (X * &e) + &self.u) + }; + + if x1 != x2 { + return false; + } + + true + } +} + +/// sample random value of an element of a multiplicative group +pub trait SampleFromMultiplicativeGroup { + fn from_modulo(N: &BigInt) -> BigInt; + fn from_paillier_key(ek: &EncryptionKey) -> BigInt; +} + +impl SampleFromMultiplicativeGroup for BigInt { + fn from_modulo(N: &BigInt) -> BigInt { + let One = BigInt::one(); + loop { + let r = Self::sample_below(N); + if r.gcd(N) == One { + return r; + } + } + } + + fn from_paillier_key(ek: &EncryptionKey) -> BigInt { + Self::from_modulo(ek.n.borrow()) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use paillier::traits::{Encrypt, EncryptWithChosenRandomness, KeyGeneration}; + use paillier::{Add, DecryptionKey, Mul, Paillier, RawCiphertext, RawPlaintext}; + + fn generate( + a_encrypted: &BigInt, + mta_encrypted: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &Randomness, + ) -> BobProofExt { + // proving a basic proof (with modified hash) + let (bob_proof, u) = BobProof::generate( + a_encrypted, + mta_encrypted, + b, + beta_prim, + alice_ek, + dlog_statement, + r, + true, + ); + + BobProofExt { + proof: bob_proof, + u: u.unwrap(), + } + } + + pub(crate) fn generate_init() -> (DLogStatement, EncryptionKey, DecryptionKey) { + let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&ek_tilde.n); + let (xhi, _) = loop { + let xhi_ = BigInt::sample_below(&phi); + match BigInt::mod_inv(&xhi_, &phi) { + Some(inv) => break (xhi_, inv), + None => continue, + } + }; + let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); + + let (ek, dk) = Paillier::keypair().keys(); + let dlog_statement = DLogStatement { + g: h1, + ni: h2, + N: ek_tilde.n, + }; + (dlog_statement, ek, dk) + } + + #[test] + fn alice_zkp() { + let (dlog_statement, ek, _) = generate_init(); + + // Alice's secret value + let a = Scalar::::random().to_bigint(); + let r = BigInt::from_paillier_key(&ek); + let cipher = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(a.clone()), + &Randomness::from(&r), + ) + .0 + .clone() + .into_owned(); + + let alice_proof = AliceProof::generate(&a, &cipher, &ek, &dlog_statement, &r); + + assert!(alice_proof.verify(&cipher, &ek, &dlog_statement)); + } + + #[test] + fn bob_zkp() { + let (dlog_statement, ek, _) = generate_init(); + + (0..5).for_each(|_| { + let alice_public_key = &ek; + + // run MtA protocol with different inputs + (0..5).for_each(|_| { + // Simulate Alice + let a = Scalar::::random().to_bigint(); + let encrypted_a = Paillier::encrypt(alice_public_key, RawPlaintext::from(a)) + .0 + .clone() + .into_owned(); + + // Bob follows MtA + let b = Scalar::::random(); + // E(a) * b + let b_times_enc_a = Paillier::mul( + alice_public_key, + RawCiphertext::from(encrypted_a.clone()), + RawPlaintext::from(&b.to_bigint()), + ); + let beta_prim = BigInt::sample_below(&alice_public_key.n); + let r = Randomness::sample(alice_public_key); + let enc_beta_prim = Paillier::encrypt_with_chosen_randomness( + alice_public_key, + RawPlaintext::from(&beta_prim), + &r, + ); + + let mta_out = Paillier::add(alice_public_key, b_times_enc_a, enc_beta_prim); + + let (bob_proof, _) = BobProof::generate( + &encrypted_a, + &mta_out.0.clone().into_owned(), + &b, + &beta_prim, + alice_public_key, + &dlog_statement, + &r, + false, + ); + assert!(bob_proof.verify( + &encrypted_a, + &mta_out.0.clone().into_owned(), + alice_public_key, + &dlog_statement, + None + )); + + // Bob follows MtAwc + let ec_gen = Point::generator(); + let X = ec_gen * &b; + let bob_proof = generate( + &encrypted_a, + &mta_out.0.clone().into_owned(), + &b, + &beta_prim, + alice_public_key, + &dlog_statement, + &r, + ); + assert!(bob_proof.verify( + &encrypted_a, + &mta_out.0.clone().into_owned(), + alice_public_key, + &dlog_statement, + &X + )); + }); + }); + } +} diff --git a/multi-party-ecdsa/src/utilities/mta/test.rs b/multi-party-ecdsa/src/utilities/mta/test.rs new file mode 100644 index 00000000..0602ea9a --- /dev/null +++ b/multi-party-ecdsa/src/utilities/mta/test.rs @@ -0,0 +1,19 @@ +use crate::utilities::mta::range_proofs::tests::generate_init; +use crate::utilities::mta::{MessageA, MessageB}; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Scalar}; + +#[test] +fn test_mta() { + let alice_input = Scalar::::random(); + let (dlog_statement, ek_alice, dk_alice) = generate_init(); + let bob_input = Scalar::::random(); + let (m_a, _) = MessageA::a(&alice_input, &ek_alice, &[dlog_statement.clone()]); + let (m_b, beta, _, _) = MessageB::b(&bob_input, &ek_alice, m_a, &[dlog_statement]).unwrap(); + let alpha = m_b + .verify_proofs_get_alpha(&dk_alice, &alice_input) + .expect("wrong dlog or m_b"); + + let left = alpha.0 + beta; + let right = alice_input * bob_input; + assert_eq!(left, right); +} diff --git a/multi-party-ecdsa/src/utilities/zk_pdl/mod.rs b/multi-party-ecdsa/src/utilities/zk_pdl/mod.rs new file mode 100644 index 00000000..2d703527 --- /dev/null +++ b/multi-party-ecdsa/src/utilities/zk_pdl/mod.rs @@ -0,0 +1,262 @@ +#![allow(non_snake_case)] +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +//! We use the proof as given in protocol 6.1 in https://eprint.iacr.org/2017/552.pdf +//! Statement: (c, pk, Q, G) +//! witness (x, r, sk) such that Q = xG, c = Enc(pk, x, r) and Dec(sk, c) = x. +//! note that because of the range proof, the proof is sound only for x < q/3 + +use std::ops::Shl; + +use curv::arithmetic::traits::*; +use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; +use curv::cryptographic_primitives::commitments::traits::Commitment; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use paillier::Paillier; +use paillier::{Add, Decrypt, Encrypt, Mul}; +use paillier::{DecryptionKey, EncryptionKey, RawCiphertext, RawPlaintext}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use thiserror::Error; +use zk_paillier::zkproofs::IncorrectProof; +use zk_paillier::zkproofs::RangeProofNi; + +#[derive(Error, Debug)] +pub enum ZkPdlError { + #[error("zk pdl message2 failed")] + Message2, + #[error("zk pdl finalize failed")] + Finalize, +} + +#[derive(Clone)] +pub struct PDLStatement { + pub ciphertext: BigInt, + pub ek: EncryptionKey, + pub Q: Point, + pub G: Point, +} +#[derive(Clone)] +pub struct PDLWitness { + pub x: Scalar, + pub r: BigInt, + pub dk: DecryptionKey, +} + +#[derive(Debug, Clone)] +pub struct PDLVerifierState { + pub c_tag: BigInt, + pub c_tag_tag: BigInt, + a: BigInt, + b: BigInt, + blindness: BigInt, + q_tag: Point, + c_hat: BigInt, +} + +#[derive(Debug, Clone)] +pub struct PDLProverState { + pub decommit: PDLProverDecommit, + pub alpha: BigInt, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PDLVerifierFirstMessage { + pub c_tag: BigInt, + pub c_tag_tag: BigInt, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PDLProverFirstMessage { + pub c_hat: BigInt, + pub range_proof: RangeProofNi, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PDLVerifierSecondMessage { + pub a: BigInt, + pub b: BigInt, + pub blindness: BigInt, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PDLProverDecommit { + pub q_hat: Point, + pub blindness: BigInt, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PDLProverSecondMessage { + pub decommit: PDLProverDecommit, +} + +pub struct Prover {} +pub struct Verifier {} + +impl Verifier { + pub fn message1(statement: &PDLStatement) -> (PDLVerifierFirstMessage, PDLVerifierState) { + let a_fe = Scalar::::random(); + let a = a_fe.to_bigint(); + let q = Scalar::::group_order(); + let q_sq = q.pow(2); + let b = BigInt::sample_below(&q_sq); + let b_fe = Scalar::::from(&b); + let b_enc = Paillier::encrypt(&statement.ek, RawPlaintext::from(b.clone())); + let ac = Paillier::mul( + &statement.ek, + RawCiphertext::from(statement.ciphertext.clone()), + RawPlaintext::from(a.clone()), + ); + let c_tag = Paillier::add(&statement.ek, ac, b_enc).0.into_owned(); + let ab_concat = a.clone() + b.clone().shl(a.bit_length()); + let blindness = BigInt::sample_below(q); + let c_tag_tag = HashCommitment::::create_commitment_with_user_defined_randomness( + &ab_concat, &blindness, + ); + let q_tag = &statement.Q * &a_fe + &statement.G * b_fe; + + ( + PDLVerifierFirstMessage { + c_tag: c_tag.clone(), + c_tag_tag: c_tag_tag.clone(), + }, + PDLVerifierState { + c_tag, + c_tag_tag, + a, + b, + blindness, + q_tag, + c_hat: BigInt::zero(), + }, + ) + } + + pub fn message2( + prover_first_messasge: &PDLProverFirstMessage, + statement: &PDLStatement, + state: &mut PDLVerifierState, + ) -> Result { + let decommit_message = PDLVerifierSecondMessage { + a: state.a.clone(), + b: state.b.clone(), + blindness: state.blindness.clone(), + }; + let range_proof_is_ok = + verify_range_proof(statement, &prover_first_messasge.range_proof).is_ok(); + state.c_hat = prover_first_messasge.c_hat.clone(); + if range_proof_is_ok { + Ok(decommit_message) + } else { + Err(ZkPdlError::Message2) + } + } + + pub fn finalize( + prover_first_message: &PDLProverFirstMessage, + prover_second_message: &PDLProverSecondMessage, + state: &PDLVerifierState, + ) -> Result<(), ZkPdlError> { + let c_hat_test = HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(prover_second_message.decommit.q_hat.to_bytes(true).as_ref()), + &prover_second_message.decommit.blindness, + ); + + if prover_first_message.c_hat == c_hat_test + && prover_second_message.decommit.q_hat == state.q_tag + { + Ok(()) + } else { + Err(ZkPdlError::Finalize) + } + } +} + +impl Prover { + pub fn message1( + witness: &PDLWitness, + statement: &PDLStatement, + verifier_first_message: &PDLVerifierFirstMessage, + ) -> (PDLProverFirstMessage, PDLProverState) { + let c_tag = verifier_first_message.c_tag.clone(); + let alpha = Paillier::decrypt(&witness.dk, &RawCiphertext::from(c_tag)); + let alpha_fe = Scalar::::from(alpha.0.as_ref()); + let q_hat = &statement.G * alpha_fe; + let blindness = BigInt::sample_below(Scalar::::group_order()); + let c_hat = HashCommitment::::create_commitment_with_user_defined_randomness( + &BigInt::from_bytes(q_hat.to_bytes(true).as_ref()), + &blindness, + ); + // in parallel generate range proof: + let range_proof = generate_range_proof(statement, witness); + ( + PDLProverFirstMessage { c_hat, range_proof }, + PDLProverState { + decommit: PDLProverDecommit { blindness, q_hat }, + alpha: alpha.0.into_owned(), + }, + ) + } + + pub fn message2( + verifier_first_message: &PDLVerifierFirstMessage, + verifier_second_message: &PDLVerifierSecondMessage, + witness: &PDLWitness, + state: &PDLProverState, + ) -> Result { + let ab_concat = &verifier_second_message.a + + verifier_second_message + .b + .clone() + .shl(verifier_second_message.a.bit_length()); // b|a (in the paper it is a|b) + let c_tag_tag_test = + HashCommitment::::create_commitment_with_user_defined_randomness( + &ab_concat, + &verifier_second_message.blindness, + ); + let ax1 = &verifier_second_message.a * witness.x.to_bigint(); + let alpha_test = ax1 + &verifier_second_message.b; + if alpha_test == state.alpha && verifier_first_message.c_tag_tag == c_tag_tag_test { + Ok(PDLProverSecondMessage { + decommit: state.decommit.clone(), + }) + } else { + Err(ZkPdlError::Message2) + } + } +} + +fn generate_range_proof(statement: &PDLStatement, witness: &PDLWitness) -> RangeProofNi { + RangeProofNi::prove( + &statement.ek, + Scalar::::group_order(), + &statement.ciphertext, + &witness.x.to_bigint(), + &witness.r, + ) +} + +fn verify_range_proof( + statement: &PDLStatement, + range_proof: &RangeProofNi, +) -> Result<(), IncorrectProof> { + range_proof.verify(&statement.ek, &statement.ciphertext) +} + +#[cfg(test)] +mod test; diff --git a/multi-party-ecdsa/src/utilities/zk_pdl/test.rs b/multi-party-ecdsa/src/utilities/zk_pdl/test.rs new file mode 100644 index 00000000..3f4e6994 --- /dev/null +++ b/multi-party-ecdsa/src/utilities/zk_pdl/test.rs @@ -0,0 +1,58 @@ +#![allow(non_snake_case)] + +use curv::arithmetic::traits::*; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use paillier::core::Randomness; +use paillier::traits::{EncryptWithChosenRandomness, KeyGeneration}; +use paillier::Paillier; +use paillier::RawPlaintext; + +use crate::utilities::zk_pdl::{PDLStatement, PDLWitness, Prover, Verifier}; + +#[test] +fn test_zk_pdl() { + // pre-test: + + let (ek, dk) = Paillier::keypair().keys(); + let randomness = Randomness::sample(&ek); + let x = Scalar::::random(); + let x: Scalar = + Scalar::::from(&x.to_bigint().div_floor(&BigInt::from(3))); + + let Q = Point::generator() * &x; + + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint()), + &randomness, + ) + .0 + .into_owned(); + let statement = PDLStatement { + ciphertext: c, + ek, + Q, + G: Point::generator().to_point(), + }; + let witness = PDLWitness { + x, + r: randomness.0, + dk, + }; + // + let (verifier_message1, mut verifier_state) = Verifier::message1(&statement); + let (prover_message1, prover_state) = + Prover::message1(&witness, &statement, &verifier_message1); + let verifier_message2 = + Verifier::message2(&prover_message1, &statement, &mut verifier_state).expect(""); + let prover_message2 = Prover::message2( + &verifier_message1, + &verifier_message2, + &witness, + &prover_state, + ) + .expect(""); + let result = Verifier::finalize(&prover_message1, &prover_message2, &verifier_state); + assert!(result.is_ok()); +} diff --git a/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs new file mode 100644 index 00000000..c9d2c70c --- /dev/null +++ b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/mod.rs @@ -0,0 +1,202 @@ +#![allow(non_snake_case)] +/* + Multi-party ECDSA + + Copyright 2018 by Kzen Networks + + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) + + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + + @license GPL-3.0+ +*/ + +//! We use the proof as given in proof PIi in https://eprint.iacr.org/2016/013.pdf. +//! This proof ws taken from the proof 6.3 (left side ) in https://www.cs.unc.edu/~reiter/papers/2004/IJIS.pdf +//! +//! Statement: (c, pk, Q, G) +//! witness (x, r) such that Q = xG, c = Enc(pk, x, r) +//! note that because of the range proof, the proof has a slack in the range: x in [-q^3, q^3] + +use curv::arithmetic::traits::*; +use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use paillier::EncryptionKey; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ZkPdlWithSlackError { + #[error("zk pdl with slack verification failed")] + Verify, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PDLwSlackStatement { + pub ciphertext: BigInt, + pub ek: EncryptionKey, + pub Q: Point, + pub G: Point, + pub h1: BigInt, + pub h2: BigInt, + pub N_tilde: BigInt, +} +#[derive(Clone)] +pub struct PDLwSlackWitness { + pub x: Scalar, + pub r: BigInt, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PDLwSlackProof { + z: BigInt, + u1: Point, + u2: BigInt, + u3: BigInt, + s1: BigInt, + s2: BigInt, + s3: BigInt, +} + +impl PDLwSlackProof { + pub fn prove(witness: &PDLwSlackWitness, statement: &PDLwSlackStatement) -> Self { + let q3 = Scalar::::group_order().pow(3); + let q_N_tilde = Scalar::::group_order() * &statement.N_tilde; + let q3_N_tilde = &q3 * &statement.N_tilde; + + let alpha = BigInt::sample_below(&q3); + let one = BigInt::one(); + let beta = BigInt::sample_range(&one, &(&statement.ek.n - &one)); + let rho = BigInt::sample_below(&q_N_tilde); + let gamma = BigInt::sample_below(&q3_N_tilde); + + let z = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &witness.x.to_bigint(), + &rho, + ); + let u1 = &statement.G * &Scalar::::from(&alpha); + let u2 = commitment_unknown_order( + &(&statement.ek.n + BigInt::one()), + &beta, + &statement.ek.nn, + &alpha, + &statement.ek.n, + ); + let u3 = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &alpha, + &gamma, + ); + + let e = Sha256::new() + .chain_bigint(&BigInt::from_bytes(statement.G.to_bytes(true).as_ref())) + .chain_bigint(&BigInt::from_bytes(statement.Q.to_bytes(true).as_ref())) + .chain_bigint(&statement.ciphertext) + .chain_bigint(&z) + .chain_bigint(&BigInt::from_bytes(u1.to_bytes(true).as_ref())) + .chain_bigint(&u2) + .chain_bigint(&u3) + .result_bigint(); + + let s1 = &e * witness.x.to_bigint() + alpha; + let s2 = commitment_unknown_order(&witness.r, &beta, &statement.ek.n, &e, &BigInt::one()); + let s3 = &e * rho + gamma; + + PDLwSlackProof { + z, + u1, + u2, + u3, + s1, + s2, + s3, + } + } + + pub fn verify(&self, statement: &PDLwSlackStatement) -> Result<(), ZkPdlWithSlackError> { + let e = Sha256::new() + .chain_bigint(&BigInt::from_bytes(statement.G.to_bytes(true).as_ref())) + .chain_bigint(&BigInt::from_bytes(statement.Q.to_bytes(true).as_ref())) + .chain_bigint(&statement.ciphertext) + .chain_bigint(&self.z) + .chain_bigint(&BigInt::from_bytes(self.u1.to_bytes(true).as_ref())) + .chain_bigint(&self.u2) + .chain_bigint(&self.u3) + .result_bigint(); + + let g_s1 = statement.G.clone() * &Scalar::::from(&self.s1); + let e_fe_neg: Scalar = + Scalar::::from(&(Scalar::::group_order() - &e)); + let y_minus_e = &statement.Q * &e_fe_neg; + let u1_test = g_s1 + y_minus_e; + + let u2_test_tmp = commitment_unknown_order( + &(&statement.ek.n + BigInt::one()), + &self.s2, + &statement.ek.nn, + &self.s1, + &statement.ek.n, + ); + let u2_test = commitment_unknown_order( + &u2_test_tmp, + &statement.ciphertext, + &statement.ek.nn, + &BigInt::one(), + &(-&e), + ); + + let u3_test_tmp = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &self.s1, + &self.s3, + ); + let u3_test = commitment_unknown_order( + &u3_test_tmp, + &self.z, + &statement.N_tilde, + &BigInt::one(), + &(-&e), + ); + + if self.u1 == u1_test && self.u2 == u2_test && self.u3 == u3_test { + Ok(()) + } else { + Err(ZkPdlWithSlackError::Verify) + } + } +} + +pub fn commitment_unknown_order( + h1: &BigInt, + h2: &BigInt, + N_tilde: &BigInt, + x: &BigInt, + r: &BigInt, +) -> BigInt { + let h1_x = BigInt::mod_pow(h1, x, N_tilde); + let h2_r = { + if r < &BigInt::zero() { + let h2_inv = BigInt::mod_inv(h2, N_tilde).unwrap(); + BigInt::mod_pow(&h2_inv, &(-r), N_tilde) + } else { + BigInt::mod_pow(h2, r, N_tilde) + } + }; + BigInt::mod_mul(&h1_x, &h2_r, N_tilde) +} + +#[cfg(test)] +mod test; diff --git a/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs new file mode 100644 index 00000000..fabcda4b --- /dev/null +++ b/multi-party-ecdsa/src/utilities/zk_pdl_with_slack/test.rs @@ -0,0 +1,129 @@ +#![allow(non_snake_case)] +use crate::utilities::zk_pdl_with_slack::*; +use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; +use curv::BigInt; +use paillier::core::Randomness; +use paillier::traits::{EncryptWithChosenRandomness, KeyGeneration}; +use paillier::Paillier; +use paillier::RawPlaintext; +use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; + +#[test] +fn test_zk_pdl_with_slack() { + // N_tilde, h1, h2 generation + let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); + // note: safe primes should be used: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&phi); + let S = BigInt::from(2).pow(256_u32); + let xhi = BigInt::sample_below(&S); + let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); + let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); + let statement = DLogStatement { + N: ek_tilde.n.clone(), + g: h1.clone(), + ni: h2.clone(), + }; + + let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); + + // generate the scalar secret and Paillier encrypt it + let (ek, _dk) = Paillier::keypair().keys(); + // note: safe primes should be used here as well: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let randomness = Randomness::sample(&ek); + let x = Scalar::::random(); + + let Q = Point::generator() * &x; + + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint()), + &randomness, + ) + .0 + .into_owned(); + + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: c, + ek, + Q, + G: Point::generator().to_point(), + h1, + h2, + N_tilde: ek_tilde.n, + }; + + let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; + + let proof = PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); + // verify h1,h2, N_tilde + let setup_result = composite_dlog_proof.verify(&statement); + assert!(setup_result.is_ok()); + let result = proof.verify(&pdl_w_slack_statement); + assert!(result.is_ok()); +} + +#[test] +#[should_panic] +fn test_zk_pdl_with_slack_soundness() { + // N_tilde, h1, h2 generation + let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); + // note: safe primes should be used: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&phi); + let S = BigInt::from(2).pow(256_u32); + let xhi = BigInt::sample_below(&S); + let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); + let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); + let statement = DLogStatement { + N: ek_tilde.n.clone(), + g: h1.clone(), + ni: h2.clone(), + }; + + let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); + + // generate the scalar secret and Paillier encrypt it + let (ek, _dk) = Paillier::keypair().keys(); + // note: safe primes should be used here as well: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let randomness = Randomness::sample(&ek); + let x = Scalar::::random(); + + let Q = Point::generator() * &x; + + // here we encrypt x + 1 instead of x: + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint() + BigInt::one()), + &randomness, + ) + .0 + .into_owned(); + + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: c, + ek, + Q, + G: Point::generator().to_point(), + h1, + h2, + N_tilde: ek_tilde.n, + }; + + let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; + + let proof = PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); + // verify h1,h2, N_tilde + let setup_result = composite_dlog_proof.verify(&statement); + assert!(setup_result.is_ok()); + let result = proof.verify(&pdl_w_slack_statement); + assert!(result.is_ok()); +} From 4842bfe8ad407c03c403e43f784a418d38fa3d9b Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 09:11:04 +0800 Subject: [PATCH 02/41] Add fs-dkr as workspace member. Switch to stable channel for workspace.dependencies. --- Cargo.lock | 1092 ++++++++++++++++++++- Cargo.toml | 14 +- fs-dkr/.github/workflows/pull_request.yml | 65 ++ fs-dkr/.gitignore | 10 + fs-dkr/Cargo.toml | 40 + fs-dkr/LICENSE | 674 +++++++++++++ fs-dkr/README.md | 103 ++ fs-dkr/src/add_party_message.rs | 297 ++++++ fs-dkr/src/error.rs | 60 ++ fs-dkr/src/lib.rs | 27 + fs-dkr/src/range_proofs.rs | 746 ++++++++++++++ fs-dkr/src/refresh_message.rs | 475 +++++++++ fs-dkr/src/ring_pedersen_proof.rs | 179 ++++ fs-dkr/src/test.rs | 419 ++++++++ fs-dkr/src/zk_pdl_with_slack.rs | 332 +++++++ rust-toolchain.toml | 3 +- 16 files changed, 4485 insertions(+), 51 deletions(-) create mode 100644 fs-dkr/.github/workflows/pull_request.yml create mode 100644 fs-dkr/.gitignore create mode 100644 fs-dkr/Cargo.toml create mode 100644 fs-dkr/LICENSE create mode 100644 fs-dkr/README.md create mode 100644 fs-dkr/src/add_party_message.rs create mode 100644 fs-dkr/src/error.rs create mode 100644 fs-dkr/src/lib.rs create mode 100644 fs-dkr/src/range_proofs.rs create mode 100644 fs-dkr/src/refresh_message.rs create mode 100644 fs-dkr/src/ring_pedersen_proof.rs create mode 100644 fs-dkr/src/test.rs create mode 100644 fs-dkr/src/zk_pdl_with_slack.rs diff --git a/Cargo.lock b/Cargo.lock index 376d42ca..d06a6d75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array 0.14.6", +] + [[package]] name = "aead" version = "0.4.3" @@ -36,6 +45,17 @@ dependencies = [ "generic-array 0.14.6", ] +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher 0.2.5", +] + [[package]] name = "aes" version = "0.7.5" @@ -59,6 +79,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" +dependencies = [ + "aead 0.3.2", + "aes 0.6.0", + "cipher 0.2.5", + "ctr 0.6.0", + "ghash 0.3.1", + "subtle", +] + [[package]] name = "aes-gcm" version = "0.9.4" @@ -87,6 +121,26 @@ dependencies = [ "subtle", ] +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher 0.2.5", + "opaque-debug 0.3.0", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher 0.2.5", + "opaque-debug 0.3.0", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -102,6 +156,115 @@ version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1da3ae8dabd9c00f453a329dfe1fb28da3c0a72e2478cdcd93171740c20499" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg 1.1.0", + "cfg-if 1.0.0", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix", + "slab", + "socket2", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-sse" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e6fa871e4334a622afd6bb2f611635e8083a6f5e2936c0f90f37c7ef9856298" +dependencies = [ + "async-channel", + "futures-lite", + "http-types", + "log", + "memchr", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils 0.8.12", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-stream" version = "0.3.3" @@ -120,9 +283,15 @@ checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", ] +[[package]] +name = "async-task" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9441c6b2fe128a7c2bf680a44c34d0df31ce09e5b7e401fcca3faa483dbc921" + [[package]] name = "async-trait" version = "0.1.58" @@ -131,7 +300,7 @@ checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -143,13 +312,19 @@ dependencies = [ "autocfg 1.1.0", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi 0.3.9", ] @@ -184,6 +359,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + [[package]] name = "base16ct" version = "0.1.1" @@ -296,6 +477,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +[[package]] +name = "blocking" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c4ef1f913d78636d78d538eec1f18de81e481f44b1be0a81060090530846e1" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + [[package]] name = "bstr" version = "0.2.17" @@ -351,6 +548,12 @@ dependencies = [ "iovec", ] +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + [[package]] name = "bytes" version = "1.2.1" @@ -407,12 +610,12 @@ dependencies = [ "criterion", "curv-kzen", "digest 0.9.0", - "fs-dkr", + "fs-dkr 0.1.0 (git+https://github.com/webb-tools/fs-dkr)", "futures 0.3.25", "generic-array 0.14.6", "hex", "kzen-paillier", - "multi-party-ecdsa", + "multi-party-ecdsa 0.8.2", "rand 0.8.5", "rand_chacha 0.3.1", "reqwest", @@ -430,6 +633,15 @@ dependencies = [ "zk-paillier", ] +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array 0.14.6", +] + [[package]] name = "cipher" version = "0.3.0" @@ -473,12 +685,27 @@ dependencies = [ "bitflags", ] +[[package]] +name = "concurrent-queue" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +dependencies = [ + "crossbeam-utils 0.8.12", +] + [[package]] name = "const-oid" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +[[package]] +name = "const_fn" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" + [[package]] name = "cookie" version = "0.12.0" @@ -489,6 +716,23 @@ dependencies = [ "url 1.7.2", ] +[[package]] +name = "cookie" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" +dependencies = [ + "aes-gcm 0.8.0", + "base64 0.13.1", + "hkdf 0.10.0", + "hmac 0.10.1", + "percent-encoding 2.2.0", + "rand 0.8.5", + "sha2 0.9.9", + "time 0.2.27", + "version_check", +] + [[package]] name = "cookie" version = "0.16.1" @@ -497,7 +741,7 @@ checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917" dependencies = [ "aes-gcm 0.10.1", "base64 0.13.1", - "hkdf", + "hkdf 0.12.3", "hmac 0.12.1", "percent-encoding 2.2.0", "rand 0.8.5", @@ -534,6 +778,12 @@ dependencies = [ "libc", ] +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + [[package]] name = "crc32fast" version = "1.3.2" @@ -693,6 +943,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array 0.14.6", + "subtle", +] + [[package]] name = "crypto-mac" version = "0.11.1" @@ -731,6 +991,25 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.103", +] + +[[package]] +name = "ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +dependencies = [ + "cipher 0.2.5", +] + [[package]] name = "ctr" version = "0.8.0" @@ -749,6 +1028,37 @@ dependencies = [ "cipher 0.4.3", ] +[[package]] +name = "curl" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "winapi 0.3.9", +] + +[[package]] +name = "curl-sys" +version = "0.4.66+curl-8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.48.0", +] + [[package]] name = "curv-kzen" version = "0.10.0" @@ -815,7 +1125,7 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -848,7 +1158,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -880,6 +1190,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "dtoa" version = "0.4.8" @@ -933,6 +1249,33 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "errno" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "failure" version = "0.1.8" @@ -951,7 +1294,7 @@ checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", "synstructure", ] @@ -970,6 +1313,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "ff" version = "0.12.1" @@ -1003,7 +1352,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -1030,6 +1379,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bebadab126f8120d410b677ed95eee4ba6eb7c6dd8e34a5ec88a08050e26132" +dependencies = [ + "futures-core", + "futures-sink", + "spinning_top", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1045,6 +1405,23 @@ dependencies = [ "percent-encoding 2.2.0", ] +[[package]] +name = "fs-dkr" +version = "0.1.0" +dependencies = [ + "bitvec", + "curv-kzen", + "kzen-paillier", + "multi-party-ecdsa 0.8.2", + "round-based 0.1.7 (git+https://github.com/webb-tools/round-based-protocol)", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror", + "zeroize", + "zk-paillier", +] + [[package]] name = "fs-dkr" version = "0.1.0" @@ -1053,7 +1430,7 @@ dependencies = [ "bitvec", "curv-kzen", "kzen-paillier", - "multi-party-ecdsa", + "multi-party-ecdsa 0.8.2 (git+https://github.com/webb-tools/multi-party-ecdsa)", "round-based 0.1.7 (git+https://github.com/webb-tools/round-based-protocol)", "serde", "serde_derive", @@ -1151,9 +1528,24 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.8.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] [[package]] name = "futures-macro" @@ -1163,7 +1555,7 @@ checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -1250,6 +1642,16 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "ghash" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" +dependencies = [ + "opaque-debug 0.3.0", + "polyval 0.4.5", +] + [[package]] name = "ghash" version = "0.4.4" @@ -1282,6 +1684,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "group" version = "0.12.1" @@ -1341,6 +1755,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + [[package]] name = "hex" version = "0.4.3" @@ -1350,6 +1770,16 @@ dependencies = [ "serde", ] +[[package]] +name = "hkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +dependencies = [ + "digest 0.9.0", + "hmac 0.10.1", +] + [[package]] name = "hkdf" version = "0.12.3" @@ -1359,13 +1789,23 @@ dependencies = [ "hmac 0.12.1", ] +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac 0.10.1", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ - "crypto-mac", + "crypto-mac 0.11.1", "digest 0.9.0", ] @@ -1423,6 +1863,42 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-client" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1947510dc91e2bf586ea5ffb412caad7673264e14bb39fb9078da114a94ce1a5" +dependencies = [ + "async-std", + "async-trait", + "cfg-if 1.0.0", + "http-types", + "isahc", + "log", +] + +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel", + "async-std", + "base64 0.13.1", + "cookie 0.14.4", + "futures-lite", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded 0.7.1", + "url 2.3.1", +] + [[package]] name = "httparse" version = "1.8.0" @@ -1531,6 +2007,12 @@ dependencies = [ "serde", ] +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + [[package]] name = "inlinable_string" version = "0.1.15" @@ -1555,6 +2037,17 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.3", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "iovec" version = "0.1.4" @@ -1564,6 +2057,29 @@ dependencies = [ "libc", ] +[[package]] +name = "isahc" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2948a0ce43e2c2ef11d7edf6816508998d99e13badd1150be0914205df9388a" +dependencies = [ + "bytes 0.5.6", + "crossbeam-utils 0.8.12", + "curl", + "curl-sys", + "flume", + "futures-lite", + "http 0.2.8", + "log", + "once_cell", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url 2.3.1", + "waker-fn", +] + [[package]] name = "itertools" version = "0.7.11" @@ -1619,6 +2135,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "kzen-paillier" version = "0.4.3" @@ -1638,9 +2163,37 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "libnghttp2-sys" +version = "0.1.8+1.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fae956c192dadcdb5dace96db71fa0b827333cce7c7b38dc71446f024d8a340" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libz-sys" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" @@ -1668,6 +2221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if 1.0.0", + "value-bag", ] [[package]] @@ -1792,7 +2346,7 @@ dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1827,6 +2381,39 @@ dependencies = [ "version_check", ] +[[package]] +name = "multi-party-ecdsa" +version = "0.8.2" +dependencies = [ + "aes-gcm 0.9.4", + "anyhow", + "async-sse", + "centipede", + "criterion", + "curv-kzen", + "derivative", + "futures 0.3.25", + "hex", + "kzen-paillier", + "log", + "rand 0.8.5", + "reqwest", + "rocket", + "round-based 0.1.7 (git+https://github.com/webb-tools/round-based-protocol)", + "secp256k1", + "serde", + "serde_json", + "sha2 0.9.9", + "structopt", + "subtle", + "surf", + "thiserror", + "tokio 1.21.2", + "uuid 0.8.2", + "zeroize", + "zk-paillier", +] + [[package]] name = "multi-party-ecdsa" version = "0.8.2" @@ -1915,7 +2502,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", ] @@ -1952,6 +2539,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -1984,6 +2589,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "parking" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" + [[package]] name = "parking_lot" version = "0.9.0" @@ -2030,7 +2641,7 @@ dependencies = [ "libc", "redox_syscall 0.2.16", "smallvec 1.10.0", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -2053,7 +2664,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -2066,7 +2677,27 @@ checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" name = "percent-encoding" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] [[package]] name = "pin-project-lite" @@ -2080,6 +2711,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.9.0" @@ -2090,6 +2732,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + [[package]] name = "plotters" version = "0.3.4" @@ -2118,6 +2766,33 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg 1.1.0", + "bitflags", + "cfg-if 1.0.0", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polyval" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +dependencies = [ + "cpuid-bool", + "opaque-debug 0.3.0", + "universal-hash 0.4.1", +] + [[package]] name = "polyval" version = "0.5.3" @@ -2157,7 +2832,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.103", "version_check", ] @@ -2172,11 +2847,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -2189,7 +2870,7 @@ checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", "version_check", "yansi", ] @@ -2206,9 +2887,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.21" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -2483,7 +3164,7 @@ checksum = "abfa8511e9e94fd3de6585a3d3cd00e01ed556dc9814829280af0e8dc72a8f36" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -2539,7 +3220,7 @@ dependencies = [ "mime_guess", "serde", "serde_json", - "serde_urlencoded", + "serde_urlencoded 0.5.5", "time 0.1.44", "tokio 0.1.22", "tokio-executor", @@ -2613,7 +3294,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn", + "syn 1.0.103", "unicode-xid", ] @@ -2697,6 +3378,20 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.37.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4279d76516df406a8bd37e7dff53fd37d1a093f997a3c34a5c21658c126db06d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + [[package]] name = "rustversion" version = "1.0.9" @@ -2718,6 +3413,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2815,7 +3519,7 @@ checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -2829,6 +3533,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding 2.2.0", + "serde", + "thiserror", +] + [[package]] name = "serde_urlencoded" version = "0.5.5" @@ -2841,6 +3556,33 @@ dependencies = [ "url 1.7.2", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.4", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.8.2" @@ -2926,6 +3668,17 @@ dependencies = [ "autocfg 1.1.0", ] +[[package]] +name = "sluice" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel", + "futures-core", + "futures-io", +] + [[package]] name = "smallvec" version = "0.6.14" @@ -2957,6 +3710,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +[[package]] +name = "spinning_top" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9eb1a2f4c41445a3a0ff9abc5221c5fcd28e1f13cd7c0397706f9ac938ddb0" +dependencies = [ + "lock_api 0.4.9", +] + [[package]] name = "spki" version = "0.6.0" @@ -2976,6 +3738,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + [[package]] name = "state" version = "0.5.3" @@ -2985,6 +3756,55 @@ dependencies = [ "loom", ] +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn 1.0.103", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn 1.0.103", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + [[package]] name = "string" version = "0.2.1" @@ -3021,7 +3841,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -3030,6 +3850,29 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +[[package]] +name = "surf" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7" +dependencies = [ + "async-std", + "async-trait", + "cfg-if 1.0.0", + "encoding_rs", + "futures-util", + "getrandom 0.2.8", + "http-client", + "http-types", + "log", + "mime_guess", + "once_cell", + "pin-project-lite", + "serde", + "serde_json", + "web-sys", +] + [[package]] name = "syn" version = "1.0.103" @@ -3041,6 +3884,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -3049,7 +3903,7 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", "unicode-xid", ] @@ -3066,7 +3920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if 1.0.0", - "fastrand", + "fastrand 1.8.0", "libc", "redox_syscall 0.2.16", "remove_dir_all", @@ -3099,7 +3953,7 @@ checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -3122,6 +3976,21 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros 0.1.1", + "version_check", + "winapi 0.3.9", +] + [[package]] name = "time" version = "0.3.17" @@ -3131,7 +4000,7 @@ dependencies = [ "itoa 1.0.4", "serde", "time-core", - "time-macros", + "time-macros 0.2.6", ] [[package]] @@ -3140,6 +4009,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + [[package]] name = "time-macros" version = "0.2.6" @@ -3149,6 +4028,19 @@ dependencies = [ "time-core", ] +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn 1.0.103", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -3262,7 +4154,7 @@ checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -3383,6 +4275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3396,7 +4289,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", ] [[package]] @@ -3409,6 +4302,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + [[package]] name = "tracing-log" version = "0.1.3" @@ -3566,6 +4469,7 @@ dependencies = [ "form_urlencoded", "idna 0.3.0", "percent-encoding 2.2.0", + "serde", ] [[package]] @@ -3592,6 +4496,22 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "value-bag" +version = "1.0.0-alpha.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" +dependencies = [ + "ctor", + "version_check", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -3604,6 +4524,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + [[package]] name = "walkdir" version = "2.3.2" @@ -3675,10 +4601,22 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.103", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.83" @@ -3697,7 +4635,7 @@ checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3780,21 +4718,51 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.42.0", "windows_aarch64_msvc 0.42.0", "windows_i686_gnu 0.42.0", "windows_i686_msvc 0.42.0", "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.42.0", "windows_x86_64_msvc 0.42.0", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_msvc" version = "0.32.0" @@ -3807,6 +4775,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_i686_gnu" version = "0.32.0" @@ -3819,6 +4793,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_msvc" version = "0.32.0" @@ -3831,6 +4811,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_x86_64_gnu" version = "0.32.0" @@ -3843,12 +4829,24 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_msvc" version = "0.32.0" @@ -3861,6 +4859,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "winreg" version = "0.6.2" @@ -3912,7 +4916,7 @@ checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.103", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index 7f9298b0..016203ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,17 @@ authors = [ [workspace] members = [ - "multi-party-ecdsa" + "multi-party-ecdsa", + "fs-dkr", ] +[workspace.dependencies.multi-party-ecdsa] +version = "0.8" +path = "multi-party-ecdsa" +default-features = false + [dependencies] +multi-party-ecdsa.workspace = true bincode = "1.3.3" serde = { version = "1.0", features = ["derive"] } zeroize = "1" @@ -33,11 +40,6 @@ version = "0.4.3" package = "kzen-paillier" default-features = false -[dependencies.multi-party-ecdsa] -version = "0.8" -path = "multi-party-ecdsa" -default-features = false - [dev-dependencies] criterion = "0.3" aes-gcm = "0.9.4" diff --git a/fs-dkr/.github/workflows/pull_request.yml b/fs-dkr/.github/workflows/pull_request.yml new file mode 100644 index 00000000..bf45f3a6 --- /dev/null +++ b/fs-dkr/.github/workflows/pull_request.yml @@ -0,0 +1,65 @@ +name: FS-DKR Test Suite + +on: [pull_request, push] + +jobs: + check: + name: Cargo Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + + test: + name: Test Suite + runs-on: ${{ matrix.os }} + strategy: + matrix: + # TODO: add windows-latest after solving the gmp linking issue + os: [ macos-latest, ubuntu-latest] + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: test + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - run: rustup component add rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy_check: + name: Clippy Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + - uses: actions-rs/clippy@master + with: + args: --all-features --all-targets \ No newline at end of file diff --git a/fs-dkr/.gitignore b/fs-dkr/.gitignore new file mode 100644 index 00000000..088ba6ba --- /dev/null +++ b/fs-dkr/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/fs-dkr/Cargo.toml b/fs-dkr/Cargo.toml new file mode 100644 index 00000000..113dedcc --- /dev/null +++ b/fs-dkr/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "fs-dkr" +version = "0.1.0" +authors = [ + "Omer Shlomovits ", + "Tudor Cebere ", + "Drew Stone ", +] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies.paillier] +version = "0.4.3" +package = "kzen-paillier" +default-features = false + +[dependencies.multi-party-ecdsa] +workspace = true +default-features = false + +[dependencies.bitvec] +version = "1" +default-features = false +features = ["atomic", "alloc"] + +[dependencies] +zk-paillier = { version = "0.4.4", default-features = false } +curv = { package = "curv-kzen", version = "0.10", default-features = false } +serde = { version = "1.0", features = ["derive"] } +serde_derive = "1.0" +zeroize = "1" +round-based = { git = "https://github.com/webb-tools/round-based-protocol", version = "0.1.4", features = ["dev"] } +thiserror = "1.0.26" +sha2 = "0.9" + +[features] +default = ["rust-gmp-kzen"] +rust-gmp-kzen = ["curv/rust-gmp-kzen"] +num-bigint = ["curv/num-bigint"] \ No newline at end of file diff --git a/fs-dkr/LICENSE b/fs-dkr/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/fs-dkr/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/fs-dkr/README.md b/fs-dkr/README.md new file mode 100644 index 00000000..68d3b260 --- /dev/null +++ b/fs-dkr/README.md @@ -0,0 +1,103 @@ +# FS-DKR: One Round Distributed Key Rotation + + +## Intro +In this note we aim to re-purpose the [Fouque-Stern](https://hal.inria.fr/inria-00565274/document) Distributed Key Generation (DKG) to support a secure Distributed Key Refresh (DKR). As we claim, FS-DKR is well suited for rotation of [threshold ECDSA](https://eprint.iacr.org/2020/540.pdf) keys. + + + +## Background +The FS-DKG protocol is a one round DKG based on Publicly Verifiable Secret Sharing (PVSS) and the [Paillier cryptosystem](https://en.wikipedia.org/wiki/Paillier_cryptosystem). There are two major security shortcomings to FS-DKG: +1. It introduces a factoring assumptions (DCRA) +2. it is insecure against rushing adversary + +Rushing adversary is a common assumption in Multiparty Computation (MPC). In FS-DKG, an adversary waiting to receive messages from all other parties will be able to decide on the final public key. In the worst case it can lead to a rogue-key attack, giving full control of the secret key to the attacker. This is the main reason, in our opinion, why FS-DKG, altough with prominent features, was over-looked for the past 20 years. +in this write-up we show how by adjusting FS-DKG to key rotation for threshold ecdsa the above shortcomings are avoided. + +## Our Model +We use standard proactive security assumptions. The protocol will be run by $n$ parties. We assume honest majority, that is, number of corruptions is $t<=n/2$. The adversary is malicious, and rushing. +For communication, the parties have access to a broadcast channel (can be implemented via a bulletin board). +For threshold ECDSA, we focus on [GG20](https://eprint.iacr.org/2020/540.pdf) protocol, currently considered state of the art and most widely deployed threshold ecdsa scheme (e.g. [multi-party-ecdsa](https://github.com/ZenGo-X/multi-party-ecdsa), [tss-lib](https://github.com/binance-chain/tss-lib)). + +## How To Use +### Refresh a Key +Each party calls `RefreshMessage::distribute(key)` on their `LocalKey` and broadcasts the `RefreshMessage` while saving their new `DecryptionKey`.
+After recieving all the refresh messages each party calls `RefreshMessage::collect(..)` with a vector of all the refresh messages, a mutable reference to their own key, and their new `DecryptionKey`, This will validate all the refresh messages, and if all the proofs are correct it will update the local key to contain the new decryption keys of all the parties. + +Example: +```rust +// All parties should run this +let mut party_i_key: LocalKey<_>; +let (party_i_refresh_message, party_i_new_decryption_key) = RefreshMessage::distribute(party_i_key); +broadcast(party_i_refresh_message); +let vec_refresh_messages = recv_from_broadcast(); +RefreshMessage::collect(&vec_refresh_messages, &mut party_i_key, party_i_new_decryption_key, &[])?; +``` + +### Replacing a party +Each party that wants to join first generates a `JoinMessage` via `JoinMessage::distribute()` and broadcasts it to the current parties.
+The existing parties choose the index(who are they replacing) for the joining party. +Note that this part is delicate and needs to happen outside of the library because it requires some kind of mutual agreement, and you cannot trust the new party to communicate which party are they replacing.
+After agreeing on the index each party modifies the join message to contain the index `join_message.party_index = Some(index)`.
+Each existing party calls `RefreshMessage::replace(join_message, local_key)` with the join message and its own local key, this returns a refresh message and a new decryption key, just like in a Key Refresh, and they all broadcast the `RefreshMessage`.
+Each existing party recieves all the broadcasted refresh messages and calls `RefreshMessage::collect(..)` with a vector of all the refresh messages, a mutable reference to their own key, the new `DecryptionKey`, and a slice of all the join messages(`JoinMessage`)
+This will validate both the refresh messages and the join messages and if all the proofs are correct it will update the local key both as a refresh(new decryption keys) and replace the existing parties with the new ones.
+The new party calls `join_message.collect(..)` with the broadcasted `RefreshMessage` of the existing parties and all the join messages which returns a new `LocalKey` for the new party. + +Example: +```rust +// PoV of the new party +let (join_message, new_party_decryption_key) = JoinMessage::distribute(); +broadcast(join_message); +let new_party_index = recv_broadcast(); +let vec_refresh_messages = recv_from_broadcast(); +let new_party_local_key = join_message.collect(&vec_refresh_messages, new_party_decryption_key, &[])?; + +// PoV of the other parties +let mut party_i_key: LocalKey<_>; +let mut join_message = recv_broadcast(); +let new_index = decide_who_to_replace(&join_message); +broadcast(new_index); +assert!(recv_from_broadcast().iter().all(|index| index == new_index)); +join_message.party_index = Some(new_index); +let (party_i_refresh_message, party_i_new_decryption_key) = RefreshMessage::replace(join_message, party_i_key)?; +broadcast(party_i_refresh_message); +let vec_refresh_messages = recv_from_broadcast(); +RefreshMessage::collect(&vec_refresh_messages, &mut party_i_key, party_i_new_decryption_key, &[join_message])?; +``` + +## High-level Description of FS-DKG +Here we give a short description of the FS-DKG protocol. +FS-DKG works in one round. This round includes a single broadcast message from each party $P_j$. For Setup, we assume every party in the system has a public/private key pair for Paillier encryption scheme. +At first, $P_j$ picks a random secret $s$ and secret shares it. $P_j$ publishes one set of size $t$ of commitment points $\textbf{A}$ corresponding to the polynomial coefficients: $A_i = a_iG$, and one set of $n$ commitment points $\textbf{S}$ corresponding to $n$ points on the polynomial: $S_i = \sigma_i G$. The points on the polynomial are also encrypted using the paillier keys of the receiving parties: $Enc_{pk_i}(\sigma_i)$. Finally, $P_j$ computes zero knowledge proofs $\pi_i$ to show that the paillier encryption for $P_i$ encrypts the same value commited in $S_i$. The ZK proof is a sigma protocol (can be made non-interactive using Fiat-Shamir) given in the original FS paper under the name proof of fairness. We [implemented it](https://github.com/ZenGo-X/fs-dkr/blob/main/src/proof_of_fairness.rs) under the same name. + +Verification proceeds as follows. Each party $P_j$ verifies: +1. all broadcasted proofs of fairness +2. all secret sharing schemes - computing the polynomial points "at the exponent" + +The parties define the set $\mathcal{Q}$ to be the set of the first $t+1$ parties for which all checks passed. we now show a simple optimization on how each party computes its local secret key: Each party [maps its encrypted shares](https://github.com/ZenGo-X/fs-dkr/blob/main/src/lib.rs#L181) from $\{t,n\}$ to $\{\mathcal{Q},\mathcal{Q}\}$. It then homomorphically adds all the paillier ciphertext (which is an additive homomorphic scheme) and decrypts to get the local secret key. + + +## Adjusting FS-DKG to DKR and threshold ECDSA +We will now highlight the adjustments required for FS-DKR. +In a key refresh protocol the parties start with their inputs equal to the outputs of a DKG done in the past or the output of previous DKR. Meaning, as opposed to FS-DKG protocol in which the inputs are pseudorandom such that the attacker can bias the output, for example in a rushing adversary attack, FS-DKR avoids this potential attack on FS-DKG because of the added restriction over the inputs of the attacker. Concretely, in the case the parties must reshare their DKG/DKR output secret share, all other parties already know a public commitment to the attacker secret share and can check for it. +Recall that FS-DKG is secure assuming Paillier is secure (what we called DCRA assumption). Moreover, we assumed a setup phase in which all parties generate paillier keys and share them. This fits well with threshold ECDSA: First, GG20 already requires us to assume Paillier security, therefore in this particular case, no new assumption is needed. The setup phase actually happens as part of GG20 DKG. We will use this to our advantage, running the FS-DKR using the GG20-DKG paillier keys. Obviously because we need to refresh the paillier keys as well we will also add a step to FS-DKR to generate new paillier keys and prove they were generated correctly. This is a standard proof, that can be made non-interactive. See the [zk-paillier lib](https://github.com/ZenGo-X/zk-paillier/blob/master/src/zkproofs/correct_key_ni.rs) for an implementation. + +**Adding/Removing parties:** There is a clear distinction between parties with secret shares (”Senders”) and new parties (”Receivers”). The FS-DKR protocol therefore supports adding and removing parties in a natural way: Define $\mathcal{J}>t+1$ the subset of parties participating in the protocol. To remove an existing party $P_i$, other parties exclude it from the subset $\mathcal{J}$. To add a new party, we assume the parties in $\mathcal{J}$ are aware of the new party' paillier key. In that case, the parties in $\mathcal{J}$ assign an index $i$ to the new party and broadcast the PVSS messages to it. Removal of a party is simply done by not broadcasting the encrypted messages to it. If enough parties decide on that for a party index, they will not be able to reconstruct a rotated key. + +**Identifiable Abort:** A nice property of FS-DKR is that if a party misbehaves all honest parties learn about it. This is due to the nature of PVSS used in the protocol. As GG20, our reference threshold ECDSA protocol, also have this property, it is important that identifiable abort can be guaranteed throughout the DKR as well. + +For completeness, Below is the FS-DKR protocol, written as FS-DKG with changes in red for DKR. ![](https://i.imgur.com/V50DfBz.png) +The protocol is implemented in the [ZenGo-X/fs-dkr repo](https://github.com/ZenGo-X/fs-dkr) (warning, the code is not audited yet). + + +## Related Work +Our main requirement from FS-DKR is minimal round-count. In FS-DKR the parties can pre-process all the data they need to send. Our main bottleneck is $\mathcal{O}(n^2)$ communication, which seems a standard cost in our context: It is the same asymptotic complexity as we have in GG20-DKG and GG20-Signing. + +In this section we focus on alternative protocols for DKR. Three recent results come to mind. The first one, [CGGMP20](https://eprint.iacr.org/2021/060.pdf), is another threshold ECDSA protocol with a companion refresh protocol, see figure 6 in the paper. Their protocol has the most resemblance to FS-DKR, with few notable differences. First, while FS-DKR is publicly verifiable, CGGMP20-DKR current [version](https://eprint.iacr.org/2021/060/20210118:082423) suffers from a technichal issue with its Identifiable Abort (acknowledged by the authors). Second, the paillier keys used in the CGGMP20-DKR are the new ones, while in FS-DKR, we use the old ones, already known to all, which helps us save a round of communication. Finally, CGMMP20-DKR key refresh is done by adding shares of zero while in FS-DKR we re-share existing shares. Overall we treat the similarities between the protocols as a positive signal of validation for FS-DKR. +A second protocol, by [Gurkan et. al.](https://eprint.iacr.org/2021/005), uses gossip for aggregating transcripts from the parties. However, their DKG is generating group elements secret shares and we need field elements secret shares for our threshold ECDSA. +The third relevant work is Jens Groth' [Non interactive DKG and DKR](https://eprint.iacr.org/2021/339). There, instead of paillier encryption, they use El-Gamal based encryption scheme that offers forward security. Their DKR makes the assumption that the El-Gamal decryption keys are long-term and not rotated. This assumption seems crucial for the Groth-DKG construction. In our context it means that we need to let the parties generate, store and use a new set of keypair,in addition to the Paillier keypair, and that this new keypair poses a security risk against the classical mobile adversary, which our model does not allow. As opposed to Groth-DKR, FS-DKR is reusing the existing paillier keypair and rotate it as well. In terms of efficiency - there is no complexity analysis given in the paper, however, from inspection we estimate the asymptotic complexity is comparable to FS-DKR (quadratic in the number of parties). + +## Acknowledgments +We thank Claudio Orlandi, Kobi Gurkan and Nikolaos Makriyannis for reviewing the note + diff --git a/fs-dkr/src/add_party_message.rs b/fs-dkr/src/add_party_message.rs new file mode 100644 index 00000000..7151c4df --- /dev/null +++ b/fs-dkr/src/add_party_message.rs @@ -0,0 +1,297 @@ +//! Message definitions for new parties that can join the protocol +//! Key points about a new party joining the refresh protocol: +//! * A new party wants to join, broadcasting a paillier ek, correctness of the ek generation, +//! dlog statements and dlog proofs. +//! * All the existing parties receives the join message. We assume for now that everyone accepts +//! the new party. All parties pick an index and add the new ek to their LocalKey at the given index. +//! * The party index of the new party is transmitted back to the joining party offchannel (it's +//! public information). +//! * All the existing parties enter the distribute phase, in which they start refreshing their +//! existing keys taking into the account the join messages that they received. +//! ** All parties (including new ones) collect the refresh messages and the join messages. + +use crate::error::{FsDkrError, FsDkrResult}; +use crate::refresh_message::RefreshMessage; +use curv::arithmetic::{BasicOps, Modulo, One, Samplable, Zero}; +use curv::cryptographic_primitives::hashing::Digest; +use curv::cryptographic_primitives::secret_sharing::feldman_vss::{ + ShamirSecretSharing, VerifiableSS, +}; +use curv::elliptic::curves::{Curve, Point, Scalar}; +use curv::BigInt; +use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::party_i::Keys; +use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::party_i::SharedKeys; +use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::keygen::LocalKey; +use paillier::{Decrypt, EncryptionKey, KeyGeneration, Paillier}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt::Debug; +use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement, NiCorrectKeyProof}; + +use crate::ring_pedersen_proof::{RingPedersenProof, RingPedersenStatement}; + +/// Message used by new parties to join the protocol. +#[derive(Clone, Deserialize, Serialize, Debug)] +#[serde(bound = "E: Curve, H: Digest + Clone")] +pub struct JoinMessage { + pub(crate) ek: EncryptionKey, + pub(crate) dk_correctness_proof: NiCorrectKeyProof, + pub(crate) party_index: Option, + pub(crate) dlog_statement: DLogStatement, + pub(crate) composite_dlog_proof_base_h1: CompositeDLogProof, + pub(crate) composite_dlog_proof_base_h2: CompositeDLogProof, + pub(crate) ring_pedersen_statement: RingPedersenStatement, + pub(crate) ring_pedersen_proof: RingPedersenProof, +} + +/// Generates the parameters needed for the h1_h2_N_tilde_vec. These parameters can be seen as +/// environment variables for each party that they agree on. In this case, each new party generates +/// it's own DlogStatements and submits it's proofs +fn generate_h1_h2_n_tilde() -> (BigInt, BigInt, BigInt, BigInt, BigInt) { + let (ek_tilde, dk_tilde) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&ek_tilde.n); + let (mut xhi, mut xhi_inv) = loop { + let xhi_ = BigInt::sample_below(&phi); + match BigInt::mod_inv(&xhi_, &phi) { + Some(inv) => break (xhi_, inv), + None => continue, + } + }; + let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); + xhi = BigInt::sub(&phi, &xhi); + xhi_inv = BigInt::sub(&phi, &xhi_inv); + (ek_tilde.n, h1, h2, xhi, xhi_inv) +} + +/// Generates the DlogStatement and CompositeProofs using the parameters generated by [generate_h1_h2_n_tilde] +fn generate_dlog_statement_proofs() -> (DLogStatement, CompositeDLogProof, CompositeDLogProof) { + let (n_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_n_tilde(); + + let dlog_statement_base_h1 = DLogStatement { + N: n_tilde.clone(), + g: h1.clone(), + ni: h2.clone(), + }; + + let dlog_statement_base_h2 = DLogStatement { + N: n_tilde, + g: h2, + ni: h1, + }; + + let composite_dlog_proof_base_h1 = CompositeDLogProof::prove(&dlog_statement_base_h1, &xhi); + let composite_dlog_proof_base_h2 = CompositeDLogProof::prove(&dlog_statement_base_h2, &xhi_inv); + + ( + dlog_statement_base_h1, + composite_dlog_proof_base_h1, + composite_dlog_proof_base_h2, + ) +} + +impl JoinMessage { + pub fn set_party_index(&mut self, new_party_index: u16) { + self.party_index = Some(new_party_index); + } + /// The distribute phase for a new party. This distribute phase has to happen before the existing + /// parties distribute. Calling this function will generate a JoinMessage and a pair of Paillier + /// [Keys] that are going to be used when generating the [LocalKey]. + pub fn distribute() -> (Self, Keys) { + let paillier_key_pair = Keys::create(0); + let (dlog_statement, composite_dlog_proof_base_h1, composite_dlog_proof_base_h2) = + generate_dlog_statement_proofs(); + + let (ring_pedersen_statement, ring_pedersen_witness) = RingPedersenStatement::generate(); + + let ring_pedersen_proof = + RingPedersenProof::prove(&ring_pedersen_witness, &ring_pedersen_statement); + + let join_message = JoinMessage { + // in a join message, we only care about the ek and the correctness proof + ek: paillier_key_pair.ek.clone(), + dk_correctness_proof: NiCorrectKeyProof::proof(&paillier_key_pair.dk, None), + dlog_statement, + composite_dlog_proof_base_h1, + composite_dlog_proof_base_h2, + ring_pedersen_statement, + ring_pedersen_proof, + party_index: None, + }; + + (join_message, paillier_key_pair) + } + /// Returns the party index if it has been assigned one, throws + /// [FsDkrError::NewPartyUnassignedIndexError] otherwise + pub fn get_party_index(&self) -> FsDkrResult { + self.party_index + .ok_or(FsDkrError::NewPartyUnassignedIndexError) + } + + /// Collect phase of the protocol. Compared to the [RefreshMessage::collect], this has to be + /// tailored for a sent JoinMessage on which we assigned party_index. In this collect, a [LocalKey] + /// is filled with the information provided by the [RefreshMessage]s from the other parties and + /// the other join messages (multiple parties can be added/replaced at once). + pub fn collect( + &self, + refresh_messages: &[RefreshMessage], + paillier_key: Keys, + join_messages: &[JoinMessage], + new_t: u16, + new_n: u16, + current_t: u16, + ) -> FsDkrResult> { + RefreshMessage::validate_collect(refresh_messages, current_t, new_n)?; + + for refresh_message in refresh_messages.iter() { + RingPedersenProof::verify( + &refresh_message.ring_pedersen_proof, + &refresh_message.ring_pedersen_statement, + ) + .map_err(|_| FsDkrError::RingPedersenProofValidation { + party_index: refresh_message.party_index, + })?; + } + + for join_message in join_messages.iter() { + RingPedersenProof::verify( + &join_message.ring_pedersen_proof, + &join_message.ring_pedersen_statement, + ) + .map_err(|e| { + if let Some(party_index) = join_message.party_index { + FsDkrError::RingPedersenProofValidation { party_index } + } else { + e + } + })?; + } + + // check if a party_index has been assigned to the current party + let party_index = self.get_party_index()?; + + // check if a party_index has been assigned to all other new parties + // TODO: Check if no party_index collision exists + for join_message in join_messages.iter() { + join_message.get_party_index()?; + } + + let parameters = ShamirSecretSharing { + threshold: new_t, + share_count: new_n, + }; + + // generate a new share, the details can be found here https://hackmd.io/@omershlo/Hy1jBo6JY. + let (cipher_text_sum, li_vec) = RefreshMessage::get_ciphertext_sum( + refresh_messages, + party_index, + ¶meters, + &paillier_key.ek, + current_t + ); + let new_share = Paillier::decrypt(&paillier_key.dk, cipher_text_sum) + .0 + .into_owned(); + + let new_share_fe: Scalar = Scalar::::from(&new_share); + let paillier_dk = paillier_key.dk.clone(); + let key_linear_x_i = new_share_fe.clone(); + let key_linear_y = Point::::generator() * new_share_fe.clone(); + let keys_linear = SharedKeys { + x_i: key_linear_x_i, + y: key_linear_y, + }; + let mut pk_vec: Vec<_> = (0..new_n as usize) + .map(|i| refresh_messages[0].points_committed_vec[i].clone() * li_vec[0].clone()) + .collect(); + + for i in 0..new_n as usize { + for j in 1..(current_t + 1) as usize { + pk_vec[i] = pk_vec[i].clone() + + refresh_messages[j].points_committed_vec[i].clone() * li_vec[j].clone(); + } + } + + // check what parties are assigned in the current rotation and associate their paillier + // ek to each available party index. + + let available_parties: HashMap = refresh_messages + .iter() + .map(|msg| (msg.party_index, &msg.ek)) + .chain(std::iter::once((party_index, &paillier_key.ek))) + .chain( + join_messages + .iter() + .map(|join_message| (join_message.party_index.unwrap(), &join_message.ek)), + ) + .collect(); + + // TODO: submit the statement the dlog proof as well! + // check what parties are assigned in the current rotation and associate their DLogStatements + // and check their CompositeDlogProofs. + let available_h1_h2_ntilde_vec: HashMap = refresh_messages + .iter() + .map(|msg| (msg.party_index, &msg.dlog_statement)) + .chain(std::iter::once((party_index, &self.dlog_statement))) + .chain(join_messages.iter().map(|join_message| { + ( + join_message.party_index.unwrap(), + &join_message.dlog_statement, + ) + })) + .collect(); + + // generate the paillier public key vec needed for the LocalKey generation. + let paillier_key_vec: Vec = (1..new_n + 1) + .map(|party| { + let ek = available_parties.get(&party); + match ek { + None => EncryptionKey { + n: BigInt::zero(), + nn: BigInt::zero(), + }, + Some(key) => (*key).clone(), + } + }) + .collect(); + // generate the DLogStatement vec needed for the LocalKey generation. + let h1_h2_ntilde_vec: Vec = (1..new_n + 1) + .map(|party| { + let statement = available_h1_h2_ntilde_vec.get(&party); + + match statement { + None => generate_dlog_statement_proofs().0, + Some(dlog_statement) => (*dlog_statement).clone(), + } + }) + .collect(); + + // check if all the existing parties submitted the same public key. If they differ, abort. + // TODO: this should be verifiable? + for refresh_message in refresh_messages.iter() { + if refresh_message.public_key != refresh_messages[0].public_key { + return Err(FsDkrError::BroadcastedPublicKeyError); + } + } + + // generate the vss_scheme for the LocalKey + let (vss_scheme, _) = VerifiableSS::::share(new_t, new_n, &new_share_fe); + // TODO: secret cleanup might be needed. + + let local_key = LocalKey { + paillier_dk, + pk_vec, + keys_linear, + paillier_key_vec, + y_sum_s: refresh_messages[0].public_key.clone(), + h1_h2_n_tilde_vec: h1_h2_ntilde_vec, + vss_scheme, + i: party_index, + t: new_t, + n: new_n, + }; + + Ok(local_key) + } +} diff --git a/fs-dkr/src/error.rs b/fs-dkr/src/error.rs new file mode 100644 index 00000000..e1c78bb1 --- /dev/null +++ b/fs-dkr/src/error.rs @@ -0,0 +1,60 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +pub type FsDkrResult = Result; + +#[derive(Error, Debug, Clone, Serialize, Deserialize)] +pub enum FsDkrError { + #[error("Too many malicious parties detected! Threshold {threshold:?}, Number of Refreshed Messages: {refreshed_keys:?}, Malicious parties detected when trying to refresh: malicious_parties:?")] + PartiesThresholdViolation { + threshold: u16, + refreshed_keys: usize, + // TODO: figure out how to retrieve the malicious parties indexes and add them to the err. + // malicious_parties: [usize] + }, + + #[error("Shares did not pass verification.")] + PublicShareValidationError, + + #[error("SizeMismatch error for the refresh message {refresh_message_index:?} - pdl proof length: {pdl_proof_len:?}, Points Commited Length: {points_commited_len:?}, Points Encrypted Length: {points_encrypted_len:?}")] + SizeMismatchError { + refresh_message_index: usize, + pdl_proof_len: usize, + points_commited_len: usize, + points_encrypted_len: usize, + }, + + #[error("PDLwSlack proof verification failed, results: u1 == u1_test: {is_u1_eq:?}, u2 == u2_test: {is_u2_eq:?}, u3 == u3_test: {is_u3_eq:?}")] + PDLwSlackProof { + is_u1_eq: bool, + is_u2_eq: bool, + is_u3_eq: bool, + }, + + #[error("Ring Pedersen Proof Failed")] + RingPedersenProofError, + + #[error("Range Proof failed for party: {party_index:?}")] + RangeProof { party_index: usize }, + + #[error("The Paillier moduli size of party: {party_index:?} is {moduli_size:?} bits, when it should be 2047-2048 bits")] + ModuliTooSmall { + party_index: u16, + moduli_size: usize, + }, + + #[error("Paillier verification proof failed for party {party_index:?}")] + PaillierVerificationError { party_index: u16 }, + + #[error("A new party did not receive a valid index.")] + NewPartyUnassignedIndexError, + + #[error("The broadcasted public key is not the same from everyone, aborting")] + BroadcastedPublicKeyError, + + #[error("DLog proof failed for party {party_index:?}")] + DLogProofValidation { party_index: u16 }, + + #[error("Ring pedersen proof failed for party {party_index:?}")] + RingPedersenProofValidation { party_index: u16 }, +} diff --git a/fs-dkr/src/lib.rs b/fs-dkr/src/lib.rs new file mode 100644 index 00000000..4898fbe5 --- /dev/null +++ b/fs-dkr/src/lib.rs @@ -0,0 +1,27 @@ +#![allow(dead_code)] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::many_single_char_names))] +//! FS-DKR is a protocol for rotation of threshold ECDSA keys. +//! +//! We use standard proactive security assumptions. The protocol will be run +//! by $n$ parties. We assume honest majority, that is, number of corruptions is $t<=n/2$. +//! The adversary is malicious, and rushing. For communication, the parties have access +//! to a broadcast channel (can be implemented via a bulletin board). For threshold ECDSA, +//! we focus on GG20 protocol, currently considered state of the art and most widely deployed +//! threshold ecdsa scheme (e.g. multi-party-ecdsa, tss-lib). +//! +//! Components of the library: +//! +//! * [refresh_message]: crate::refresh_message +//! + +pub mod add_party_message; +pub mod error; +pub mod range_proofs; +pub mod refresh_message; +pub mod ring_pedersen_proof; +pub mod zk_pdl_with_slack; + +mod test; + +pub const PAILLIER_KEY_SIZE: usize = 2048; +pub const M_SECURITY: usize = 256; diff --git a/fs-dkr/src/range_proofs.rs b/fs-dkr/src/range_proofs.rs new file mode 100644 index 00000000..c194d598 --- /dev/null +++ b/fs-dkr/src/range_proofs.rs @@ -0,0 +1,746 @@ +#![allow(non_snake_case)] + +//! This file is a modified version of ING bank's range proofs implementation: +//! https://github.com/ing-bank/threshold-signatures/blob/master/src/algorithms/zkp.rs +//! +//! Zero knowledge range proofs for MtA protocol are implemented here. +//! Formal description can be found in Appendix A of https://eprint.iacr.org/2019/114.pdf +//! There are some deviations from the original specification: +//! 1) In Bob's proofs `gamma` is sampled from `[0;q^2 * N]` and `tau` from `[0;q^3 * N_tilde]`. +//! 2) A non-interactive version is implemented, with challenge `e` computed via Fiat-Shamir. + +// TODO: Verify this matches (if possible) range proofs from multi-party-ecdsa + +use curv::arithmetic::traits::*; +use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; +use curv::elliptic::curves::Point; +use curv::elliptic::curves::Scalar; +use curv::elliptic::curves::{Curve, Secp256k1}; +use curv::BigInt; +use paillier::{EncryptionKey, Randomness}; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; +use std::marker::PhantomData; +use zeroize::Zeroize; +use zk_paillier::zkproofs::DLogStatement; + +/// Represents the first round of the interactive version of the proof +#[derive(Zeroize)] +#[zeroize(drop)] +struct AliceZkpRound1 { + alpha: BigInt, + beta: BigInt, + gamma: BigInt, + ro: BigInt, + z: BigInt, + u: BigInt, + w: BigInt, +} + +impl AliceZkpRound1 { + fn from( + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + a: &BigInt, + q: &BigInt, + ) -> Self { + assert!( + q.bit_length() <= 256, + "We use SHA256 so we don't currently support moduli bigger than 256" + ); + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let N_tilde = &dlog_statement.N; + let alpha = BigInt::sample_below(&q.pow(3)); + let beta = BigInt::from_paillier_key(alice_ek); + let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let ro = BigInt::sample_below(&(q * N_tilde)); + let z = (BigInt::mod_pow(h1, &a, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; + let u = ((alpha.borrow() * &alice_ek.n + 1) + * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) + % &alice_ek.nn; + let w = + (BigInt::mod_pow(h1, &alpha, N_tilde) * BigInt::mod_pow(h2, &gamma, N_tilde)) % N_tilde; + Self { + alpha, + beta, + gamma, + ro, + z, + u, + w, + } + } +} + +/// Represents the second round of the interactive version of the proof +struct AliceZkpRound2 { + s: BigInt, + s1: BigInt, + s2: BigInt, +} + +impl AliceZkpRound2 { + fn from( + alice_ek: &EncryptionKey, + round1: &AliceZkpRound1, + e: &BigInt, + a: &BigInt, + r: &BigInt, + ) -> Self { + Self { + s: (BigInt::mod_pow(r, &e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, + s1: (e * a) + round1.alpha.borrow(), + s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), + } + } +} + +/// Alice's proof +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AliceProof { + z: BigInt, + e: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, + _phantom: PhantomData<(E, H)>, +} + +impl AliceProof { + /// verify Alice's proof using the proof and public keys + pub fn verify( + &self, + cipher: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + ) -> bool { + let N = &alice_ek.n; + let NN = &alice_ek.nn; + let N_tilde = &dlog_statement.N; + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let Gen = alice_ek.n.borrow() + 1; + + if self.s1 > Scalar::::group_order().pow(3) { + return false; + } + + let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); + let z_e_inv = match z_e_inv { + // z must be invertible, yet the check is done here + None => return false, + Some(c) => c, + }; + + let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) + * BigInt::mod_pow(h2, &self.s2, N_tilde) + * z_e_inv) + % N_tilde; + + let gs1 = (self.s1.borrow() * N + 1) % NN; + let cipher_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&cipher, &self.e, NN), NN); + let cipher_e_inv = match cipher_e_inv { + None => return false, + Some(c) => c, + }; + + let u = (gs1 * BigInt::mod_pow(&self.s, N, NN) * cipher_e_inv) % NN; + + let e = H::new() + .chain_bigint(&N) + .chain_bigint(&Gen) + .chain_bigint(cipher) + .chain_bigint(&self.z) + .chain_bigint(&u) + .chain_bigint(&w) + .result_bigint(); + + if e != self.e { + return false; + } + + true + } + /// Create the proof using Alice's Paillier private keys and public ZKP setup. + /// Requires randomness used for encrypting Alice's secret a. + /// It is assumed that a curve order smaller than 2^256 is used.. + pub fn generate( + a: &BigInt, + cipher: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &BigInt, + ) -> Self { + let q = Scalar::::group_order(); + assert!( + q.bit_length() <= 256, + "We use SHA256 so we don't currently support moduli bigger than 256" + ); + let round1 = AliceZkpRound1::from(alice_ek, dlog_statement, a, &q); + + let Gen = alice_ek.n.borrow() + 1; + let e = H::new() + .chain_bigint(&alice_ek.n) + .chain_bigint(&Gen) + .chain_bigint(cipher) + .chain_bigint(&round1.z) + .chain_bigint(&round1.u) + .chain_bigint(&round1.w) + .result_bigint(); + + let round2 = AliceZkpRound2::from(alice_ek, &round1, &e, a, r); + + Self { + z: round1.z.clone(), + e, + s: round2.s, + s1: round2.s1, + s2: round2.s2, + _phantom: PhantomData, + } + } +} + +/// Represents first round of the interactive version of the proof +struct BobZkpRound1 { + pub alpha: BigInt, + pub beta: BigInt, + pub gamma: BigInt, + pub ro: BigInt, + pub ro_prim: BigInt, + pub sigma: BigInt, + pub tau: BigInt, + pub z: BigInt, + pub z_prim: BigInt, + pub t: BigInt, + pub w: BigInt, + pub v: BigInt, + _phantom: PhantomData, +} + +impl Zeroize for BobZkpRound1 { + fn zeroize(&mut self) { + self.alpha.zeroize(); + self.beta.zeroize(); + self.gamma.zeroize(); + self.ro.zeroize(); + self.ro_prim.zeroize(); + self.sigma.zeroize(); + self.tau.zeroize(); + self.z.zeroize(); + self.z_prim.zeroize(); + self.t.zeroize(); + self.w.zeroize(); + self.v.zeroize(); + } +} + +impl Drop for BobZkpRound1 { + fn drop(&mut self) { + self.zeroize(); + } +} + +impl BobZkpRound1 { + /// `b` - Bob's secret + /// `beta_prim` - randomly chosen in `MtA` by Bob + /// `a_encrypted` - Alice's secret encrypted by Alice + fn from( + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + b: &Scalar, + beta_prim: &BigInt, + a_encrypted: &BigInt, + q: &BigInt, + ) -> Self { + assert!( + q.bit_length() <= 256, + "We use SHA256 so we don't currently support moduli bigger than 256" + ); + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let N_tilde = &dlog_statement.N; + let b_bn = b.to_bigint(); + + let alpha = BigInt::sample_below(&q.pow(3)); + let beta = BigInt::from_paillier_key(alice_ek); + let gamma = BigInt::sample_below(&(q.pow(2) * &alice_ek.n)); + let ro = BigInt::sample_below(&(q * N_tilde)); + let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let sigma = BigInt::sample_below(&(q * N_tilde)); + let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; + let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) + * BigInt::mod_pow(h2, &ro_prim, N_tilde)) + % N_tilde; + let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * BigInt::mod_pow(h2, &sigma, N_tilde)) + % N_tilde; + let w = + (BigInt::mod_pow(h1, &gamma, N_tilde) * BigInt::mod_pow(h2, &tau, N_tilde)) % N_tilde; + let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) + * (gamma.borrow() * &alice_ek.n + 1) + * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) + % &alice_ek.nn; + Self { + alpha, + beta, + gamma, + ro, + ro_prim, + sigma, + tau, + z, + z_prim, + t, + w, + v, + _phantom: PhantomData, + } + } +} + +/// represents second round of the interactive version of the proof +struct BobZkpRound2 { + pub s: BigInt, + pub s1: BigInt, + pub s2: BigInt, + pub t1: BigInt, + pub t2: BigInt, + _phantom: PhantomData, +} + +impl BobZkpRound2 { + /// `e` - the challenge in interactive ZKP, the hash in non-interactive ZKP + /// `b` - Bob's secret + /// `beta_prim` - randomly chosen in `MtA` by Bob + /// `r` - randomness used by Bob on Alice's public Paillier key to encrypt `beta_prim` in `MtA` + fn from( + alice_ek: &EncryptionKey, + round1: &BobZkpRound1, + e: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + r: &Randomness, + ) -> Self { + let b_bn = b.to_bigint(); + Self { + s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, + s1: (e * b_bn) + round1.alpha.borrow(), + s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), + t1: (e * beta_prim) + round1.gamma.borrow(), + t2: (e * round1.sigma.borrow()) + round1.tau.borrow(), + _phantom: PhantomData, + } + } +} + +/// Additional fields in Bob's proof if MtA is run with check +pub struct BobCheck { + u: Point, + X: Point, +} + +/// Bob's regular proof +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct BobProof { + t: BigInt, + z: BigInt, + e: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, + t1: BigInt, + t2: BigInt, + _phantom: PhantomData<(E, H)>, +} + +#[allow(clippy::too_many_arguments)] +impl BobProof { + pub fn verify( + &self, + a_enc: &BigInt, + mta_avc_out: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + check: Option<&BobCheck>, + ) -> bool { + let N = &alice_ek.n; + let NN = &alice_ek.nn; + let N_tilde = &dlog_statement.N; + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + + if self.s1 > Scalar::::group_order().pow(3) { + return false; + } + + let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); + let z_e_inv = match z_e_inv { + // z must be invertible, yet the check is done here + None => return false, + Some(c) => c, + }; + + let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) + * BigInt::mod_pow(h2, &self.s2, N_tilde) + * z_e_inv) + % N_tilde; + + let mta_e_inv = BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); + let mta_e_inv = match mta_e_inv { + None => return false, + Some(c) => c, + }; + + let v = (BigInt::mod_pow(a_enc, &self.s1, NN) + * BigInt::mod_pow(&self.s, N, NN) + * (self.t1.borrow() * N + 1) + * mta_e_inv) + % NN; + + let t_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.t, &self.e, N_tilde), N_tilde); + let t_e_inv = match t_e_inv { + None => return false, + Some(c) => c, + }; + + let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) + * BigInt::mod_pow(h2, &self.t2, N_tilde) + * t_e_inv) + % N_tilde; + + let Gen = alice_ek.n.borrow() + 1; + + let hash = H::new() + .chain_bigint(&alice_ek.n) + .chain_bigint(&Gen) + .chain_bigint(a_enc) + .chain_bigint(mta_avc_out) + .chain_bigint(&self.z) + .chain_bigint(&z_prim) + .chain_bigint(&self.t) + .chain_bigint(&v) + .chain_bigint(&w); + + let e = match check { + Some(_) => { + let X_x_coor = check.unwrap().X.x_coord().unwrap(); + let X_y_coor = check.unwrap().X.y_coord().unwrap(); + let u_x_coor = check.unwrap().u.x_coord().unwrap(); + let u_y_coor = check.unwrap().u.y_coord().unwrap(); + hash.chain_bigint(&X_x_coor) + .chain_bigint(&X_y_coor) + .chain_bigint(&u_x_coor) + .chain_bigint(&u_y_coor) + .result_bigint() + } + None => hash.result_bigint(), + }; + + if e != self.e { + return false; + } + + true + } + + pub fn generate( + a_encrypted: &BigInt, + mta_encrypted: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &Randomness, + check: bool, + ) -> (BobProof, Option>) { + let round1 = BobZkpRound1::from( + alice_ek, + dlog_statement, + b, + beta_prim, + a_encrypted, + &Scalar::::group_order(), + ); + + let Gen = alice_ek.n.borrow() + 1; + let hash = H::new() + .chain_bigint(&alice_ek.n) + .chain_bigint(&Gen) + .chain_bigint(a_encrypted) + .chain_bigint(mta_encrypted) + .chain_bigint(&round1.z) + .chain_bigint(&round1.z_prim) + .chain_bigint(&round1.t) + .chain_bigint(&round1.v) + .chain_bigint(&round1.w); + let mut check_u = None; + let e = if check { + let (X, u) = { + let ec_gen: Point = Point::::generator().to_point(); + let alpha: Scalar = Scalar::::from(&round1.alpha); + (ec_gen.clone() * b.clone(), ec_gen.clone() * alpha) + }; + check_u = Some(u.clone()); + let X_x_coor = X.x_coord().unwrap(); + let X_y_coor = X.y_coord().unwrap(); + let u_x_coor = u.x_coord().unwrap(); + let u_y_coor = u.y_coord().unwrap(); + hash.chain_bigint(&X_x_coor) + .chain_bigint(&X_y_coor) + .chain_bigint(&u_x_coor) + .chain_bigint(&u_y_coor) + .result_bigint() + } else { + hash.result_bigint() + }; + + let round2 = BobZkpRound2::from(alice_ek, &round1, &e, b, beta_prim, r); + + ( + BobProof { + t: round1.t.clone(), + z: round1.z.clone(), + e, + s: round2.s, + s1: round2.s1, + s2: round2.s2, + t1: round2.t1, + t2: round2.t2, + _phantom: PhantomData, + }, + check_u, + ) + } +} + +/// Bob's extended proof, adds the knowledge of $`B = g^b \in \mathcal{G}`$ +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct BobProofExt { + proof: BobProof, + u: Point, +} + +#[allow(clippy::too_many_arguments)] +impl BobProofExt { + pub fn verify( + &self, + a_enc: &BigInt, + mta_avc_out: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + X: &Point, + ) -> bool { + // check basic proof first + if !self.proof.verify( + a_enc, + mta_avc_out, + alice_ek, + dlog_statement, + Some(&BobCheck { + u: self.u.clone(), + X: X.clone(), + }), + ) { + return false; + } + + // fiddle with EC points + let (x1, x2) = { + let ec_gen: Point = Point::::generator().to_point(); + let s1: Scalar = Scalar::::from(&self.proof.s1); + let e: Scalar = Scalar::::from(&self.proof.e); + (ec_gen * s1, (X.clone() * e) + self.u.clone()) + }; + + if x1 != x2 { + return false; + } + + true + } + + fn generate( + a_encrypted: &BigInt, + mta_encrypted: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &Randomness, + ) -> BobProofExt { + // proving a basic proof (with modified hash) + let (bob_proof, u) = BobProof::generate( + a_encrypted, + mta_encrypted, + b, + beta_prim, + alice_ek, + dlog_statement, + r, + true, + ); + + BobProofExt { + proof: bob_proof, + u: u.unwrap(), + } + } +} + +/// sample random value of an element of a multiplicative group +pub trait SampleFromMultiplicativeGroup { + fn from_modulo(N: &BigInt) -> BigInt; + fn from_paillier_key(ek: &EncryptionKey) -> BigInt; +} + +impl SampleFromMultiplicativeGroup for BigInt { + fn from_modulo(N: &BigInt) -> BigInt { + let One = BigInt::one(); + loop { + let r = Self::sample_below(N); + if r.gcd(N) == One { + return r; + } + } + } + + fn from_paillier_key(ek: &EncryptionKey) -> BigInt { + Self::from_modulo(ek.n.borrow()) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use curv::elliptic::curves::secp256_k1::{Secp256k1Point, Secp256k1Scalar}; + use curv::elliptic::curves::ECScalar; + use paillier::traits::{Encrypt, EncryptWithChosenRandomness, KeyGeneration}; + use paillier::{Add, DecryptionKey, Mul, Paillier, RawCiphertext, RawPlaintext}; + use sha2::Sha256; + + type GE = Secp256k1Point; + type FE = Secp256k1Scalar; + + pub(crate) fn generate_init() -> (DLogStatement, EncryptionKey, DecryptionKey) { + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&ek_tilde.n); + let (xhi, _) = loop { + let xhi_ = BigInt::sample_below(&phi); + match BigInt::mod_inv(&xhi_, &phi) { + Some(inv) => break (xhi_, inv), + None => continue, + } + }; + let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); + + let (ek, dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + let dlog_statement = DLogStatement { + g: h1, + ni: h2, + N: ek_tilde.n, + }; + (dlog_statement, ek, dk) + } + + #[test] + fn alice_zkp() { + let (dlog_statement, ek, _) = generate_init(); + + // Alice's secret value + let a = FE::random().to_bigint(); + let r = BigInt::from_paillier_key(&ek); + let cipher = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(a.clone()), + &Randomness::from(&r), + ) + .0 + .clone() + .into_owned(); + + let alice_proof = + AliceProof::::generate(&a, &cipher, &ek, &dlog_statement, &r); + + assert!(alice_proof.verify(&cipher, &ek, &dlog_statement)); + } + + #[test] + fn bob_zkp() { + let (dlog_statement, ek, _) = generate_init(); + + (0..5).for_each(|_| { + let alice_public_key = &ek; + + // run MtA protocol with different inputs + (0..5).for_each(|_| { + // Simulate Alice + let a = FE::random().to_bigint(); + let encrypted_a = Paillier::encrypt(alice_public_key, RawPlaintext::from(a)) + .0 + .clone() + .into_owned(); + + // Bob follows MtA + let b: Scalar = Scalar::::random(); + // E(a) * b + let b_times_enc_a = Paillier::mul( + alice_public_key, + RawCiphertext::from(encrypted_a.clone()), + RawPlaintext::from(&b.to_bigint()), + ); + let beta_prim = BigInt::sample_below(&alice_public_key.n); + let r = Randomness::sample(alice_public_key); + let enc_beta_prim = Paillier::encrypt_with_chosen_randomness( + alice_public_key, + RawPlaintext::from(&beta_prim), + &r, + ); + + let mta_out = Paillier::add(alice_public_key, b_times_enc_a, enc_beta_prim); + + let (bob_proof, _) = BobProof::::generate( + &encrypted_a, + &mta_out.0.clone().into_owned(), + &b, + &beta_prim, + alice_public_key, + &dlog_statement, + &r, + false, + ); + assert!(bob_proof.verify( + &encrypted_a, + &mta_out.0.clone().into_owned(), + alice_public_key, + &dlog_statement, + None + )); + + // Bob follows MtAwc + let ec_gen: Point = Point::::generator().to_point(); + let X = ec_gen * b.clone(); + let bob_proof = BobProofExt::::generate( + &encrypted_a, + &mta_out.0.clone().into_owned(), + &b, + &beta_prim, + alice_public_key, + &dlog_statement, + &r, + ); + assert!(bob_proof.verify( + &encrypted_a, + &mta_out.0.clone().into_owned(), + alice_public_key, + &dlog_statement, + &X + )); + }); + }); + } +} diff --git a/fs-dkr/src/refresh_message.rs b/fs-dkr/src/refresh_message.rs new file mode 100644 index 00000000..446818bd --- /dev/null +++ b/fs-dkr/src/refresh_message.rs @@ -0,0 +1,475 @@ +use crate::add_party_message::JoinMessage; +use crate::error::{FsDkrError, FsDkrResult}; +use crate::range_proofs::AliceProof; +use crate::zk_pdl_with_slack::{PDLwSlackProof, PDLwSlackStatement, PDLwSlackWitness}; +use curv::arithmetic::{BitManipulation, Samplable, Zero}; +use curv::cryptographic_primitives::hashing::Digest; +use curv::cryptographic_primitives::secret_sharing::feldman_vss::{ + ShamirSecretSharing, VerifiableSS, +}; +use curv::elliptic::curves::{Curve, Point, Scalar}; +use curv::BigInt; +use curv::HashChoice; +use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::keygen::LocalKey; +pub use paillier::DecryptionKey; +use paillier::{ + Add, Decrypt, Encrypt, EncryptWithChosenRandomness, EncryptionKey, KeyGeneration, Mul, + Paillier, Randomness, RawCiphertext, RawPlaintext, +}; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; +use std::collections::HashMap; +use std::fmt::Debug; +use zeroize::Zeroize; +use zk_paillier::zkproofs::{DLogStatement, NiCorrectKeyProof, SALT_STRING}; + +use crate::ring_pedersen_proof::{RingPedersenProof, RingPedersenStatement}; + +// Everything here can be broadcasted +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(bound = "E: Curve, H: Digest + Clone")] +pub struct RefreshMessage { + pub(crate) old_party_index: u16, + pub(crate) party_index: u16, + pdl_proof_vec: Vec>, + range_proofs: Vec>, + coefficients_committed_vec: VerifiableSS, + pub(crate) points_committed_vec: Vec>, + points_encrypted_vec: Vec, + dk_correctness_proof: NiCorrectKeyProof, + pub(crate) dlog_statement: DLogStatement, + pub(crate) ek: EncryptionKey, + pub(crate) remove_party_indices: Vec, + pub(crate) public_key: Point, + pub(crate) ring_pedersen_statement: RingPedersenStatement, + pub(crate) ring_pedersen_proof: RingPedersenProof, + #[serde(skip)] + pub hash_choice: HashChoice, +} + +impl RefreshMessage { + pub fn distribute( + old_party_index: u16, + local_key: &mut LocalKey, + new_t: u16, + new_n: u16, + ) -> FsDkrResult<(RefreshMessage, DecryptionKey)> { + assert!(new_t <= new_n / 2); + let secret = local_key.keys_linear.x_i.clone(); + // secret share old key + if new_n <= new_t { + return Err(FsDkrError::NewPartyUnassignedIndexError); + } + let (vss_scheme, secret_shares) = VerifiableSS::::share(new_t, new_n, &secret); + + local_key.vss_scheme = vss_scheme.clone(); + + // commit to points on the polynomial + let points_committed_vec: Vec<_> = (0..secret_shares.len()) + .map(|i| Point::::generator() * &secret_shares[i].clone().into()) + .collect(); + + // encrypt points on the polynomial using Paillier keys + let (points_encrypted_vec, randomness_vec): (Vec<_>, Vec<_>) = (0..secret_shares.len()) + .map(|i| { + let randomness = BigInt::sample_below(&local_key.paillier_key_vec[i].n); + let ciphertext = Paillier::encrypt_with_chosen_randomness( + &local_key.paillier_key_vec[i], + RawPlaintext::from(secret_shares[i].to_bigint()), + &Randomness::from(randomness.clone()), + ) + .0 + .into_owned(); + (ciphertext, randomness) + }) + .unzip(); + + // generate PDL proofs for each {point_committed, point_encrypted} pair + let pdl_proof_vec: Vec<_> = (0..secret_shares.len()) + .map(|i| { + let witness = PDLwSlackWitness { + x: secret_shares[i].clone(), + r: randomness_vec[i].clone(), + }; + let statement = PDLwSlackStatement { + ciphertext: points_encrypted_vec[i].clone(), + ek: local_key.paillier_key_vec[i].clone(), + Q: points_committed_vec[i].clone(), + G: Point::::generator().to_point(), + h1: local_key.h1_h2_n_tilde_vec[i].g.clone(), + h2: local_key.h1_h2_n_tilde_vec[i].ni.clone(), + N_tilde: local_key.h1_h2_n_tilde_vec[i].N.clone(), + }; + PDLwSlackProof::prove(&witness, &statement) + }) + .collect(); + + let range_proofs = (0..secret_shares.len()) + .map(|i| { + AliceProof::generate( + &secret_shares[i].to_bigint(), + &points_encrypted_vec[i], + &local_key.paillier_key_vec[i], + &local_key.h1_h2_n_tilde_vec[i], + &randomness_vec[i], + ) + }) + .collect(); + + let (ek, dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + let dk_correctness_proof = NiCorrectKeyProof::proof(&dk, None); + + let (ring_pedersen_statement, ring_pedersen_witness) = RingPedersenStatement::generate(); + + let ring_pedersen_proof = + RingPedersenProof::prove(&ring_pedersen_witness, &ring_pedersen_statement); + Ok(( + RefreshMessage { + old_party_index, + party_index: local_key.i, + pdl_proof_vec, + range_proofs, + coefficients_committed_vec: vss_scheme, + points_committed_vec, + points_encrypted_vec, + dk_correctness_proof, + dlog_statement: local_key.h1_h2_n_tilde_vec[(local_key.i - 1) as usize].clone(), + ek, + remove_party_indices: Vec::new(), + public_key: local_key.y_sum_s.clone(), + ring_pedersen_statement, + ring_pedersen_proof, + hash_choice: HashChoice::new(), + }, + dk, + )) + } + + pub fn validate_collect(refresh_messages: &[Self], current_t: u16, new_n: u16) -> FsDkrResult<()> { + // check we got at least current threshold t + 1 refresh messages + // (i.e a quorum of existing parties has sent refresh messages). + if refresh_messages.len() <= current_t.into() { + return Err(FsDkrError::PartiesThresholdViolation { + threshold: current_t, + refreshed_keys: refresh_messages.len(), + }); + } + + // check all vectors are of same length + let reference_len = refresh_messages[0].pdl_proof_vec.len(); + + for (k, refresh_message) in refresh_messages.iter().enumerate() { + let pdl_proof_len = refresh_message.pdl_proof_vec.len(); + let points_commited_len = refresh_message.points_committed_vec.len(); + let points_encrypted_len = refresh_message.points_encrypted_vec.len(); + + if !(pdl_proof_len == reference_len + && points_commited_len == reference_len + && points_encrypted_len == reference_len) + { + return Err(FsDkrError::SizeMismatchError { + refresh_message_index: k, + pdl_proof_len, + points_commited_len, + points_encrypted_len, + }); + } + } + + for refresh_message in refresh_messages.iter() { + for i in 0..new_n as usize { + //TODO: we should handle the case of t( + refresh_messages: &'a [Self], + party_index: u16, + parameters: &'a ShamirSecretSharing, + ek: &'a EncryptionKey, + current_t: u16, + ) -> (RawCiphertext<'a>, Vec>) { + // TODO: check we have large enough qualified set , at least t+1 + //decrypt the new share + // we first homomorphically add all ciphertext encrypted using our encryption key + let ciphertext_vec: Vec<_> = (0..refresh_messages.len()) + .map(|k| refresh_messages[k].points_encrypted_vec[(party_index - 1) as usize].clone()) + .collect(); + + let indices: Vec = (0..(current_t + 1) as usize) + .map(|i| refresh_messages[i].old_party_index - 1) + .collect(); + + // optimization - one decryption + let li_vec: Vec<_> = (0..current_t as usize + 1) + .map(|i| { + VerifiableSS::::map_share_to_new_params( + parameters.clone().borrow(), + indices[i], + &indices, + ) + }) + .collect(); + + let ciphertext_vec_at_indices_mapped: Vec<_> = (0..(current_t + 1) as usize) + .map(|i| { + Paillier::mul( + ek, + RawCiphertext::from(ciphertext_vec[i].clone()), + RawPlaintext::from(li_vec[i].to_bigint()), + ) + }) + .collect(); + + let ciphertext_sum = ciphertext_vec_at_indices_mapped.iter().fold( + Paillier::encrypt(ek, RawPlaintext::from(BigInt::zero())), + |acc, x| Paillier::add(ek, acc, x.clone()), + ); + + (ciphertext_sum, li_vec) + } + + pub fn replace( + new_parties: &[JoinMessage], + key: &mut LocalKey, + old_to_new_map: &HashMap, + new_t: u16, + new_n: u16, + ) -> FsDkrResult<(Self, DecryptionKey)> { + let current_len = key.paillier_key_vec.len() as u16; + let mut paillier_key_h1_h2_n_tilde_hash_map: HashMap = + HashMap::new(); + for old_party_index in old_to_new_map.keys() { + let paillier_key = key + .paillier_key_vec + .get((old_party_index - 1) as usize) + .unwrap() + .clone(); + let h1_h2_n_tilde = key + .h1_h2_n_tilde_vec + .get((old_party_index - 1) as usize) + .unwrap() + .clone(); + paillier_key_h1_h2_n_tilde_hash_map.insert( + old_to_new_map.get(old_party_index).unwrap().clone(), + (paillier_key, h1_h2_n_tilde), + ); + } + + for new_party_index in paillier_key_h1_h2_n_tilde_hash_map.keys() { + if new_party_index.clone() <= current_len { + key.paillier_key_vec[(new_party_index - 1) as usize] = + paillier_key_h1_h2_n_tilde_hash_map + .get(new_party_index) + .unwrap() + .clone() + .0; + key.h1_h2_n_tilde_vec[(new_party_index - 1) as usize] = + paillier_key_h1_h2_n_tilde_hash_map + .get(new_party_index) + .unwrap() + .clone() + .1; + } else { + key.paillier_key_vec.insert( + (new_party_index - 1) as usize, + paillier_key_h1_h2_n_tilde_hash_map + .get(new_party_index) + .unwrap() + .clone() + .0, + ); + key.h1_h2_n_tilde_vec.insert( + (new_party_index - 1) as usize, + paillier_key_h1_h2_n_tilde_hash_map + .get(new_party_index) + .unwrap() + .clone() + .1, + ); + } + } + + for join_message in new_parties.iter() { + let party_index = join_message.get_party_index()?; + if party_index <= current_len { + key.paillier_key_vec[(party_index - 1) as usize] = join_message.ek.clone(); + key.h1_h2_n_tilde_vec[(party_index - 1) as usize] = + join_message.dlog_statement.clone(); + } else { + key.paillier_key_vec + .insert((party_index - 1) as usize, join_message.ek.clone()); + key.h1_h2_n_tilde_vec.insert( + (party_index - 1) as usize, + join_message.dlog_statement.clone(), + ); + } + } + let old_party_index = key.i; + key.i = *old_to_new_map.get(&key.i).unwrap(); + key.t = new_t; + key.n = new_n; + + RefreshMessage::distribute(old_party_index, key, new_t, new_n) + } + + pub fn collect( + refresh_messages: &[Self], + mut local_key: &mut LocalKey, + new_dk: DecryptionKey, + join_messages: &[JoinMessage], + current_t: u16, + ) -> FsDkrResult<()> { + let new_n = refresh_messages.len() + join_messages.len(); + RefreshMessage::validate_collect(refresh_messages, current_t, new_n as u16)?; + + for refresh_message in refresh_messages.iter() { + for i in 0..(new_n as usize) { + let statement = PDLwSlackStatement { + ciphertext: refresh_message.points_encrypted_vec[i].clone(), + ek: local_key.paillier_key_vec[i].clone(), + Q: refresh_message.points_committed_vec[i].clone(), + G: Point::::generator().to_point(), + h1: local_key.h1_h2_n_tilde_vec[i].g.clone(), + h2: local_key.h1_h2_n_tilde_vec[i].ni.clone(), + N_tilde: local_key.h1_h2_n_tilde_vec[i].N.clone(), + }; + refresh_message.pdl_proof_vec[i].verify(&statement)?; + if !refresh_message.range_proofs[i].verify( + &statement.ciphertext, + &statement.ek, + &local_key.h1_h2_n_tilde_vec[i], + ) { + return Err(FsDkrError::RangeProof { party_index: i }); + } + } + } + + // Verify ring-pedersen parameters + for refresh_message in refresh_messages.iter() { + RingPedersenProof::verify( + &refresh_message.ring_pedersen_proof, + &refresh_message.ring_pedersen_statement, + )?; + } + + for join_message in join_messages.iter() { + RingPedersenProof::verify( + &join_message.ring_pedersen_proof, + &join_message.ring_pedersen_statement, + )?; + } + + let old_ek = local_key.paillier_key_vec[(local_key.i - 1) as usize].clone(); + let (cipher_text_sum, li_vec) = RefreshMessage::get_ciphertext_sum( + refresh_messages, + local_key.i, + &local_key.vss_scheme.parameters, + &old_ek, + current_t + ); + + for refresh_message in refresh_messages.iter() { + if refresh_message + .dk_correctness_proof + .verify(&refresh_message.ek, SALT_STRING) + .is_err() + { + return Err(FsDkrError::PaillierVerificationError { + party_index: refresh_message.party_index, + }); + } + let n_length = refresh_message.ek.n.bit_length(); + if n_length > crate::PAILLIER_KEY_SIZE || n_length < crate::PAILLIER_KEY_SIZE - 1 { + return Err(FsDkrError::ModuliTooSmall { + party_index: refresh_message.party_index, + moduli_size: n_length, + }); + } + + // if the proof checks, we add the new paillier public key to the key + local_key.paillier_key_vec[(refresh_message.party_index - 1) as usize] = + refresh_message.ek.clone(); + } + + for join_message in join_messages { + let party_index = join_message.get_party_index()?; + + if join_message + .dk_correctness_proof + .verify(&join_message.ek, SALT_STRING) + .is_err() + { + return Err(FsDkrError::PaillierVerificationError { party_index }); + } + + // creating an inverse dlog statement + let dlog_statement_base_h2 = DLogStatement { + N: join_message.dlog_statement.N.clone(), + g: join_message.dlog_statement.ni.clone(), + ni: join_message.dlog_statement.g.clone(), + }; + if join_message + .composite_dlog_proof_base_h1 + .verify(&join_message.dlog_statement) + .is_err() + || join_message + .composite_dlog_proof_base_h2 + .verify(&dlog_statement_base_h2) + .is_err() + { + return Err(FsDkrError::DLogProofValidation { party_index }); + } + + let n_length = join_message.ek.n.bit_length(); + if n_length > crate::PAILLIER_KEY_SIZE || n_length < crate::PAILLIER_KEY_SIZE - 1 { + return Err(FsDkrError::ModuliTooSmall { + party_index: join_message.get_party_index()?, + moduli_size: n_length, + }); + } + + // if the proof checks, we add the new paillier public key to the key + local_key.paillier_key_vec[(party_index - 1) as usize] = join_message.ek.clone(); + } + + let new_share = Paillier::decrypt(&local_key.paillier_dk, cipher_text_sum) + .0 + .into_owned(); + + let new_share_fe: Scalar = Scalar::::from(&new_share); + + // zeroize the old dk key + local_key.paillier_dk.q.zeroize(); + local_key.paillier_dk.p.zeroize(); + local_key.paillier_dk = new_dk; + + // update old key and output new key + local_key.keys_linear.x_i = new_share_fe.clone(); + local_key.keys_linear.y = Point::::generator() * new_share_fe; + + // update local key list of local public keys (X_i = g^x_i is updated by adding all committed points to that party) + for i in 0..refresh_messages.len() + join_messages.len() as usize { + local_key.pk_vec.insert( + i, + refresh_messages[0].points_committed_vec[i].clone() * li_vec[0].clone(), + ); + for j in 1..current_t as usize + 1 { + local_key.pk_vec[i] = local_key.pk_vec[i].clone() + + refresh_messages[j].points_committed_vec[i].clone() * li_vec[j].clone(); + } + } + + Ok(()) + } +} diff --git a/fs-dkr/src/ring_pedersen_proof.rs b/fs-dkr/src/ring_pedersen_proof.rs new file mode 100644 index 00000000..c3cfe17a --- /dev/null +++ b/fs-dkr/src/ring_pedersen_proof.rs @@ -0,0 +1,179 @@ +#![allow(non_snake_case)] + +/* + Ring Pedersen Proof + Copyright 2022 by Webb Technologies. + + ring_pedersen_proof.rs is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + @license GPL-3.0+ +*/ + +use bitvec::prelude::*; +use curv::arithmetic::traits::*; +use curv::cryptographic_primitives::hashing::Digest; +use curv::cryptographic_primitives::hashing::DigestExt; +use curv::elliptic::curves::Curve; +use curv::BigInt; +use paillier::EncryptionKey; +use paillier::{KeyGeneration, Paillier}; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; + +use crate::error::FsDkrError; +use crate::error::FsDkrResult; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound = "E: Curve, H: Digest + Clone")] +pub struct RingPedersenStatement { + pub S: BigInt, + pub T: BigInt, + pub N: BigInt, + phi: BigInt, + pub ek: EncryptionKey, + #[serde(skip)] + phantom: PhantomData<(E, H)>, +} + +pub struct RingPedersenWitness { + p: BigInt, + q: BigInt, + lambda: BigInt, + phantom: PhantomData<(E, H)>, +} + +impl RingPedersenStatement { + pub fn generate() -> (Self, RingPedersenWitness) { + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let r = BigInt::sample_below(&ek_tilde.n); + let lambda = BigInt::sample_below(&phi); + let t = BigInt::mod_pow(&r, &BigInt::from(2), &ek_tilde.n); + let s = BigInt::mod_pow(&t, &lambda, &ek_tilde.n); + + ( + Self { + S: s, + T: t, + N: ek_tilde.clone().n, + phi: phi, + ek: ek_tilde, + phantom: PhantomData, + }, + RingPedersenWitness { + p: dk_tilde.p, + q: dk_tilde.q, + lambda, + phantom: PhantomData, + }, + ) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound = "E: Curve, H: Digest + Clone")] +pub struct RingPedersenProof { + A: Vec, + Z: Vec, + #[serde(skip)] + phantom: PhantomData<(E, H)>, +} + +// Link to the UC non-interactive threshold ECDSA paper +impl RingPedersenProof { + pub fn prove( + witness: &RingPedersenWitness, + statement: &RingPedersenStatement, + ) -> RingPedersenProof { + // 1. Sample alphas from 1 -> m from \phi(N) + let mut a = [(); M].map(|_| BigInt::zero()); + let mut A = [(); M].map(|_| BigInt::zero()); + let mut hash = H::new(); + for i in 0..M { + // TODO: Consider ensuring we get a unit element of this subgroup + let a_i = BigInt::sample_below(&statement.phi); + a[i] = a_i.clone(); + let A_i = BigInt::mod_pow(&statement.T, &a_i, &statement.N); + A[i] = A_i.clone(); + hash = H::chain_bigint(hash, &A_i); + } + + let e: BigInt = hash.result_bigint(); + let bitwise_e: BitVec = BitVec::from_vec(e.to_bytes()); + + let mut Z = [(); M].map(|_| BigInt::zero()); + for i in 0..M { + let e_i = if bitwise_e[i] { + BigInt::one() + } else { + BigInt::zero() + }; + let z_i = BigInt::mod_add(&a[i], &(e_i * &witness.lambda), &statement.phi); + Z[i] = z_i; + } + + Self { + A: A.to_vec(), + Z: Z.to_vec(), + phantom: PhantomData, + } + } + + pub fn verify( + proof: &RingPedersenProof, + statement: &RingPedersenStatement, + ) -> FsDkrResult<()> { + let mut hash = H::new(); + for i in 0..M { + hash = H::chain_bigint(hash, &proof.A[i]); + } + + let e: BigInt = hash.result_bigint(); + let bitwise_e: BitVec = BitVec::from_vec(e.to_bytes()); + + for i in 0..M { + let mut e_i = 0; + if bitwise_e[i] { + e_i = 1; + } + + if BigInt::mod_pow(&statement.T, &proof.Z[i], &statement.N) + == BigInt::mod_mul( + &proof.A[i], + &BigInt::mod_pow(&statement.S, &BigInt::from(e_i), &statement.N), + &statement.N, + ) + { + continue; + } else { + return Err(FsDkrError::RingPedersenProofError); + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use curv::elliptic::curves::secp256_k1::Secp256k1; + use sha2::Sha256; + + #[test] + fn test_ring_pedersen() { + let (statement, witness) = RingPedersenStatement::::generate(); + let proof = RingPedersenProof::::prove( + &witness, &statement, + ); + assert!( + RingPedersenProof::::verify( + &proof, &statement + ) + .is_ok() + ); + } +} diff --git a/fs-dkr/src/test.rs b/fs-dkr/src/test.rs new file mode 100644 index 00000000..a9fc8410 --- /dev/null +++ b/fs-dkr/src/test.rs @@ -0,0 +1,419 @@ +#[cfg(test)] +mod tests { + use crate::refresh_message::RefreshMessage; + use curv::arithmetic::Converter; + use curv::cryptographic_primitives::secret_sharing::feldman_vss::{ + ShamirSecretSharing, VerifiableSS, + }; + use curv::elliptic::curves::secp256_k1::Secp256k1Point; + use curv::elliptic::curves::Secp256k1; + use curv::BigInt; + use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::party_i::verify; + use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::party_i::Keys; + use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::keygen::{ + Keygen, LocalKey, + }; + use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::sign::{ + CompletedOfflineStage, OfflineStage, SignManual, + }; + use sha2::Sha256; + + use crate::add_party_message::JoinMessage; + use crate::error::FsDkrResult; + use curv::{ + elliptic::curves::Scalar, + cryptographic_primitives::{ + hashing::Digest, proofs::sigma_dlog::DLogProof} + }; + use paillier::DecryptionKey; + use round_based::dev::Simulation; + use std::collections::HashMap; + + type GE = Secp256k1Point; + + #[test] + fn test1() { + //simulate keygen + let t = 3; + let n = 6; + let mut keys = simulate_keygen(t, n); + + let old_keys = keys.clone(); + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); + + // check that sum of old keys is equal to sum of new keys + let old_linear_secret_key: Vec<_> = (0..old_keys.len()) + .map(|i| old_keys[i].keys_linear.x_i.clone()) + .collect(); + + let new_linear_secret_key: Vec<_> = (0..keys.len()) + .map(|i| keys[i].keys_linear.x_i.clone()) + .collect(); + let indices: Vec<_> = (0..(t + 1) as u16).collect(); + let vss = VerifiableSS:: { + parameters: ShamirSecretSharing { + threshold: t, + share_count: n, + }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random()), + }; + assert_eq!( + vss.reconstruct(&indices[..], &old_linear_secret_key[0..(t + 1) as usize]), + vss.reconstruct(&indices[..], &new_linear_secret_key[0..(t + 1) as usize]) + ); + assert_ne!(old_linear_secret_key, new_linear_secret_key); + } + + #[test] + fn test_sign_rotate_sign() { + let mut keys = simulate_keygen(2, 5); + let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); + let offline_sign = simulate_offline_stage(keys.clone(), &[2, 3, 4]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); + let offline_sign = simulate_offline_stage(keys, &[1, 3, 5]); + simulate_signing(offline_sign, b"ZenGo"); + } + + #[test] + fn test_remove_sign_rotate_sign() { + let mut keys = simulate_keygen(2, 5); + let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr_removal::<{ crate::M_SECURITY }>(&mut keys, [1].to_vec(), None); + let offline_sign = simulate_offline_stage(keys.clone(), &[2, 3, 4]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr_removal::<{ crate::M_SECURITY }>(&mut keys, [1, 2].to_vec(), None); + let offline_sign = simulate_offline_stage(keys, &[3, 4, 5]); + simulate_signing(offline_sign, b"ZenGo"); + } + + #[test] + fn test_add_party_with_permute() { + fn simulate_replace( + keys: &mut Vec>, + party_indices: &[u16], + old_to_new_map: &HashMap, + t: u16, + n: u16, + ) -> FsDkrResult<()> { + fn generate_join_messages_and_keys( + number_of_new_parties: usize, + ) -> (Vec>, Vec) { + // the new party generates it's join message to start joining the computation + (0..number_of_new_parties) + .map(|_| JoinMessage::distribute()) + .unzip() + } + + fn generate_refresh_parties_replace( + keys: &mut [LocalKey], + old_to_new_map: &HashMap, + join_messages: &[JoinMessage], + ) -> ( + Vec>, + Vec, + ) { + let new_n = (&keys.len() + join_messages.len()) as u16; + keys.iter_mut() + .map(|key| { + RefreshMessage::replace(join_messages, key, old_to_new_map, key.t, new_n).unwrap() + }) + .unzip() + } + + // each party that wants to join generates a join message and a pair of paillier keys. + let (mut join_messages, new_keys) = + generate_join_messages_and_keys::<{ crate::M_SECURITY }>(party_indices.len()); + + // each new party has to be informed through offchannel communication what party index + // it has been assigned (the information is public). + for (join_message, party_index) in join_messages.iter_mut().zip(party_indices) { + join_message.party_index = Some(*party_index); + } + + // each existing party has to generate it's refresh message aware of the new parties + let (refresh_messages, dk_keys) = + generate_refresh_parties_replace(keys, &old_to_new_map, join_messages.as_slice()); + let mut new_keys_vec: Vec<(u16, LocalKey)> = + Vec::with_capacity(keys.len() + join_messages.len()); + // all existing parties rotate aware of the join_messages + for i in 0..keys.len() as usize { + RefreshMessage::collect( + refresh_messages.as_slice(), + &mut keys[i], + dk_keys[i].clone(), + join_messages.as_slice(), + t, + ) + .expect(""); + new_keys_vec.push((keys[i].i - 1, keys[i].clone())); + } + + // all new parties generate a local key + for (join_message, dk) in join_messages.iter().zip(new_keys) { + let party_index = join_message.party_index.unwrap(); + let local_key = join_message.collect( + refresh_messages.as_slice(), + dk, + join_messages.as_slice(), + t, + n, + t, + )?; + + new_keys_vec.push((party_index - 1, local_key)); + } + + new_keys_vec.sort_by(|a, b| a.0.cmp(&b.0)); + let keys_replacements = new_keys_vec + .iter() + .map(|a| a.1.clone()) + .collect::>>(); + *keys = keys_replacements; + Ok(()) + } + + let t = 2; + let n = 7; + + let all_keys = simulate_keygen(t, n); + // Remove the 2nd and 7th party + let mut keys = all_keys.clone(); + keys.remove(6); + keys.remove(1); + + let mut old_to_new_map: HashMap = HashMap::new(); + old_to_new_map.insert(1, 4); + old_to_new_map.insert(3, 1); + old_to_new_map.insert(4, 3); + old_to_new_map.insert(5, 6); + old_to_new_map.insert(6, 5); + + // Simulate the replace + simulate_replace::<{ crate::M_SECURITY }>(&mut keys, &[2, 7], &old_to_new_map, t, n) + .unwrap(); + // check that sum of old keys is equal to sum of new keys + let old_linear_secret_key: Vec<_> = (0..all_keys.len()) + .map(|i| all_keys[i].keys_linear.x_i.clone()) + .collect(); + + let new_linear_secret_key: Vec<_> = (0..keys.len()) + .map(|i| keys[i].keys_linear.x_i.clone()) + .collect(); + let indices: Vec<_> = (0..(t + 1) as u16).collect(); + let vss = VerifiableSS:: { + parameters: ShamirSecretSharing { + threshold: t, + share_count: n, + }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random()), + }; + assert_eq!( + vss.reconstruct(&indices[..], &old_linear_secret_key[0..(t + 1) as usize]), + vss.reconstruct(&indices[..], &new_linear_secret_key[0..(t + 1) as usize]) + ); + assert_ne!(old_linear_secret_key, new_linear_secret_key); + + let offline_sign = simulate_offline_stage(keys, &[1, 2, 7]); + simulate_signing(offline_sign, b"ZenGo"); + } + + #[test] + fn test_change_threshold_sign_rotate_sign() { + let mut keys = simulate_keygen(2, 5); + let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); + simulate_signing(offline_sign, b"ZenGo"); + // Change threshold to 1 (i.e quorum size = 2). + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, Some(1)); + let offline_sign = simulate_offline_stage(keys.clone(), &[3, 4]); + simulate_signing(offline_sign, b"ZenGo"); + // Change threshold to back to 2 (i.e quorum size = 3). + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, Some(2)); + let offline_sign = simulate_offline_stage(keys, &[1, 3, 5]); + simulate_signing(offline_sign, b"ZenGo"); + } + + fn simulate_keygen(t: u16, n: u16) -> Vec> { + //simulate keygen + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + for i in 1..=n { + simulation.add_party(Keygen::new(i, t, n).unwrap()); + } + + simulation.run().unwrap() + } + + fn simulate_dkr_removal( + keys: &mut Vec>, + remove_party_indices: Vec, + new_t_option: Option, + ) { + let mut broadcast_messages: HashMap>> = + HashMap::new(); + let mut new_dks: HashMap = HashMap::new(); + let mut refresh_messages: Vec> = Vec::new(); + let mut party_key: HashMap> = HashMap::new(); + // TODO: Verify this is correct + let new_n = keys.len() as u16; + let current_t = keys[0].t; + let new_t = new_t_option.unwrap_or(current_t); + for key in keys.iter_mut() { + let (refresh_message, new_dk) = RefreshMessage::distribute(key.i, key, new_t, new_n).unwrap(); + refresh_messages.push(refresh_message.clone()); + new_dks.insert(refresh_message.party_index.into(), new_dk); + party_key.insert(refresh_message.party_index.into(), key.clone()); + } + + for refresh_message in refresh_messages.iter() { + broadcast_messages.insert(refresh_message.party_index.into(), Vec::new()); + } + + for refresh_message in refresh_messages.iter_mut() { + if !remove_party_indices.contains(&refresh_message.party_index.into()) { + refresh_message.remove_party_indices = remove_party_indices.clone(); + } else { + let mut new_remove_party_indices = remove_party_indices.clone(); + new_remove_party_indices + .retain(|value| *value != refresh_message.party_index.into()); + refresh_message.remove_party_indices = new_remove_party_indices; + } + + for (party_index, refresh_bucket) in broadcast_messages.iter_mut() { + if refresh_message + .remove_party_indices + .contains(&(*party_index as u16)) + { + continue; + } + refresh_bucket.push(refresh_message.clone()); + } + } + + for remove_party_index in remove_party_indices.iter() { + assert_eq!(broadcast_messages[&(*remove_party_index as usize)].len(), 1); + } + + // keys will be updated to refreshed values + for (party, key) in party_key.iter_mut() { + if remove_party_indices.contains(&(*party as u16)) { + continue; + } + + RefreshMessage::collect( + broadcast_messages[party].clone().as_slice(), + key, + new_dks[party].clone(), + &[], + current_t + ) + .expect(""); + } + + for remove_party_index in remove_party_indices { + let result = RefreshMessage::collect( + &broadcast_messages[&(remove_party_index as usize)], + &mut keys[remove_party_index as usize], + new_dks[&(remove_party_index as usize)].clone(), + &[], + current_t + ); + assert!(result.is_err()); + } + } + + fn simulate_dkr( + keys: &mut Vec>, + new_t_option: Option, + ) -> ( + Vec>, + Vec, + ) { + let mut broadcast_vec: Vec> = Vec::new(); + let mut new_dks: Vec = Vec::new(); + let keys_len = keys.len(); + let current_t = keys[0].t; + let new_t = new_t_option.unwrap_or(current_t); + for key in keys.iter_mut() { + let (refresh_message, new_dk) = + RefreshMessage::distribute(key.i, key, new_t, keys_len as u16).unwrap(); + broadcast_vec.push(refresh_message); + new_dks.push(new_dk); + } + + // keys will be updated to refreshed values + for i in 0..keys.len() as usize { + RefreshMessage::collect(&broadcast_vec, &mut keys[i], new_dks[i].clone(), &[], current_t) + .expect(""); + } + + (broadcast_vec, new_dks) + } + + fn simulate_offline_stage( + local_keys: Vec>, + s_l: &[u16], + ) -> Vec { + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + for (i, &keygen_i) in (1..).zip(s_l) { + simulation.add_party( + OfflineStage::new( + i, + s_l.to_vec(), + local_keys[usize::from(keygen_i - 1)].clone(), + ) + .unwrap(), + ); + } + + simulation.run().unwrap() + } + + fn simulate_signing(offline: Vec, message: &[u8]) { + let message = create_hash(&[&BigInt::from_bytes(message)]); + let pk = &offline[0].public_key(); + + let parties = offline + .iter() + .map(|o| SignManual::new(message.clone(), o.clone())) + .collect::, _>>() + .unwrap(); + let (parties, local_sigs): (Vec<_>, Vec<_>) = parties.into_iter().unzip(); + // parties.remove(0).complete(&local_sigs[1..]).unwrap(); + let local_sigs_except = |i: usize| { + let mut v = vec![]; + v.extend_from_slice(&local_sigs[..i]); + if i + 1 < local_sigs.len() { + v.extend_from_slice(&local_sigs[i + 1..]); + } + v + }; + + assert!(parties + .into_iter() + .enumerate() + .map(|(i, p)| p.complete(&local_sigs_except(i)).unwrap()) + .all(|signature| verify(&signature, &pk, &message).is_ok())); + } + + fn create_hash(big_ints: &[&BigInt]) -> BigInt { + let hasher = Sha256::new(); + + for value in big_ints { + hasher.clone().chain(&BigInt::to_bytes(value)); + } + + let result_hex = hasher.finalize(); + BigInt::from_bytes(&result_hex[..]) + } +} diff --git a/fs-dkr/src/zk_pdl_with_slack.rs b/fs-dkr/src/zk_pdl_with_slack.rs new file mode 100644 index 00000000..e17017b2 --- /dev/null +++ b/fs-dkr/src/zk_pdl_with_slack.rs @@ -0,0 +1,332 @@ +#![allow(non_snake_case)] + +//! We use the proof as given in proof PIi in https://eprint.iacr.org/2016/013.pdf. +//! This proof ws taken from the proof 6.3 (left side ) in https://www.cs.unc.edu/~reiter/papers/2004/IJIS.pdf +//! +//! Statement: (c, pk, Q, G) +//! witness (x, r) such that Q = xG, c = Enc(pk, x, r) +//! note that because of the range proof, the proof has a slack in the range: x in [-q^3, q^3] + +use std::marker::PhantomData; + +use crate::error::{FsDkrError, FsDkrResult}; +use curv::arithmetic::traits::*; +use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; +use curv::elliptic::curves::Curve; +use curv::elliptic::curves::Point; +use curv::elliptic::curves::Scalar; +use curv::elliptic::curves::Secp256k1; +use curv::BigInt; +use paillier::EncryptionKey; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PDLwSlackStatement { + pub ciphertext: BigInt, + pub ek: EncryptionKey, + pub Q: Point, + pub G: Point, + pub h1: BigInt, + pub h2: BigInt, + pub N_tilde: BigInt, +} +#[derive(Clone)] +pub struct PDLwSlackWitness { + pub x: Scalar, + pub r: BigInt, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(bound = "E: Curve, H: Digest + Clone")] +pub struct PDLwSlackProof { + z: BigInt, + u1: Point, + u2: BigInt, + u3: BigInt, + s1: BigInt, + s2: BigInt, + s3: BigInt, + _phantom: PhantomData, +} + +impl PDLwSlackProof { + pub fn prove(witness: &PDLwSlackWitness, statement: &PDLwSlackStatement) -> Self { + let q3 = Scalar::::group_order().pow(3); + let q_N_tilde = Scalar::::group_order() * &statement.N_tilde; + let q3_N_tilde = &q3 * &statement.N_tilde; + + let alpha = BigInt::sample_below(&q3); + let one = BigInt::one(); + let beta = BigInt::sample_range(&one, &(&statement.ek.n - &one)); + let rho = BigInt::sample_below(&q_N_tilde); + let gamma = BigInt::sample_below(&q3_N_tilde); + + let z = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &witness.x.to_bigint(), + &rho, + ); + let u1 = statement.G.clone() * Scalar::::from(&alpha); + let u2 = commitment_unknown_order( + &(&statement.ek.n + BigInt::one()), + &beta, + &statement.ek.nn, + &alpha, + &statement.ek.n, + ); + let u3 = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &alpha, + &gamma, + ); + + let e = H::new() + .chain_bigint(&BigInt::from_bytes(&statement.G.to_bytes(true))) + .chain_bigint(&BigInt::from_bytes(&statement.Q.to_bytes(true))) + .chain_bigint(&statement.ciphertext) + .chain_bigint(&z) + .chain_bigint(&BigInt::from_bytes(&u1.to_bytes(true))) + .chain_bigint(&u2) + .chain_bigint(&u3) + .result_bigint(); + + let s1 = &e * witness.x.to_bigint() + alpha; + let s2 = commitment_unknown_order(&witness.r, &beta, &statement.ek.n, &e, &BigInt::one()); + let s3 = &e * rho + gamma; + + PDLwSlackProof { + z, + u1, + u2, + u3, + s1, + s2, + s3, + _phantom: PhantomData, + } + } + + pub fn verify(&self, statement: &PDLwSlackStatement) -> FsDkrResult<()> { + let e = H::new() + .chain_bigint(&BigInt::from_bytes(&statement.G.to_bytes(true))) + .chain_bigint(&BigInt::from_bytes(&statement.Q.to_bytes(true))) + .chain_bigint(&statement.ciphertext) + .chain_bigint(&self.z) + .chain_bigint(&BigInt::from_bytes(&self.u1.to_bytes(true))) + .chain_bigint(&self.u2) + .chain_bigint(&self.u3) + .result_bigint(); + + let g_s1 = statement.G.clone() * Scalar::::from(&self.s1); + let e_fe_neg = Scalar::::from(&(Scalar::::group_order() - &e)); + let y_minus_e = statement.Q.clone() * e_fe_neg; + let u1_test = g_s1 + y_minus_e; + + let u2_test_tmp = commitment_unknown_order( + &(&statement.ek.n + BigInt::one()), + &self.s2, + &statement.ek.nn, + &self.s1, + &statement.ek.n, + ); + let u2_test = commitment_unknown_order( + &u2_test_tmp, + &statement.ciphertext, + &statement.ek.nn, + &BigInt::one(), + &(-&e), + ); + + let u3_test_tmp = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &self.s1, + &self.s3, + ); + let u3_test = commitment_unknown_order( + &u3_test_tmp, + &self.z, + &statement.N_tilde, + &BigInt::one(), + &(-&e), + ); + if &self.u1 == &u1_test && &self.u2 == &u2_test && &self.u3 == &u3_test { + Ok(()) + } else { + Err(FsDkrError::PDLwSlackProof { + is_u1_eq: self.u1 == u1_test, + is_u2_eq: self.u2 == u2_test, + is_u3_eq: self.u3 == u3_test, + }) + } + } +} + +pub fn commitment_unknown_order( + h1: &BigInt, + h2: &BigInt, + N_tilde: &BigInt, + x: &BigInt, + r: &BigInt, +) -> BigInt { + let h1_x = BigInt::mod_pow(h1, &x, &N_tilde); + let h2_r = { + if r < &BigInt::zero() { + let h2_inv = BigInt::mod_inv(h2, &N_tilde).unwrap(); + BigInt::mod_pow(&h2_inv, &(-r), &N_tilde) + } else { + BigInt::mod_pow(h2, &r, &N_tilde) + } + }; + let com = BigInt::mod_mul(&h1_x, &h2_r, &N_tilde); + com +} + +#[cfg(test)] +mod test { + use super::*; + use curv::elliptic::curves::secp256_k1::{Secp256k1Point, Secp256k1Scalar}; + use curv::BigInt; + use paillier::core::Randomness; + use paillier::traits::{EncryptWithChosenRandomness, KeyGeneration}; + use paillier::Paillier; + use paillier::RawPlaintext; + use sha2::Sha256; + use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; + + type GE = Secp256k1Point; + type FE = Secp256k1Scalar; + + #[test] + fn test_zk_pdl_with_slack() { + // N_tilde, h1, h2 generation + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + // note: safe primes should be used: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&phi); + let S = BigInt::from(2).pow(256 as u32); + let xhi = BigInt::sample_below(&S); + let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); + let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); + let statement = DLogStatement { + N: ek_tilde.n.clone(), + g: h1.clone(), + ni: h2.clone(), + }; + + let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); + + // generate the scalar secret and Paillier encrypt it + let (ek, _dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + // note: safe primes should be used here as well: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let randomness = Randomness::sample(&ek); + let x: Scalar = Scalar::::random(); + + let Q = Point::::generator().to_point() * &x; + + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint().clone()), + &randomness, + ) + .0 + .into_owned(); + + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: c, + ek, + Q, + G: Point::::generator().to_point(), + h1, + h2, + N_tilde: ek_tilde.n, + }; + + let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; + + let proof = PDLwSlackProof::::prove( + &pdl_w_slack_witness, + &pdl_w_slack_statement, + ); + // verify h1,h2, N_tilde + let setup_result = composite_dlog_proof.verify(&statement); + assert!(setup_result.is_ok()); + let result = proof.verify(&pdl_w_slack_statement); + assert!(result.is_ok()); + } + + #[test] + #[should_panic] + fn test_zk_pdl_with_slack_soundness() { + // N_tilde, h1, h2 generation + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + // note: safe primes should be used: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&phi); + let S = BigInt::from(2).pow(256 as u32); + let xhi = BigInt::sample_below(&S); + let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); + let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); + let statement = DLogStatement { + N: ek_tilde.n.clone(), + g: h1.clone(), + ni: h2.clone(), + }; + + let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); + + // generate the scalar secret and Paillier encrypt it + let (ek, _dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + // note: safe primes should be used here as well: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let randomness = Randomness::sample(&ek); + let x: Scalar = Scalar::::random(); + + let Q = Point::::generator().to_point() * &x; + + // here we encrypt x + 1 instead of x: + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint().clone() + BigInt::one()), + &randomness, + ) + .0 + .into_owned(); + + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: c, + ek, + Q, + G: Point::::generator().to_point(), + h1, + h2, + N_tilde: ek_tilde.n, + }; + + let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; + + let proof = PDLwSlackProof::::prove( + &pdl_w_slack_witness, + &pdl_w_slack_statement, + ); + // verify h1,h2, N_tilde + let setup_result = composite_dlog_proof.verify(&statement); + assert!(setup_result.is_ok()); + let result = proof.verify(&pdl_w_slack_statement); + assert!(result.is_ok()); + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 50318f77..9931eddb 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,5 @@ [toolchain] -channel = "nightly-2022-05-15" +#channel = "nightly-2022-05-15" +channel = "stable" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] \ No newline at end of file From 874cfc37d1cb14d74577cd4c7c32797cdfa6423e Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 09:17:58 +0800 Subject: [PATCH 03/41] Fix dependency tree, update channel. --- .gitignore | 1 + Cargo.lock | 1377 +++++++++++++++++++------------------------ Cargo.toml | 2 +- rust-toolchain.toml | 2 +- 4 files changed, 622 insertions(+), 760 deletions(-) diff --git a/.gitignore b/.gitignore index eb5a316c..9445061f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ target +*.bak diff --git a/Cargo.lock b/Cargo.lock index d06a6d75..62063b4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.17.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -23,7 +23,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", ] [[package]] @@ -32,17 +32,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ - "generic-array 0.14.6", -] - -[[package]] -name = "aead" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8" -dependencies = [ - "crypto-common", - "generic-array 0.14.6", + "generic-array 0.14.7", ] [[package]] @@ -68,17 +58,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "aes" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" -dependencies = [ - "cfg-if 1.0.0", - "cipher 0.4.3", - "cpufeatures", -] - [[package]] name = "aes-gcm" version = "0.8.0" @@ -107,20 +86,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "aes-gcm" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e1366e0c69c9f927b1fa5ce2c7bf9eafc8f9268c0b9800729e8b267612447c" -dependencies = [ - "aead 0.5.1", - "aes 0.8.2", - "cipher 0.4.3", - "ctr 0.9.2", - "ghash 0.5.0", - "subtle", -] - [[package]] name = "aes-soft" version = "0.6.4" @@ -141,6 +106,15 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "aho-corasick" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +dependencies = [ + "memchr", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -152,9 +126,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.66" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "async-channel" @@ -210,9 +184,9 @@ dependencies = [ "log", "parking", "polling", - "rustix", + "rustix 0.37.24", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] @@ -249,7 +223,7 @@ dependencies = [ "async-global-executor", "async-io", "async-lock", - "crossbeam-utils 0.8.12", + "crossbeam-utils 0.8.16", "futures-channel", "futures-core", "futures-io", @@ -267,23 +241,24 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", + "pin-project-lite", ] [[package]] name = "async-stream-impl" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.37", ] [[package]] @@ -294,23 +269,20 @@ checksum = "b9441c6b2fe128a7c2bf680a44c34d0df31ce09e5b7e401fcca3faa483dbc921" [[package]] name = "async-trait" -version = "0.1.58" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.37", ] [[package]] name = "atomic" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c" -dependencies = [ - "autocfg 1.1.0", -] +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" [[package]] name = "atomic-waker" @@ -346,9 +318,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.66" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -419,6 +391,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "bitvec" version = "1.0.1" @@ -450,16 +428,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ "block-padding 0.2.1", - "generic-array 0.14.6", + "generic-array 0.14.7", ] [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", ] [[package]] @@ -493,18 +471,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata", - "serde", -] - [[package]] name = "bulletproof-kzen" version = "1.2.1" @@ -512,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e1ad3c6d12dfa84b0f3d33d3aafccd81a8a493c449216cb5246b66c846900f3" dependencies = [ "curv-kzen", - "generic-array 0.14.6", + "generic-array 0.14.7", "itertools 0.7.11", "serde", "serde_derive", @@ -521,9 +487,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byte-tools" @@ -556,9 +522,9 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "1.2.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cast" @@ -568,9 +534,12 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.74" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "centipede" @@ -580,7 +549,7 @@ checksum = "6c30db9451346358a37cbabe951e4dfdfb34b0c0804df4bfdfc2978cb790a512" dependencies = [ "bulletproof-kzen", "curv-kzen", - "generic-array 0.14.6", + "generic-array 0.14.7", "rayon", "serde", "serde_derive", @@ -610,12 +579,12 @@ dependencies = [ "criterion", "curv-kzen", "digest 0.9.0", - "fs-dkr 0.1.0 (git+https://github.com/webb-tools/fs-dkr)", - "futures 0.3.25", - "generic-array 0.14.6", + "fs-dkr", + "futures 0.3.28", + "generic-array 0.14.7", "hex", "kzen-paillier", - "multi-party-ecdsa 0.8.2", + "multi-party-ecdsa", "rand 0.8.5", "rand_chacha 0.3.1", "reqwest", @@ -627,7 +596,7 @@ dependencies = [ "sha2 0.9.9", "structopt", "thiserror", - "tokio 1.21.2", + "tokio 1.32.0", "uuid 0.8.2", "zeroize", "zk-paillier", @@ -639,7 +608,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", ] [[package]] @@ -648,17 +617,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array 0.14.6", -] - -[[package]] -name = "cipher" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" -dependencies = [ - "crypto-common", - "inout", + "generic-array 0.14.7", ] [[package]] @@ -669,7 +628,7 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", - "bitflags", + "bitflags 1.3.2", "strsim", "textwrap", "unicode-width", @@ -682,7 +641,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -691,14 +650,14 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" dependencies = [ - "crossbeam-utils 0.8.12", + "crossbeam-utils 0.8.16", ] [[package]] name = "const-oid" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "const_fn" @@ -712,7 +671,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" dependencies = [ - "time 0.1.44", + "time 0.1.45", "url 1.7.2", ] @@ -724,9 +683,9 @@ checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" dependencies = [ "aes-gcm 0.8.0", "base64 0.13.1", - "hkdf 0.10.0", + "hkdf", "hmac 0.10.1", - "percent-encoding 2.2.0", + "percent-encoding 2.3.0", "rand 0.8.5", "sha2 0.9.9", "time 0.2.27", @@ -735,19 +694,12 @@ dependencies = [ [[package]] name = "cookie" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" dependencies = [ - "aes-gcm 0.10.1", - "base64 0.13.1", - "hkdf 0.12.3", - "hmac 0.12.1", - "percent-encoding 2.2.0", - "rand 0.8.5", - "sha2 0.10.6", - "subtle", - "time 0.3.17", + "percent-encoding 2.3.0", + "time 0.3.29", "version_check", ] @@ -764,16 +716,16 @@ dependencies = [ "publicsuffix", "serde", "serde_json", - "time 0.1.44", + "time 0.1.45", "try_from", "url 1.7.2", ] [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -829,16 +781,6 @@ dependencies = [ "itertools 0.10.5", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils 0.8.12", -] - [[package]] name = "crossbeam-deque" version = "0.7.4" @@ -852,13 +794,13 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if 1.0.0", - "crossbeam-epoch 0.9.11", - "crossbeam-utils 0.8.12", + "crossbeam-epoch 0.9.15", + "crossbeam-utils 0.8.16", ] [[package]] @@ -878,14 +820,14 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.11" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", - "crossbeam-utils 0.8.12", - "memoffset 0.6.5", + "crossbeam-utils 0.8.16", + "memoffset 0.9.0", "scopeguard", ] @@ -913,9 +855,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if 1.0.0", ] @@ -926,7 +868,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", "rand_core 0.6.4", "subtle", "zeroize", @@ -938,8 +880,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.6", - "rand_core 0.6.4", + "generic-array 0.14.7", "typenum", ] @@ -949,7 +890,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", "subtle", ] @@ -959,7 +900,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", "subtle", ] @@ -971,13 +912,12 @@ checksum = "e35f15e1a0699dd988fed910dd78fdc6407f44654cd12589c91fa44ea67d9159" [[package]] name = "csv" -version = "1.1.6" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086" dependencies = [ - "bstr", "csv-core", - "itoa 0.4.8", + "itoa 1.0.9", "ryu", "serde", ] @@ -991,16 +931,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ctor" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" -dependencies = [ - "quote", - "syn 1.0.103", -] - [[package]] name = "ctr" version = "0.6.0" @@ -1019,15 +949,6 @@ dependencies = [ "cipher 0.3.0", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher 0.4.3", -] - [[package]] name = "curl" version = "0.4.44" @@ -1039,7 +960,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2", + "socket2 0.4.9", "winapi 0.3.9", ] @@ -1056,7 +977,7 @@ dependencies = [ "openssl-sys", "pkg-config", "vcpkg", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1069,12 +990,12 @@ dependencies = [ "curve25519-dalek", "digest 0.9.0", "ff-zeroize", - "generic-array 0.14.6", + "generic-array 0.14.7", "hex", "hmac 0.11.0", "lazy_static", "merkle-cbt", - "num-bigint 0.4.3", + "num-bigint 0.4.4", "num-integer", "num-traits", "p256", @@ -1117,6 +1038,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "derivative" version = "2.2.0" @@ -1125,14 +1052,14 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] name = "devise" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c7580b072f1c8476148f16e0a0d5dedddab787da98d86c5082c5e9ed8ab595" +checksum = "d6eacefd3f541c66fc61433d65e54e0e46e0a029a819a7dbbc7a7b489e8a85f8" dependencies = [ "devise_codegen", "devise_core", @@ -1140,9 +1067,9 @@ dependencies = [ [[package]] name = "devise_codegen" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "123c73e7a6e51b05c75fe1a1b2f4e241399ea5740ed810b0e3e6cacd9db5e7b2" +checksum = "9c8cf4b8dd484ede80fd5c547592c46c3745a617c8af278e2b72bea86b2dfed6" dependencies = [ "devise_core", "quote", @@ -1150,15 +1077,15 @@ dependencies = [ [[package]] name = "devise_core" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841ef46f4787d9097405cac4e70fb8644fc037b526e8c14054247c0263c400d0" +checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" dependencies = [ - "bitflags", + "bitflags 2.4.0", "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 1.0.103", + "syn 2.0.37", ] [[package]] @@ -1176,16 +1103,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.6", + "generic-array 0.14.7", ] [[package]] name = "digest" -version = "0.10.5" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.3", + "block-buffer 0.10.4", "crypto-common", "subtle", ] @@ -1216,9 +1143,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" @@ -1229,9 +1156,9 @@ dependencies = [ "base16ct", "crypto-bigint", "der", - "digest 0.10.5", + "digest 0.10.7", "ff", - "generic-array 0.14.6", + "generic-array 0.14.7", "group", "pkcs8", "rand_core 0.6.4", @@ -1242,13 +1169,19 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.4" @@ -1257,7 +1190,7 @@ checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1294,7 +1227,7 @@ checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", "synstructure", ] @@ -1306,9 +1239,9 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "fastrand" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] @@ -1352,14 +1285,14 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] name = "figment" -version = "0.10.8" +version = "0.10.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e56602b469b2201400dec66a66aec5a9b8761ee97cd1b8c96ab2483fcc16cc9" +checksum = "4547e226f4c9ab860571e070a9034192b3175580ecea38da34fcdb53a018c9a5" dependencies = [ "atomic", "pear", @@ -1371,9 +1304,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide", @@ -1398,39 +1331,21 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ - "percent-encoding 2.2.0", -] - -[[package]] -name = "fs-dkr" -version = "0.1.0" -dependencies = [ - "bitvec", - "curv-kzen", - "kzen-paillier", - "multi-party-ecdsa 0.8.2", - "round-based 0.1.7 (git+https://github.com/webb-tools/round-based-protocol)", - "serde", - "serde_derive", - "sha2 0.9.9", - "thiserror", - "zeroize", - "zk-paillier", + "percent-encoding 2.3.0", ] [[package]] name = "fs-dkr" version = "0.1.0" -source = "git+https://github.com/webb-tools/fs-dkr#94da447f2ad32533664d43655bff518722e4fbdd" dependencies = [ "bitvec", "curv-kzen", "kzen-paillier", - "multi-party-ecdsa 0.8.2 (git+https://github.com/webb-tools/multi-party-ecdsa)", + "multi-party-ecdsa", "round-based 0.1.7 (git+https://github.com/webb-tools/round-based-protocol)", "serde", "serde_derive", @@ -1452,7 +1367,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" dependencies = [ - "bitflags", + "bitflags 1.3.2", "fuchsia-zircon-sys", ] @@ -1476,9 +1391,9 @@ checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" [[package]] name = "futures" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -1491,9 +1406,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -1501,9 +1416,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-cpupool" @@ -1517,9 +1432,9 @@ dependencies = [ [[package]] name = "futures-executor" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -1538,7 +1453,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand 1.8.0", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -1549,32 +1464,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.37", ] [[package]] name = "futures-sink" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -1590,9 +1505,9 @@ dependencies = [ [[package]] name = "generator" -version = "0.7.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc184cace1cea8335047a471cc1da80f18acf8a76f3bab2028d499e328948ec7" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" dependencies = [ "cc", "libc", @@ -1612,9 +1527,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1633,9 +1548,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if 1.0.0", "libc", @@ -1662,27 +1577,17 @@ dependencies = [ "polyval 0.5.3", ] -[[package]] -name = "ghash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" -dependencies = [ - "opaque-debug 0.3.0", - "polyval 0.6.0", -] - [[package]] name = "gimli" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gloo-timers" @@ -1718,7 +1623,7 @@ dependencies = [ "fnv", "futures 0.1.31", "http 0.1.21", - "indexmap", + "indexmap 1.9.3", "log", "slab", "string", @@ -1737,6 +1642,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" + [[package]] name = "heck" version = "0.3.3" @@ -1780,15 +1691,6 @@ dependencies = [ "hmac 0.10.1", ] -[[package]] -name = "hkdf" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" -dependencies = [ - "hmac 0.12.1", -] - [[package]] name = "hmac" version = "0.10.1" @@ -1815,7 +1717,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.5", + "digest 0.10.7", ] [[package]] @@ -1831,13 +1733,13 @@ dependencies = [ [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ - "bytes 1.2.1", + "bytes 1.5.0", "fnv", - "itoa 1.0.4", + "itoa 1.0.9", ] [[package]] @@ -1858,8 +1760,8 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes 1.2.1", - "http 0.2.8", + "bytes 1.5.0", + "http 0.2.9", "pin-project-lite", ] @@ -1896,7 +1798,7 @@ dependencies = [ "serde_json", "serde_qs", "serde_urlencoded 0.7.1", - "url 2.3.1", + "url 2.4.1", ] [[package]] @@ -1907,9 +1809,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" @@ -1929,7 +1831,7 @@ dependencies = [ "log", "net2", "rustc_version", - "time 0.1.44", + "time 0.1.45", "tokio 0.1.22", "tokio-buf", "tokio-executor", @@ -1943,25 +1845,25 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.23" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ - "bytes 1.2.1", + "bytes 1.5.0", "futures-channel", "futures-core", "futures-util", - "http 0.2.8", + "http 0.2.9", "http-body 0.4.5", "httparse", "httpdate", - "itoa 1.0.4", + "itoa 1.0.9", "pin-project-lite", - "socket2", - "tokio 1.21.2", + "socket2 0.4.9", + "tokio 1.32.0", "tower-service", "tracing", - "want 0.3.0", + "want 0.3.1", ] [[package]] @@ -1988,9 +1890,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1998,15 +1900,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg 1.1.0", - "hashbrown", + "hashbrown 0.12.3", "serde", ] +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown 0.14.1", +] + [[package]] name = "infer" version = "0.2.3" @@ -2019,15 +1931,6 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array 0.14.6", -] - [[package]] name = "instant" version = "0.1.12" @@ -2045,7 +1948,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.3", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -2057,6 +1960,17 @@ dependencies = [ "libc", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.3", + "rustix 0.38.15", + "windows-sys", +] + [[package]] name = "isahc" version = "0.9.14" @@ -2064,19 +1978,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2948a0ce43e2c2ef11d7edf6816508998d99e13badd1150be0914205df9388a" dependencies = [ "bytes 0.5.6", - "crossbeam-utils 0.8.12", + "crossbeam-utils 0.8.16", "curl", "curl-sys", "flume", "futures-lite", - "http 0.2.8", + "http 0.2.9", "log", "once_cell", "slab", "sluice", "tracing", "tracing-futures", - "url 2.3.1", + "url 2.4.1", "waker-fn", ] @@ -2106,24 +2020,27 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] [[package]] name = "keccak" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] [[package]] name = "kernel32-sys" @@ -2195,6 +2112,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "linux-raw-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" + [[package]] name = "lock_api" version = "0.3.4" @@ -2206,9 +2129,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -2216,11 +2139,10 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ - "cfg-if 1.0.0", "value-bag", ] @@ -2245,14 +2167,14 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] name = "matches" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "maybe-uninit" @@ -2262,9 +2184,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" @@ -2277,9 +2199,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.6.5" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg 1.1.0", ] @@ -2295,9 +2217,9 @@ dependencies = [ [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" @@ -2311,9 +2233,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] @@ -2339,14 +2261,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] @@ -2363,20 +2284,20 @@ dependencies = [ [[package]] name = "multer" -version = "2.0.4" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed4198ce7a4cbd2a57af78d28c6fbb57d81ac5f1d6ad79ac6c5587419cbdf22" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" dependencies = [ - "bytes 1.2.1", + "bytes 1.5.0", "encoding_rs", "futures-util", - "http 0.2.8", + "http 0.2.9", "httparse", "log", "memchr", "mime", "spin", - "tokio 1.21.2", + "tokio 1.32.0", "tokio-util", "version_check", ] @@ -2392,7 +2313,7 @@ dependencies = [ "criterion", "curv-kzen", "derivative", - "futures 0.3.25", + "futures 0.3.28", "hex", "kzen-paillier", "log", @@ -2408,36 +2329,17 @@ dependencies = [ "subtle", "surf", "thiserror", - "tokio 1.21.2", + "tokio 1.32.0", "uuid 0.8.2", "zeroize", "zk-paillier", ] -[[package]] -name = "multi-party-ecdsa" -version = "0.8.2" -source = "git+https://github.com/webb-tools/multi-party-ecdsa#facf26da1bee74f6bf10ebfba58bc8828d74c6a9" -dependencies = [ - "centipede", - "curv-kzen", - "derivative", - "kzen-paillier", - "log", - "round-based 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "serde", - "sha2 0.9.9", - "subtle", - "thiserror", - "zeroize", - "zk-paillier", -] - [[package]] name = "net2" -version = "0.2.38" +version = "0.2.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d0df99cfcd2530b2e694f6e17e7f37b8e26bb23983ac530c0c97408837c631" +checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" dependencies = [ "cfg-if 0.1.10", "libc", @@ -2467,9 +2369,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg 1.1.0", "num-integer", @@ -2489,37 +2391,37 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg 1.1.0", ] [[package]] name = "num_cpus" -version = "1.14.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi 0.3.3", "libc", ] [[package]] name = "object" -version = "0.29.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oorandom" @@ -2571,7 +2473,7 @@ checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ "ecdsa", "elliptic-curve", - "sha2 0.10.6", + "sha2 0.10.8", ] [[package]] @@ -2602,7 +2504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" dependencies = [ "lock_api 0.3.4", - "parking_lot_core 0.6.2", + "parking_lot_core 0.6.3", "rustc_version", ] @@ -2612,15 +2514,15 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "lock_api 0.4.9", - "parking_lot_core 0.9.4", + "lock_api 0.4.10", + "parking_lot_core 0.9.8", ] [[package]] name = "parking_lot_core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" +checksum = "bda66b810a62be75176a80873726630147a5ca780cd33921e0b5709033e66b0a" dependencies = [ "cfg-if 0.1.10", "cloudabi", @@ -2633,38 +2535,38 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", - "smallvec 1.10.0", - "windows-sys 0.42.0", + "redox_syscall 0.3.5", + "smallvec 1.11.1", + "windows-targets", ] [[package]] name = "pear" -version = "0.2.3" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e44241c5e4c868e3eaa78b7c1848cadd6344ed4f54d029832d32b415a58702" +checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c" dependencies = [ "inlinable_string", "pear_codegen", - "yansi", + "yansi 1.0.0-rc.1", ] [[package]] name = "pear_codegen" -version = "0.2.3" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a5ca643c2303ecb740d506539deba189e16f2754040a42901cd8105d0282d0" +checksum = "da9f0f13dac8069c139e8300a6510e3f4143ecf5259c60b116a9b271b4ca0d54" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 1.0.103", + "syn 2.0.37", ] [[package]] @@ -2675,9 +2577,9 @@ checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project" @@ -2701,9 +2603,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2740,9 +2642,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plotters" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" dependencies = [ "num-traits", "plotters-backend", @@ -2753,15 +2655,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" [[package]] name = "plotters-svg" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" dependencies = [ "plotters-backend", ] @@ -2773,13 +2675,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg 1.1.0", - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "concurrent-queue", "libc", "log", "pin-project-lite", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -2790,7 +2692,7 @@ checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" dependencies = [ "cpuid-bool", "opaque-debug 0.3.0", - "universal-hash 0.4.1", + "universal-hash", ] [[package]] @@ -2802,19 +2704,7 @@ dependencies = [ "cfg-if 1.0.0", "cpufeatures", "opaque-debug 0.3.0", - "universal-hash 0.4.1", -] - -[[package]] -name = "polyval" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" -dependencies = [ - "cfg-if 1.0.0", - "cpufeatures", - "opaque-debug 0.3.0", - "universal-hash 0.5.0", + "universal-hash", ] [[package]] @@ -2832,7 +2722,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", "version_check", ] @@ -2864,15 +2754,15 @@ dependencies = [ [[package]] name = "proc-macro2-diagnostics" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.37", "version_check", - "yansi", + "yansi 1.0.0-rc.1", ] [[package]] @@ -2882,7 +2772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95b4ce31ff0a27d93c8de1849cf58162283752f065a90d508f1105fa6c9a213f" dependencies = [ "idna 0.2.3", - "url 2.3.1", + "url 2.4.1", ] [[package]] @@ -3016,7 +2906,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.10", ] [[package]] @@ -3101,26 +2991,22 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ - "autocfg 1.1.0", - "crossbeam-deque 0.8.2", "either", "rayon-core", ] [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-channel", - "crossbeam-deque 0.8.2", - "crossbeam-utils 0.8.12", - "num_cpus", + "crossbeam-deque 0.8.3", + "crossbeam-utils 0.8.16", ] [[package]] @@ -3140,40 +3026,43 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "ref-cast" -version = "1.0.13" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b15debb4f9d60d767cd8ca9ef7abb2452922f3214671ff052defc7f3502c44" +checksum = "acde58d073e9c79da00f2b5b84eed919c8326832648a5b109b3fce1bb1175280" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.13" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abfa8511e9e94fd3de6585a3d3cd00e01ed556dc9814829280af0e8dc72a8f36" +checksum = "7f7473c2cfcf90008193dd0e3e16599455cb601a9fce322b5bb55de799664925" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.37", ] [[package]] name = "regex" -version = "1.7.0" +version = "1.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" dependencies = [ - "regex-syntax", + "aho-corasick", + "memchr", + "regex-automata 0.3.9", + "regex-syntax 0.7.5", ] [[package]] @@ -3182,23 +3071,31 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax", + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", ] [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "regex-syntax" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi 0.3.9", -] +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "reqwest" @@ -3221,7 +3118,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded 0.5.5", - "time 0.1.44", + "time 0.1.45", "tokio 0.1.22", "tokio-executor", "tokio-io", @@ -3245,20 +3142,20 @@ dependencies = [ [[package]] name = "rocket" -version = "0.5.0-rc.2" +version = "0.5.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ead083fce4a405feb349cf09abdf64471c6077f14e0ce59364aa90d4b99317" +checksum = "58734f7401ae5cfd129685b48f61182331745b357b96f2367f01aebaf1cc9cc9" dependencies = [ "async-stream", "async-trait", "atomic", - "atty", "binascii", - "bytes 1.2.1", + "bytes 1.5.0", "either", "figment", - "futures 0.3.25", - "indexmap", + "futures 0.3.28", + "indexmap 1.9.3", + "is-terminal", "log", "memchr", "multer", @@ -3273,55 +3170,55 @@ dependencies = [ "serde_json", "state", "tempfile", - "time 0.3.17", - "tokio 1.21.2", + "time 0.3.29", + "tokio 1.32.0", "tokio-stream", "tokio-util", "ubyte", "version_check", - "yansi", + "yansi 0.5.1", ] [[package]] name = "rocket_codegen" -version = "0.5.0-rc.2" +version = "0.5.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6aeb6bb9c61e9cd2c00d70ea267bf36f76a4cc615e5908b349c2f9d93999b47" +checksum = "7093353f14228c744982e409259fb54878ba9563d08214f2d880d59ff2fc508b" dependencies = [ "devise", "glob", - "indexmap", + "indexmap 1.9.3", "proc-macro2", "quote", "rocket_http", - "syn 1.0.103", + "syn 2.0.37", "unicode-xid", ] [[package]] name = "rocket_http" -version = "0.5.0-rc.2" +version = "0.5.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ded65d127954de3c12471630bf4b81a2792f065984461e65b91d0fdaafc17a2" +checksum = "936012c99162a03a67f37f9836d5f938f662e26f2717809761a9ac46432090f4" dependencies = [ - "cookie 0.16.1", + "cookie 0.17.0", "either", - "futures 0.3.25", - "http 0.2.8", - "hyper 0.14.23", - "indexmap", + "futures 0.3.28", + "http 0.2.9", + "hyper 0.14.27", + "indexmap 1.9.3", "log", "memchr", "pear", - "percent-encoding 2.2.0", + "percent-encoding 2.3.0", "pin-project-lite", "ref-cast", "serde", - "smallvec 1.10.0", + "smallvec 1.11.1", "stable-pattern", "state", - "time 0.3.17", - "tokio 1.21.2", + "time 0.3.29", + "tokio 1.32.0", "uncased", ] @@ -3332,11 +3229,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61d7da583ffbf4d938fb9dc60871b51769ff47e9836e323668fe2d791ca2fa06" dependencies = [ "async-stream", - "futures 0.3.25", + "futures 0.3.28", "log", "serde", "thiserror", - "tokio 1.21.2", + "tokio 1.32.0", ] [[package]] @@ -3345,11 +3242,11 @@ version = "0.1.7" source = "git+https://github.com/webb-tools/round-based-protocol#959126f9f6edce16d4ee95954091b93e33a83140" dependencies = [ "async-stream", - "futures 0.3.25", + "futures 0.3.28", "log", "serde", "thiserror", - "tokio 1.21.2", + "tokio 1.32.0", ] [[package]] @@ -3365,9 +3262,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc_version" @@ -3384,25 +3281,38 @@ version = "0.37.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4279d76516df406a8bd37e7dff53fd37d1a093f997a3c34a5c21658c126db06d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", - "windows-sys 0.48.0", + "linux-raw-sys 0.3.8", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "0.38.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.8", + "windows-sys", ] [[package]] name = "rustversion" -version = "1.0.9" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "same-file" @@ -3419,7 +3329,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -3430,9 +3340,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sec1" @@ -3442,7 +3352,7 @@ checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ "base16ct", "der", - "generic-array 0.14.6", + "generic-array 0.14.7", "pkcs8", "subtle", "zeroize", @@ -3485,18 +3395,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.147" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.7" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" dependencies = [ "serde", ] @@ -3513,22 +3423,22 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.37", ] [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ - "itoa 1.0.4", + "itoa 1.0.9", "ryu", "serde", ] @@ -3539,11 +3449,20 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" dependencies = [ - "percent-encoding 2.2.0", + "percent-encoding 2.3.0", "serde", "thiserror", ] +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.5.5" @@ -3563,7 +3482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.4", + "itoa 1.0.9", "ryu", "serde", ] @@ -3610,13 +3529,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.5", + "digest 0.10.7", ] [[package]] @@ -3633,18 +3552,18 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "c1b21f559e07218024e7e9f90f96f601825397de0e25420135f7f952453fed0b" dependencies = [ "lazy_static", ] [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] @@ -3655,15 +3574,15 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ - "digest 0.10.5", + "digest 0.10.7", "rand_core 0.6.4", ] [[package]] name = "slab" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg 1.1.0", ] @@ -3690,25 +3609,35 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi 0.3.9", ] +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" -version = "0.9.4" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "spinning_top" @@ -3716,7 +3645,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b9eb1a2f4c41445a3a0ff9abc5221c5fcd28e1f13cd7c0397706f9ac938ddb0" dependencies = [ - "lock_api 0.4.9", + "lock_api 0.4.10", ] [[package]] @@ -3780,7 +3709,7 @@ dependencies = [ "quote", "serde", "serde_derive", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] @@ -3796,7 +3725,7 @@ dependencies = [ "serde_derive", "serde_json", "sha1", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] @@ -3841,7 +3770,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] @@ -3861,7 +3790,7 @@ dependencies = [ "cfg-if 1.0.0", "encoding_rs", "futures-util", - "getrandom 0.2.8", + "getrandom 0.2.10", "http-client", "http-types", "log", @@ -3875,9 +3804,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.103" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -3903,7 +3832,7 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", "unicode-xid", ] @@ -3915,16 +3844,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.3.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if 1.0.0", - "fastrand 1.8.0", - "libc", - "redox_syscall 0.2.16", - "remove_dir_all", - "winapi 0.3.9", + "fastrand 2.0.1", + "redox_syscall 0.3.5", + "rustix 0.38.15", + "windows-sys", ] [[package]] @@ -3938,38 +3866,39 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.37", ] [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if 1.0.0", "once_cell", ] [[package]] name = "time" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", @@ -3993,21 +3922,22 @@ dependencies = [ [[package]] name = "time" -version = "0.3.17" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ - "itoa 1.0.4", + "deranged", + "itoa 1.0.9", "serde", "time-core", - "time-macros 0.2.6", + "time-macros 0.2.15", ] [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" @@ -4021,9 +3951,9 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.2.6" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -4038,7 +3968,7 @@ dependencies = [ "proc-macro2", "quote", "standback", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] @@ -4062,9 +3992,9 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" @@ -4087,21 +4017,20 @@ dependencies = [ [[package]] name = "tokio" -version = "1.21.2" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg 1.1.0", - "bytes 1.2.1", + "backtrace", + "bytes 1.5.0", "libc", - "memchr", - "mio 0.8.5", + "mio 0.8.8", "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.4", "tokio-macros", - "winapi 0.3.9", + "windows-sys", ] [[package]] @@ -4148,13 +4077,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.37", ] [[package]] @@ -4178,13 +4107,13 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", - "tokio 1.21.2", + "tokio 1.32.0", ] [[package]] @@ -4242,24 +4171,49 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ - "bytes 1.2.1", + "bytes 1.5.0", "futures-core", "futures-sink", "pin-project-lite", - "tokio 1.21.2", + "tokio 1.32.0", ] [[package]] name = "toml" -version = "0.5.9" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ + "indexmap 2.0.2", "serde", + "serde_spanned", + "toml_datetime", + "winnow", ] [[package]] @@ -4283,20 +4237,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.37", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", @@ -4325,16 +4279,16 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex", "sharded-slab", - "smallvec 1.10.0", + "smallvec 1.11.1", "thread_local", "tracing", "tracing-core", @@ -4343,9 +4297,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "try_from" @@ -4358,9 +4312,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ubyte" @@ -4373,9 +4327,9 @@ dependencies = [ [[package]] name = "uncased" -version = "0.9.7" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" +checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" dependencies = [ "serde", "version_check", @@ -4383,24 +4337,24 @@ dependencies = [ [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -4413,15 +4367,15 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode-xid" @@ -4435,17 +4389,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" dependencies = [ - "generic-array 0.14.6", - "subtle", -] - -[[package]] -name = "universal-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" -dependencies = [ - "crypto-common", + "generic-array 0.14.7", "subtle", ] @@ -4462,13 +4406,13 @@ dependencies = [ [[package]] name = "url" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", - "idna 0.3.0", - "percent-encoding 2.2.0", + "idna 0.4.0", + "percent-encoding 2.3.0", "serde", ] @@ -4487,7 +4431,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.10", ] [[package]] @@ -4498,13 +4442,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.0.0-alpha.9" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" -dependencies = [ - "ctor", - "version_check", -] +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" [[package]] name = "vcpkg" @@ -4532,12 +4472,11 @@ checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", - "winapi 0.3.9", "winapi-util", ] @@ -4554,11 +4493,10 @@ dependencies = [ [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -4582,9 +4520,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -4592,24 +4530,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.37", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.33" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -4619,9 +4557,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4629,28 +4567,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -4686,9 +4624,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi 0.3.9", ] @@ -4701,30 +4639,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" -dependencies = [ - "windows_aarch64_msvc 0.32.0", - "windows_i686_gnu 0.32.0", - "windows_i686_msvc 0.32.0", - "windows_x86_64_gnu 0.32.0", - "windows_x86_64_msvc 0.32.0", -] - -[[package]] -name = "windows-sys" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows_aarch64_gnullvm 0.42.0", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm 0.42.0", - "windows_x86_64_msvc 0.42.0", + "windows-targets", ] [[package]] @@ -4742,105 +4661,45 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" -[[package]] -name = "windows_aarch64_msvc" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" -[[package]] -name = "windows_i686_gnu" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" - [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" -[[package]] -name = "windows_i686_msvc" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" - [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" -[[package]] -name = "windows_x86_64_gnu" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4849,21 +4708,18 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.32.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.0" +name = "winnow" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +dependencies = [ + "memchr", +] [[package]] name = "winreg" @@ -4886,9 +4742,9 @@ dependencies = [ [[package]] name = "wyz" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ "tap", ] @@ -4899,25 +4755,30 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +[[package]] +name = "yansi" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" + [[package]] name = "zeroize" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", - "synstructure", + "syn 2.0.37", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 016203ce..bb1481d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ zeroize = "1" curv-kzen = { version = "0.10", default-features = false } centipede = { version = "0.3.1", default-features = false } zk-paillier = { version = "0.4.4", default-features = false } -fs-dkr = { git = "https://github.com/webb-tools/fs-dkr", default-features = false } +fs-dkr = { path = "fs-dkr", default-features = false } round-based = { version = "0.1.4", features = [] } thiserror = "1.0.23" sha2 = "0.9" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9931eddb..ab3566a0 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] #channel = "nightly-2022-05-15" -channel = "stable" +channel = "nightly-2023-10-01" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] \ No newline at end of file From 822019588242367e82091ed7511cd52b1975b008 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 09:31:35 +0800 Subject: [PATCH 04/41] Fix round-based dependency. Must also use the fork in the main Cargo.toml file otherwise we get conflicts with trait implementations when running the tests. --- Cargo.lock | 20 +++----------------- Cargo.toml | 5 ++--- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62063b4f..bcd1c236 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -589,7 +589,7 @@ dependencies = [ "rand_chacha 0.3.1", "reqwest", "rocket", - "round-based 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "round-based", "secp256k1", "serde", "serde_json", @@ -1346,7 +1346,7 @@ dependencies = [ "curv-kzen", "kzen-paillier", "multi-party-ecdsa", - "round-based 0.1.7 (git+https://github.com/webb-tools/round-based-protocol)", + "round-based", "serde", "serde_derive", "sha2 0.9.9", @@ -2320,7 +2320,7 @@ dependencies = [ "rand 0.8.5", "reqwest", "rocket", - "round-based 0.1.7 (git+https://github.com/webb-tools/round-based-protocol)", + "round-based", "secp256k1", "serde", "serde_json", @@ -3222,20 +3222,6 @@ dependencies = [ "uncased", ] -[[package]] -name = "round-based" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61d7da583ffbf4d938fb9dc60871b51769ff47e9836e323668fe2d791ca2fa06" -dependencies = [ - "async-stream", - "futures 0.3.28", - "log", - "serde", - "thiserror", - "tokio 1.32.0", -] - [[package]] name = "round-based" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index bb1481d3..df6a0c15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,8 @@ curv-kzen = { version = "0.10", default-features = false } centipede = { version = "0.3.1", default-features = false } zk-paillier = { version = "0.4.4", default-features = false } fs-dkr = { path = "fs-dkr", default-features = false } -round-based = { version = "0.1.4", features = [] } +#round-based = { version = "0.1.4", features = [] } +round-based = { git = "https://github.com/webb-tools/round-based-protocol", version = "0.1.4", features = ["dev"] } thiserror = "1.0.23" sha2 = "0.9" digest = "0.9" @@ -54,9 +55,7 @@ rand = "0.8" anyhow = "1" structopt = "0.3" secp256k1 = { version = "0.20", features = ["global-context"]} - thiserror = "1.0.23" -round-based = { version = "0.1.4", features = ["dev"] } [features] default = ["rust-gmp-kzen"] From 2dc717602cb7d68c8083594c31b6d44c4467c7b7 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 09:35:54 +0800 Subject: [PATCH 05/41] Do not specify toolchain in worksflow. --- .github/workflows/tests.yml | 2 +- Cargo.toml | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7faef1af..3ba39d40 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,7 @@ jobs: - name: Install Toolchain uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2022-05-15 + #toolchain: nightly-2022-05-15 components: rustfmt, clippy target: wasm32-unknown-unknown override: true diff --git a/Cargo.toml b/Cargo.toml index df6a0c15..3e21917b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ curv-kzen = { version = "0.10", default-features = false } centipede = { version = "0.3.1", default-features = false } zk-paillier = { version = "0.4.4", default-features = false } fs-dkr = { path = "fs-dkr", default-features = false } -#round-based = { version = "0.1.4", features = [] } round-based = { git = "https://github.com/webb-tools/round-based-protocol", version = "0.1.4", features = ["dev"] } thiserror = "1.0.23" sha2 = "0.9" @@ -55,7 +54,6 @@ rand = "0.8" anyhow = "1" structopt = "0.3" secp256k1 = { version = "0.20", features = ["global-context"]} -thiserror = "1.0.23" [features] default = ["rust-gmp-kzen"] From 0bf570a2f07dbe32f8d915c1143653103fe50859 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 09:38:50 +0800 Subject: [PATCH 06/41] Format code. --- fs-dkr/src/add_party_message.rs | 504 ++++++----- fs-dkr/src/error.rs | 97 +-- fs-dkr/src/lib.rs | 1 - fs-dkr/src/range_proofs.rs | 1285 ++++++++++++++--------------- fs-dkr/src/refresh_message.rs | 885 ++++++++++---------- fs-dkr/src/ring_pedersen_proof.rs | 268 +++--- fs-dkr/src/test.rs | 809 +++++++++--------- fs-dkr/src/zk_pdl_with_slack.rs | 575 +++++++------ 8 files changed, 2144 insertions(+), 2280 deletions(-) diff --git a/fs-dkr/src/add_party_message.rs b/fs-dkr/src/add_party_message.rs index 7151c4df..d998cf6c 100644 --- a/fs-dkr/src/add_party_message.rs +++ b/fs-dkr/src/add_party_message.rs @@ -3,29 +3,34 @@ //! * A new party wants to join, broadcasting a paillier ek, correctness of the ek generation, //! dlog statements and dlog proofs. //! * All the existing parties receives the join message. We assume for now that everyone accepts -//! the new party. All parties pick an index and add the new ek to their LocalKey at the given index. +//! the new party. All parties pick an index and add the new ek to their LocalKey at the given +//! index. //! * The party index of the new party is transmitted back to the joining party offchannel (it's //! public information). //! * All the existing parties enter the distribute phase, in which they start refreshing their //! existing keys taking into the account the join messages that they received. //! ** All parties (including new ones) collect the refresh messages and the join messages. -use crate::error::{FsDkrError, FsDkrResult}; -use crate::refresh_message::RefreshMessage; -use curv::arithmetic::{BasicOps, Modulo, One, Samplable, Zero}; -use curv::cryptographic_primitives::hashing::Digest; -use curv::cryptographic_primitives::secret_sharing::feldman_vss::{ - ShamirSecretSharing, VerifiableSS, +use crate::{ + error::{FsDkrError, FsDkrResult}, + refresh_message::RefreshMessage, +}; +use curv::{ + arithmetic::{BasicOps, Modulo, One, Samplable, Zero}, + cryptographic_primitives::{ + hashing::Digest, + secret_sharing::feldman_vss::{ShamirSecretSharing, VerifiableSS}, + }, + elliptic::curves::{Curve, Point, Scalar}, + BigInt, +}; +use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::{ + party_i::{Keys, SharedKeys}, + state_machine::keygen::LocalKey, }; -use curv::elliptic::curves::{Curve, Point, Scalar}; -use curv::BigInt; -use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::party_i::Keys; -use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::party_i::SharedKeys; -use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::keygen::LocalKey; use paillier::{Decrypt, EncryptionKey, KeyGeneration, Paillier}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fmt::Debug; +use std::{collections::HashMap, fmt::Debug}; use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement, NiCorrectKeyProof}; use crate::ring_pedersen_proof::{RingPedersenProof, RingPedersenStatement}; @@ -34,264 +39,239 @@ use crate::ring_pedersen_proof::{RingPedersenProof, RingPedersenStatement}; #[derive(Clone, Deserialize, Serialize, Debug)] #[serde(bound = "E: Curve, H: Digest + Clone")] pub struct JoinMessage { - pub(crate) ek: EncryptionKey, - pub(crate) dk_correctness_proof: NiCorrectKeyProof, - pub(crate) party_index: Option, - pub(crate) dlog_statement: DLogStatement, - pub(crate) composite_dlog_proof_base_h1: CompositeDLogProof, - pub(crate) composite_dlog_proof_base_h2: CompositeDLogProof, - pub(crate) ring_pedersen_statement: RingPedersenStatement, - pub(crate) ring_pedersen_proof: RingPedersenProof, + pub(crate) ek: EncryptionKey, + pub(crate) dk_correctness_proof: NiCorrectKeyProof, + pub(crate) party_index: Option, + pub(crate) dlog_statement: DLogStatement, + pub(crate) composite_dlog_proof_base_h1: CompositeDLogProof, + pub(crate) composite_dlog_proof_base_h2: CompositeDLogProof, + pub(crate) ring_pedersen_statement: RingPedersenStatement, + pub(crate) ring_pedersen_proof: RingPedersenProof, } /// Generates the parameters needed for the h1_h2_N_tilde_vec. These parameters can be seen as /// environment variables for each party that they agree on. In this case, each new party generates /// it's own DlogStatements and submits it's proofs fn generate_h1_h2_n_tilde() -> (BigInt, BigInt, BigInt, BigInt, BigInt) { - let (ek_tilde, dk_tilde) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&ek_tilde.n); - let (mut xhi, mut xhi_inv) = loop { - let xhi_ = BigInt::sample_below(&phi); - match BigInt::mod_inv(&xhi_, &phi) { - Some(inv) => break (xhi_, inv), - None => continue, - } - }; - let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); - xhi = BigInt::sub(&phi, &xhi); - xhi_inv = BigInt::sub(&phi, &xhi_inv); - (ek_tilde.n, h1, h2, xhi, xhi_inv) + let (ek_tilde, dk_tilde) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&ek_tilde.n); + let (mut xhi, mut xhi_inv) = loop { + let xhi_ = BigInt::sample_below(&phi); + match BigInt::mod_inv(&xhi_, &phi) { + Some(inv) => break (xhi_, inv), + None => continue, + } + }; + let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); + xhi = BigInt::sub(&phi, &xhi); + xhi_inv = BigInt::sub(&phi, &xhi_inv); + (ek_tilde.n, h1, h2, xhi, xhi_inv) } -/// Generates the DlogStatement and CompositeProofs using the parameters generated by [generate_h1_h2_n_tilde] +/// Generates the DlogStatement and CompositeProofs using the parameters generated by +/// [generate_h1_h2_n_tilde] fn generate_dlog_statement_proofs() -> (DLogStatement, CompositeDLogProof, CompositeDLogProof) { - let (n_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_n_tilde(); - - let dlog_statement_base_h1 = DLogStatement { - N: n_tilde.clone(), - g: h1.clone(), - ni: h2.clone(), - }; - - let dlog_statement_base_h2 = DLogStatement { - N: n_tilde, - g: h2, - ni: h1, - }; - - let composite_dlog_proof_base_h1 = CompositeDLogProof::prove(&dlog_statement_base_h1, &xhi); - let composite_dlog_proof_base_h2 = CompositeDLogProof::prove(&dlog_statement_base_h2, &xhi_inv); - - ( - dlog_statement_base_h1, - composite_dlog_proof_base_h1, - composite_dlog_proof_base_h2, - ) + let (n_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_n_tilde(); + + let dlog_statement_base_h1 = + DLogStatement { N: n_tilde.clone(), g: h1.clone(), ni: h2.clone() }; + + let dlog_statement_base_h2 = DLogStatement { N: n_tilde, g: h2, ni: h1 }; + + let composite_dlog_proof_base_h1 = CompositeDLogProof::prove(&dlog_statement_base_h1, &xhi); + let composite_dlog_proof_base_h2 = CompositeDLogProof::prove(&dlog_statement_base_h2, &xhi_inv); + + (dlog_statement_base_h1, composite_dlog_proof_base_h1, composite_dlog_proof_base_h2) } impl JoinMessage { - pub fn set_party_index(&mut self, new_party_index: u16) { - self.party_index = Some(new_party_index); - } - /// The distribute phase for a new party. This distribute phase has to happen before the existing - /// parties distribute. Calling this function will generate a JoinMessage and a pair of Paillier - /// [Keys] that are going to be used when generating the [LocalKey]. - pub fn distribute() -> (Self, Keys) { - let paillier_key_pair = Keys::create(0); - let (dlog_statement, composite_dlog_proof_base_h1, composite_dlog_proof_base_h2) = - generate_dlog_statement_proofs(); - - let (ring_pedersen_statement, ring_pedersen_witness) = RingPedersenStatement::generate(); - - let ring_pedersen_proof = - RingPedersenProof::prove(&ring_pedersen_witness, &ring_pedersen_statement); - - let join_message = JoinMessage { - // in a join message, we only care about the ek and the correctness proof - ek: paillier_key_pair.ek.clone(), - dk_correctness_proof: NiCorrectKeyProof::proof(&paillier_key_pair.dk, None), - dlog_statement, - composite_dlog_proof_base_h1, - composite_dlog_proof_base_h2, - ring_pedersen_statement, - ring_pedersen_proof, - party_index: None, - }; - - (join_message, paillier_key_pair) - } - /// Returns the party index if it has been assigned one, throws - /// [FsDkrError::NewPartyUnassignedIndexError] otherwise - pub fn get_party_index(&self) -> FsDkrResult { - self.party_index - .ok_or(FsDkrError::NewPartyUnassignedIndexError) - } - - /// Collect phase of the protocol. Compared to the [RefreshMessage::collect], this has to be - /// tailored for a sent JoinMessage on which we assigned party_index. In this collect, a [LocalKey] - /// is filled with the information provided by the [RefreshMessage]s from the other parties and - /// the other join messages (multiple parties can be added/replaced at once). - pub fn collect( - &self, - refresh_messages: &[RefreshMessage], - paillier_key: Keys, - join_messages: &[JoinMessage], - new_t: u16, - new_n: u16, - current_t: u16, - ) -> FsDkrResult> { - RefreshMessage::validate_collect(refresh_messages, current_t, new_n)?; - - for refresh_message in refresh_messages.iter() { - RingPedersenProof::verify( - &refresh_message.ring_pedersen_proof, - &refresh_message.ring_pedersen_statement, - ) - .map_err(|_| FsDkrError::RingPedersenProofValidation { - party_index: refresh_message.party_index, - })?; - } - - for join_message in join_messages.iter() { - RingPedersenProof::verify( - &join_message.ring_pedersen_proof, - &join_message.ring_pedersen_statement, - ) - .map_err(|e| { - if let Some(party_index) = join_message.party_index { - FsDkrError::RingPedersenProofValidation { party_index } - } else { - e - } - })?; - } - - // check if a party_index has been assigned to the current party - let party_index = self.get_party_index()?; - - // check if a party_index has been assigned to all other new parties - // TODO: Check if no party_index collision exists - for join_message in join_messages.iter() { - join_message.get_party_index()?; - } - - let parameters = ShamirSecretSharing { - threshold: new_t, - share_count: new_n, - }; - - // generate a new share, the details can be found here https://hackmd.io/@omershlo/Hy1jBo6JY. - let (cipher_text_sum, li_vec) = RefreshMessage::get_ciphertext_sum( - refresh_messages, - party_index, - ¶meters, - &paillier_key.ek, - current_t - ); - let new_share = Paillier::decrypt(&paillier_key.dk, cipher_text_sum) - .0 - .into_owned(); - - let new_share_fe: Scalar = Scalar::::from(&new_share); - let paillier_dk = paillier_key.dk.clone(); - let key_linear_x_i = new_share_fe.clone(); - let key_linear_y = Point::::generator() * new_share_fe.clone(); - let keys_linear = SharedKeys { - x_i: key_linear_x_i, - y: key_linear_y, - }; - let mut pk_vec: Vec<_> = (0..new_n as usize) - .map(|i| refresh_messages[0].points_committed_vec[i].clone() * li_vec[0].clone()) - .collect(); - - for i in 0..new_n as usize { - for j in 1..(current_t + 1) as usize { - pk_vec[i] = pk_vec[i].clone() - + refresh_messages[j].points_committed_vec[i].clone() * li_vec[j].clone(); - } - } - - // check what parties are assigned in the current rotation and associate their paillier - // ek to each available party index. - - let available_parties: HashMap = refresh_messages - .iter() - .map(|msg| (msg.party_index, &msg.ek)) - .chain(std::iter::once((party_index, &paillier_key.ek))) - .chain( - join_messages - .iter() - .map(|join_message| (join_message.party_index.unwrap(), &join_message.ek)), - ) - .collect(); - - // TODO: submit the statement the dlog proof as well! - // check what parties are assigned in the current rotation and associate their DLogStatements - // and check their CompositeDlogProofs. - let available_h1_h2_ntilde_vec: HashMap = refresh_messages - .iter() - .map(|msg| (msg.party_index, &msg.dlog_statement)) - .chain(std::iter::once((party_index, &self.dlog_statement))) - .chain(join_messages.iter().map(|join_message| { - ( - join_message.party_index.unwrap(), - &join_message.dlog_statement, - ) - })) - .collect(); - - // generate the paillier public key vec needed for the LocalKey generation. - let paillier_key_vec: Vec = (1..new_n + 1) - .map(|party| { - let ek = available_parties.get(&party); - match ek { - None => EncryptionKey { - n: BigInt::zero(), - nn: BigInt::zero(), - }, - Some(key) => (*key).clone(), - } - }) - .collect(); - // generate the DLogStatement vec needed for the LocalKey generation. - let h1_h2_ntilde_vec: Vec = (1..new_n + 1) - .map(|party| { - let statement = available_h1_h2_ntilde_vec.get(&party); - - match statement { - None => generate_dlog_statement_proofs().0, - Some(dlog_statement) => (*dlog_statement).clone(), - } - }) - .collect(); - - // check if all the existing parties submitted the same public key. If they differ, abort. - // TODO: this should be verifiable? - for refresh_message in refresh_messages.iter() { - if refresh_message.public_key != refresh_messages[0].public_key { - return Err(FsDkrError::BroadcastedPublicKeyError); - } - } - - // generate the vss_scheme for the LocalKey - let (vss_scheme, _) = VerifiableSS::::share(new_t, new_n, &new_share_fe); - // TODO: secret cleanup might be needed. - - let local_key = LocalKey { - paillier_dk, - pk_vec, - keys_linear, - paillier_key_vec, - y_sum_s: refresh_messages[0].public_key.clone(), - h1_h2_n_tilde_vec: h1_h2_ntilde_vec, - vss_scheme, - i: party_index, - t: new_t, - n: new_n, - }; - - Ok(local_key) - } + pub fn set_party_index(&mut self, new_party_index: u16) { + self.party_index = Some(new_party_index); + } + /// The distribute phase for a new party. This distribute phase has to happen before the + /// existing parties distribute. Calling this function will generate a JoinMessage and a pair of + /// Paillier [Keys] that are going to be used when generating the [LocalKey]. + pub fn distribute() -> (Self, Keys) { + let paillier_key_pair = Keys::create(0); + let (dlog_statement, composite_dlog_proof_base_h1, composite_dlog_proof_base_h2) = + generate_dlog_statement_proofs(); + + let (ring_pedersen_statement, ring_pedersen_witness) = RingPedersenStatement::generate(); + + let ring_pedersen_proof = + RingPedersenProof::prove(&ring_pedersen_witness, &ring_pedersen_statement); + + let join_message = JoinMessage { + // in a join message, we only care about the ek and the correctness proof + ek: paillier_key_pair.ek.clone(), + dk_correctness_proof: NiCorrectKeyProof::proof(&paillier_key_pair.dk, None), + dlog_statement, + composite_dlog_proof_base_h1, + composite_dlog_proof_base_h2, + ring_pedersen_statement, + ring_pedersen_proof, + party_index: None, + }; + + (join_message, paillier_key_pair) + } + /// Returns the party index if it has been assigned one, throws + /// [FsDkrError::NewPartyUnassignedIndexError] otherwise + pub fn get_party_index(&self) -> FsDkrResult { + self.party_index.ok_or(FsDkrError::NewPartyUnassignedIndexError) + } + + /// Collect phase of the protocol. Compared to the [RefreshMessage::collect], this has to be + /// tailored for a sent JoinMessage on which we assigned party_index. In this collect, a + /// [LocalKey] is filled with the information provided by the [RefreshMessage]s from the other + /// parties and the other join messages (multiple parties can be added/replaced at once). + pub fn collect( + &self, + refresh_messages: &[RefreshMessage], + paillier_key: Keys, + join_messages: &[JoinMessage], + new_t: u16, + new_n: u16, + current_t: u16, + ) -> FsDkrResult> { + RefreshMessage::validate_collect(refresh_messages, current_t, new_n)?; + + for refresh_message in refresh_messages.iter() { + RingPedersenProof::verify( + &refresh_message.ring_pedersen_proof, + &refresh_message.ring_pedersen_statement, + ) + .map_err(|_| FsDkrError::RingPedersenProofValidation { + party_index: refresh_message.party_index, + })?; + } + + for join_message in join_messages.iter() { + RingPedersenProof::verify( + &join_message.ring_pedersen_proof, + &join_message.ring_pedersen_statement, + ) + .map_err(|e| { + if let Some(party_index) = join_message.party_index { + FsDkrError::RingPedersenProofValidation { party_index } + } else { + e + } + })?; + } + + // check if a party_index has been assigned to the current party + let party_index = self.get_party_index()?; + + // check if a party_index has been assigned to all other new parties + // TODO: Check if no party_index collision exists + for join_message in join_messages.iter() { + join_message.get_party_index()?; + } + + let parameters = ShamirSecretSharing { threshold: new_t, share_count: new_n }; + + // generate a new share, the details can be found here https://hackmd.io/@omershlo/Hy1jBo6JY. + let (cipher_text_sum, li_vec) = RefreshMessage::get_ciphertext_sum( + refresh_messages, + party_index, + ¶meters, + &paillier_key.ek, + current_t, + ); + let new_share = Paillier::decrypt(&paillier_key.dk, cipher_text_sum).0.into_owned(); + + let new_share_fe: Scalar = Scalar::::from(&new_share); + let paillier_dk = paillier_key.dk.clone(); + let key_linear_x_i = new_share_fe.clone(); + let key_linear_y = Point::::generator() * new_share_fe.clone(); + let keys_linear = SharedKeys { x_i: key_linear_x_i, y: key_linear_y }; + let mut pk_vec: Vec<_> = (0..new_n as usize) + .map(|i| refresh_messages[0].points_committed_vec[i].clone() * li_vec[0].clone()) + .collect(); + + for i in 0..new_n as usize { + for j in 1..(current_t + 1) as usize { + pk_vec[i] = pk_vec[i].clone() + + refresh_messages[j].points_committed_vec[i].clone() * li_vec[j].clone(); + } + } + + // check what parties are assigned in the current rotation and associate their paillier + // ek to each available party index. + + let available_parties: HashMap = refresh_messages + .iter() + .map(|msg| (msg.party_index, &msg.ek)) + .chain(std::iter::once((party_index, &paillier_key.ek))) + .chain( + join_messages + .iter() + .map(|join_message| (join_message.party_index.unwrap(), &join_message.ek)), + ) + .collect(); + + // TODO: submit the statement the dlog proof as well! + // check what parties are assigned in the current rotation and associate their + // DLogStatements and check their CompositeDlogProofs. + let available_h1_h2_ntilde_vec: HashMap = refresh_messages + .iter() + .map(|msg| (msg.party_index, &msg.dlog_statement)) + .chain(std::iter::once((party_index, &self.dlog_statement))) + .chain(join_messages.iter().map(|join_message| { + (join_message.party_index.unwrap(), &join_message.dlog_statement) + })) + .collect(); + + // generate the paillier public key vec needed for the LocalKey generation. + let paillier_key_vec: Vec = (1..new_n + 1) + .map(|party| { + let ek = available_parties.get(&party); + match ek { + None => EncryptionKey { n: BigInt::zero(), nn: BigInt::zero() }, + Some(key) => (*key).clone(), + } + }) + .collect(); + // generate the DLogStatement vec needed for the LocalKey generation. + let h1_h2_ntilde_vec: Vec = (1..new_n + 1) + .map(|party| { + let statement = available_h1_h2_ntilde_vec.get(&party); + + match statement { + None => generate_dlog_statement_proofs().0, + Some(dlog_statement) => (*dlog_statement).clone(), + } + }) + .collect(); + + // check if all the existing parties submitted the same public key. If they differ, abort. + // TODO: this should be verifiable? + for refresh_message in refresh_messages.iter() { + if refresh_message.public_key != refresh_messages[0].public_key { + return Err(FsDkrError::BroadcastedPublicKeyError) + } + } + + // generate the vss_scheme for the LocalKey + let (vss_scheme, _) = VerifiableSS::::share(new_t, new_n, &new_share_fe); + // TODO: secret cleanup might be needed. + + let local_key = LocalKey { + paillier_dk, + pk_vec, + keys_linear, + paillier_key_vec, + y_sum_s: refresh_messages[0].public_key.clone(), + h1_h2_n_tilde_vec: h1_h2_ntilde_vec, + vss_scheme, + i: party_index, + t: new_t, + n: new_n, + }; + + Ok(local_key) + } } diff --git a/fs-dkr/src/error.rs b/fs-dkr/src/error.rs index e1c78bb1..6a1bfa8c 100644 --- a/fs-dkr/src/error.rs +++ b/fs-dkr/src/error.rs @@ -5,56 +5,49 @@ pub type FsDkrResult = Result; #[derive(Error, Debug, Clone, Serialize, Deserialize)] pub enum FsDkrError { - #[error("Too many malicious parties detected! Threshold {threshold:?}, Number of Refreshed Messages: {refreshed_keys:?}, Malicious parties detected when trying to refresh: malicious_parties:?")] - PartiesThresholdViolation { - threshold: u16, - refreshed_keys: usize, - // TODO: figure out how to retrieve the malicious parties indexes and add them to the err. - // malicious_parties: [usize] - }, - - #[error("Shares did not pass verification.")] - PublicShareValidationError, - - #[error("SizeMismatch error for the refresh message {refresh_message_index:?} - pdl proof length: {pdl_proof_len:?}, Points Commited Length: {points_commited_len:?}, Points Encrypted Length: {points_encrypted_len:?}")] - SizeMismatchError { - refresh_message_index: usize, - pdl_proof_len: usize, - points_commited_len: usize, - points_encrypted_len: usize, - }, - - #[error("PDLwSlack proof verification failed, results: u1 == u1_test: {is_u1_eq:?}, u2 == u2_test: {is_u2_eq:?}, u3 == u3_test: {is_u3_eq:?}")] - PDLwSlackProof { - is_u1_eq: bool, - is_u2_eq: bool, - is_u3_eq: bool, - }, - - #[error("Ring Pedersen Proof Failed")] - RingPedersenProofError, - - #[error("Range Proof failed for party: {party_index:?}")] - RangeProof { party_index: usize }, - - #[error("The Paillier moduli size of party: {party_index:?} is {moduli_size:?} bits, when it should be 2047-2048 bits")] - ModuliTooSmall { - party_index: u16, - moduli_size: usize, - }, - - #[error("Paillier verification proof failed for party {party_index:?}")] - PaillierVerificationError { party_index: u16 }, - - #[error("A new party did not receive a valid index.")] - NewPartyUnassignedIndexError, - - #[error("The broadcasted public key is not the same from everyone, aborting")] - BroadcastedPublicKeyError, - - #[error("DLog proof failed for party {party_index:?}")] - DLogProofValidation { party_index: u16 }, - - #[error("Ring pedersen proof failed for party {party_index:?}")] - RingPedersenProofValidation { party_index: u16 }, + #[error("Too many malicious parties detected! Threshold {threshold:?}, Number of Refreshed Messages: {refreshed_keys:?}, Malicious parties detected when trying to refresh: malicious_parties:?")] + PartiesThresholdViolation { + threshold: u16, + refreshed_keys: usize, + // TODO: figure out how to retrieve the malicious parties indexes and add them to the err. + // malicious_parties: [usize] + }, + + #[error("Shares did not pass verification.")] + PublicShareValidationError, + + #[error("SizeMismatch error for the refresh message {refresh_message_index:?} - pdl proof length: {pdl_proof_len:?}, Points Commited Length: {points_commited_len:?}, Points Encrypted Length: {points_encrypted_len:?}")] + SizeMismatchError { + refresh_message_index: usize, + pdl_proof_len: usize, + points_commited_len: usize, + points_encrypted_len: usize, + }, + + #[error("PDLwSlack proof verification failed, results: u1 == u1_test: {is_u1_eq:?}, u2 == u2_test: {is_u2_eq:?}, u3 == u3_test: {is_u3_eq:?}")] + PDLwSlackProof { is_u1_eq: bool, is_u2_eq: bool, is_u3_eq: bool }, + + #[error("Ring Pedersen Proof Failed")] + RingPedersenProofError, + + #[error("Range Proof failed for party: {party_index:?}")] + RangeProof { party_index: usize }, + + #[error("The Paillier moduli size of party: {party_index:?} is {moduli_size:?} bits, when it should be 2047-2048 bits")] + ModuliTooSmall { party_index: u16, moduli_size: usize }, + + #[error("Paillier verification proof failed for party {party_index:?}")] + PaillierVerificationError { party_index: u16 }, + + #[error("A new party did not receive a valid index.")] + NewPartyUnassignedIndexError, + + #[error("The broadcasted public key is not the same from everyone, aborting")] + BroadcastedPublicKeyError, + + #[error("DLog proof failed for party {party_index:?}")] + DLogProofValidation { party_index: u16 }, + + #[error("Ring pedersen proof failed for party {party_index:?}")] + RingPedersenProofValidation { party_index: u16 }, } diff --git a/fs-dkr/src/lib.rs b/fs-dkr/src/lib.rs index 4898fbe5..6f7378c4 100644 --- a/fs-dkr/src/lib.rs +++ b/fs-dkr/src/lib.rs @@ -12,7 +12,6 @@ //! Components of the library: //! //! * [refresh_message]: crate::refresh_message -//! pub mod add_party_message; pub mod error; diff --git a/fs-dkr/src/range_proofs.rs b/fs-dkr/src/range_proofs.rs index c194d598..e2aea4b4 100644 --- a/fs-dkr/src/range_proofs.rs +++ b/fs-dkr/src/range_proofs.rs @@ -11,16 +11,15 @@ // TODO: Verify this matches (if possible) range proofs from multi-party-ecdsa -use curv::arithmetic::traits::*; -use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; -use curv::elliptic::curves::Point; -use curv::elliptic::curves::Scalar; -use curv::elliptic::curves::{Curve, Secp256k1}; -use curv::BigInt; +use curv::{ + arithmetic::traits::*, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{Curve, Point, Scalar, Secp256k1}, + BigInt, +}; use paillier::{EncryptionKey, Randomness}; use serde::{Deserialize, Serialize}; -use std::borrow::Borrow; -use std::marker::PhantomData; +use std::{borrow::Borrow, marker::PhantomData}; use zeroize::Zeroize; use zk_paillier::zkproofs::DLogStatement; @@ -28,719 +27,701 @@ use zk_paillier::zkproofs::DLogStatement; #[derive(Zeroize)] #[zeroize(drop)] struct AliceZkpRound1 { - alpha: BigInt, - beta: BigInt, - gamma: BigInt, - ro: BigInt, - z: BigInt, - u: BigInt, - w: BigInt, + alpha: BigInt, + beta: BigInt, + gamma: BigInt, + ro: BigInt, + z: BigInt, + u: BigInt, + w: BigInt, } impl AliceZkpRound1 { - fn from( - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - a: &BigInt, - q: &BigInt, - ) -> Self { - assert!( - q.bit_length() <= 256, - "We use SHA256 so we don't currently support moduli bigger than 256" - ); - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - let N_tilde = &dlog_statement.N; - let alpha = BigInt::sample_below(&q.pow(3)); - let beta = BigInt::from_paillier_key(alice_ek); - let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let ro = BigInt::sample_below(&(q * N_tilde)); - let z = (BigInt::mod_pow(h1, &a, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; - let u = ((alpha.borrow() * &alice_ek.n + 1) - * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) - % &alice_ek.nn; - let w = - (BigInt::mod_pow(h1, &alpha, N_tilde) * BigInt::mod_pow(h2, &gamma, N_tilde)) % N_tilde; - Self { - alpha, - beta, - gamma, - ro, - z, - u, - w, - } - } + fn from( + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + a: &BigInt, + q: &BigInt, + ) -> Self { + assert!( + q.bit_length() <= 256, + "We use SHA256 so we don't currently support moduli bigger than 256" + ); + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let N_tilde = &dlog_statement.N; + let alpha = BigInt::sample_below(&q.pow(3)); + let beta = BigInt::from_paillier_key(alice_ek); + let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let ro = BigInt::sample_below(&(q * N_tilde)); + let z = (BigInt::mod_pow(h1, &a, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; + let u = ((alpha.borrow() * &alice_ek.n + 1) * + BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % + &alice_ek.nn; + let w = + (BigInt::mod_pow(h1, &alpha, N_tilde) * BigInt::mod_pow(h2, &gamma, N_tilde)) % N_tilde; + Self { alpha, beta, gamma, ro, z, u, w } + } } /// Represents the second round of the interactive version of the proof struct AliceZkpRound2 { - s: BigInt, - s1: BigInt, - s2: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, } impl AliceZkpRound2 { - fn from( - alice_ek: &EncryptionKey, - round1: &AliceZkpRound1, - e: &BigInt, - a: &BigInt, - r: &BigInt, - ) -> Self { - Self { - s: (BigInt::mod_pow(r, &e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, - s1: (e * a) + round1.alpha.borrow(), - s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), - } - } + fn from( + alice_ek: &EncryptionKey, + round1: &AliceZkpRound1, + e: &BigInt, + a: &BigInt, + r: &BigInt, + ) -> Self { + Self { + s: (BigInt::mod_pow(r, &e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, + s1: (e * a) + round1.alpha.borrow(), + s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), + } + } } /// Alice's proof #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AliceProof { - z: BigInt, - e: BigInt, - s: BigInt, - s1: BigInt, - s2: BigInt, - _phantom: PhantomData<(E, H)>, + z: BigInt, + e: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, + _phantom: PhantomData<(E, H)>, } impl AliceProof { - /// verify Alice's proof using the proof and public keys - pub fn verify( - &self, - cipher: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - ) -> bool { - let N = &alice_ek.n; - let NN = &alice_ek.nn; - let N_tilde = &dlog_statement.N; - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - let Gen = alice_ek.n.borrow() + 1; - - if self.s1 > Scalar::::group_order().pow(3) { - return false; - } - - let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); - let z_e_inv = match z_e_inv { - // z must be invertible, yet the check is done here - None => return false, - Some(c) => c, - }; - - let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) - * BigInt::mod_pow(h2, &self.s2, N_tilde) - * z_e_inv) - % N_tilde; - - let gs1 = (self.s1.borrow() * N + 1) % NN; - let cipher_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&cipher, &self.e, NN), NN); - let cipher_e_inv = match cipher_e_inv { - None => return false, - Some(c) => c, - }; - - let u = (gs1 * BigInt::mod_pow(&self.s, N, NN) * cipher_e_inv) % NN; - - let e = H::new() - .chain_bigint(&N) - .chain_bigint(&Gen) - .chain_bigint(cipher) - .chain_bigint(&self.z) - .chain_bigint(&u) - .chain_bigint(&w) - .result_bigint(); - - if e != self.e { - return false; - } - - true - } - /// Create the proof using Alice's Paillier private keys and public ZKP setup. - /// Requires randomness used for encrypting Alice's secret a. - /// It is assumed that a curve order smaller than 2^256 is used.. - pub fn generate( - a: &BigInt, - cipher: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - r: &BigInt, - ) -> Self { - let q = Scalar::::group_order(); - assert!( - q.bit_length() <= 256, - "We use SHA256 so we don't currently support moduli bigger than 256" - ); - let round1 = AliceZkpRound1::from(alice_ek, dlog_statement, a, &q); - - let Gen = alice_ek.n.borrow() + 1; - let e = H::new() - .chain_bigint(&alice_ek.n) - .chain_bigint(&Gen) - .chain_bigint(cipher) - .chain_bigint(&round1.z) - .chain_bigint(&round1.u) - .chain_bigint(&round1.w) - .result_bigint(); - - let round2 = AliceZkpRound2::from(alice_ek, &round1, &e, a, r); - - Self { - z: round1.z.clone(), - e, - s: round2.s, - s1: round2.s1, - s2: round2.s2, - _phantom: PhantomData, - } - } + /// verify Alice's proof using the proof and public keys + pub fn verify( + &self, + cipher: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + ) -> bool { + let N = &alice_ek.n; + let NN = &alice_ek.nn; + let N_tilde = &dlog_statement.N; + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let Gen = alice_ek.n.borrow() + 1; + + if self.s1 > Scalar::::group_order().pow(3) { + return false + } + + let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); + let z_e_inv = match z_e_inv { + // z must be invertible, yet the check is done here + None => return false, + Some(c) => c, + }; + + let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) * + BigInt::mod_pow(h2, &self.s2, N_tilde) * + z_e_inv) % N_tilde; + + let gs1 = (self.s1.borrow() * N + 1) % NN; + let cipher_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&cipher, &self.e, NN), NN); + let cipher_e_inv = match cipher_e_inv { + None => return false, + Some(c) => c, + }; + + let u = (gs1 * BigInt::mod_pow(&self.s, N, NN) * cipher_e_inv) % NN; + + let e = H::new() + .chain_bigint(&N) + .chain_bigint(&Gen) + .chain_bigint(cipher) + .chain_bigint(&self.z) + .chain_bigint(&u) + .chain_bigint(&w) + .result_bigint(); + + if e != self.e { + return false + } + + true + } + /// Create the proof using Alice's Paillier private keys and public ZKP setup. + /// Requires randomness used for encrypting Alice's secret a. + /// It is assumed that a curve order smaller than 2^256 is used.. + pub fn generate( + a: &BigInt, + cipher: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &BigInt, + ) -> Self { + let q = Scalar::::group_order(); + assert!( + q.bit_length() <= 256, + "We use SHA256 so we don't currently support moduli bigger than 256" + ); + let round1 = AliceZkpRound1::from(alice_ek, dlog_statement, a, &q); + + let Gen = alice_ek.n.borrow() + 1; + let e = H::new() + .chain_bigint(&alice_ek.n) + .chain_bigint(&Gen) + .chain_bigint(cipher) + .chain_bigint(&round1.z) + .chain_bigint(&round1.u) + .chain_bigint(&round1.w) + .result_bigint(); + + let round2 = AliceZkpRound2::from(alice_ek, &round1, &e, a, r); + + Self { + z: round1.z.clone(), + e, + s: round2.s, + s1: round2.s1, + s2: round2.s2, + _phantom: PhantomData, + } + } } /// Represents first round of the interactive version of the proof struct BobZkpRound1 { - pub alpha: BigInt, - pub beta: BigInt, - pub gamma: BigInt, - pub ro: BigInt, - pub ro_prim: BigInt, - pub sigma: BigInt, - pub tau: BigInt, - pub z: BigInt, - pub z_prim: BigInt, - pub t: BigInt, - pub w: BigInt, - pub v: BigInt, - _phantom: PhantomData, + pub alpha: BigInt, + pub beta: BigInt, + pub gamma: BigInt, + pub ro: BigInt, + pub ro_prim: BigInt, + pub sigma: BigInt, + pub tau: BigInt, + pub z: BigInt, + pub z_prim: BigInt, + pub t: BigInt, + pub w: BigInt, + pub v: BigInt, + _phantom: PhantomData, } impl Zeroize for BobZkpRound1 { - fn zeroize(&mut self) { - self.alpha.zeroize(); - self.beta.zeroize(); - self.gamma.zeroize(); - self.ro.zeroize(); - self.ro_prim.zeroize(); - self.sigma.zeroize(); - self.tau.zeroize(); - self.z.zeroize(); - self.z_prim.zeroize(); - self.t.zeroize(); - self.w.zeroize(); - self.v.zeroize(); - } + fn zeroize(&mut self) { + self.alpha.zeroize(); + self.beta.zeroize(); + self.gamma.zeroize(); + self.ro.zeroize(); + self.ro_prim.zeroize(); + self.sigma.zeroize(); + self.tau.zeroize(); + self.z.zeroize(); + self.z_prim.zeroize(); + self.t.zeroize(); + self.w.zeroize(); + self.v.zeroize(); + } } impl Drop for BobZkpRound1 { - fn drop(&mut self) { - self.zeroize(); - } + fn drop(&mut self) { + self.zeroize(); + } } impl BobZkpRound1 { - /// `b` - Bob's secret - /// `beta_prim` - randomly chosen in `MtA` by Bob - /// `a_encrypted` - Alice's secret encrypted by Alice - fn from( - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - b: &Scalar, - beta_prim: &BigInt, - a_encrypted: &BigInt, - q: &BigInt, - ) -> Self { - assert!( - q.bit_length() <= 256, - "We use SHA256 so we don't currently support moduli bigger than 256" - ); - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - let N_tilde = &dlog_statement.N; - let b_bn = b.to_bigint(); - - let alpha = BigInt::sample_below(&q.pow(3)); - let beta = BigInt::from_paillier_key(alice_ek); - let gamma = BigInt::sample_below(&(q.pow(2) * &alice_ek.n)); - let ro = BigInt::sample_below(&(q * N_tilde)); - let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let sigma = BigInt::sample_below(&(q * N_tilde)); - let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; - let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) - * BigInt::mod_pow(h2, &ro_prim, N_tilde)) - % N_tilde; - let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * BigInt::mod_pow(h2, &sigma, N_tilde)) - % N_tilde; - let w = - (BigInt::mod_pow(h1, &gamma, N_tilde) * BigInt::mod_pow(h2, &tau, N_tilde)) % N_tilde; - let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) - * (gamma.borrow() * &alice_ek.n + 1) - * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) - % &alice_ek.nn; - Self { - alpha, - beta, - gamma, - ro, - ro_prim, - sigma, - tau, - z, - z_prim, - t, - w, - v, - _phantom: PhantomData, - } - } + /// `b` - Bob's secret + /// `beta_prim` - randomly chosen in `MtA` by Bob + /// `a_encrypted` - Alice's secret encrypted by Alice + fn from( + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + b: &Scalar, + beta_prim: &BigInt, + a_encrypted: &BigInt, + q: &BigInt, + ) -> Self { + assert!( + q.bit_length() <= 256, + "We use SHA256 so we don't currently support moduli bigger than 256" + ); + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let N_tilde = &dlog_statement.N; + let b_bn = b.to_bigint(); + + let alpha = BigInt::sample_below(&q.pow(3)); + let beta = BigInt::from_paillier_key(alice_ek); + let gamma = BigInt::sample_below(&(q.pow(2) * &alice_ek.n)); + let ro = BigInt::sample_below(&(q * N_tilde)); + let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let sigma = BigInt::sample_below(&(q * N_tilde)); + let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; + let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) * + BigInt::mod_pow(h2, &ro_prim, N_tilde)) % + N_tilde; + let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * BigInt::mod_pow(h2, &sigma, N_tilde)) % + N_tilde; + let w = + (BigInt::mod_pow(h1, &gamma, N_tilde) * BigInt::mod_pow(h2, &tau, N_tilde)) % N_tilde; + let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) * + (gamma.borrow() * &alice_ek.n + 1) * + BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % + &alice_ek.nn; + Self { + alpha, + beta, + gamma, + ro, + ro_prim, + sigma, + tau, + z, + z_prim, + t, + w, + v, + _phantom: PhantomData, + } + } } /// represents second round of the interactive version of the proof struct BobZkpRound2 { - pub s: BigInt, - pub s1: BigInt, - pub s2: BigInt, - pub t1: BigInt, - pub t2: BigInt, - _phantom: PhantomData, + pub s: BigInt, + pub s1: BigInt, + pub s2: BigInt, + pub t1: BigInt, + pub t2: BigInt, + _phantom: PhantomData, } impl BobZkpRound2 { - /// `e` - the challenge in interactive ZKP, the hash in non-interactive ZKP - /// `b` - Bob's secret - /// `beta_prim` - randomly chosen in `MtA` by Bob - /// `r` - randomness used by Bob on Alice's public Paillier key to encrypt `beta_prim` in `MtA` - fn from( - alice_ek: &EncryptionKey, - round1: &BobZkpRound1, - e: &BigInt, - b: &Scalar, - beta_prim: &BigInt, - r: &Randomness, - ) -> Self { - let b_bn = b.to_bigint(); - Self { - s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, - s1: (e * b_bn) + round1.alpha.borrow(), - s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), - t1: (e * beta_prim) + round1.gamma.borrow(), - t2: (e * round1.sigma.borrow()) + round1.tau.borrow(), - _phantom: PhantomData, - } - } + /// `e` - the challenge in interactive ZKP, the hash in non-interactive ZKP + /// `b` - Bob's secret + /// `beta_prim` - randomly chosen in `MtA` by Bob + /// `r` - randomness used by Bob on Alice's public Paillier key to encrypt `beta_prim` in `MtA` + fn from( + alice_ek: &EncryptionKey, + round1: &BobZkpRound1, + e: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + r: &Randomness, + ) -> Self { + let b_bn = b.to_bigint(); + Self { + s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, + s1: (e * b_bn) + round1.alpha.borrow(), + s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), + t1: (e * beta_prim) + round1.gamma.borrow(), + t2: (e * round1.sigma.borrow()) + round1.tau.borrow(), + _phantom: PhantomData, + } + } } /// Additional fields in Bob's proof if MtA is run with check pub struct BobCheck { - u: Point, - X: Point, + u: Point, + X: Point, } /// Bob's regular proof #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub struct BobProof { - t: BigInt, - z: BigInt, - e: BigInt, - s: BigInt, - s1: BigInt, - s2: BigInt, - t1: BigInt, - t2: BigInt, - _phantom: PhantomData<(E, H)>, + t: BigInt, + z: BigInt, + e: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, + t1: BigInt, + t2: BigInt, + _phantom: PhantomData<(E, H)>, } #[allow(clippy::too_many_arguments)] impl BobProof { - pub fn verify( - &self, - a_enc: &BigInt, - mta_avc_out: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - check: Option<&BobCheck>, - ) -> bool { - let N = &alice_ek.n; - let NN = &alice_ek.nn; - let N_tilde = &dlog_statement.N; - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - - if self.s1 > Scalar::::group_order().pow(3) { - return false; - } - - let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); - let z_e_inv = match z_e_inv { - // z must be invertible, yet the check is done here - None => return false, - Some(c) => c, - }; - - let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) - * BigInt::mod_pow(h2, &self.s2, N_tilde) - * z_e_inv) - % N_tilde; - - let mta_e_inv = BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); - let mta_e_inv = match mta_e_inv { - None => return false, - Some(c) => c, - }; - - let v = (BigInt::mod_pow(a_enc, &self.s1, NN) - * BigInt::mod_pow(&self.s, N, NN) - * (self.t1.borrow() * N + 1) - * mta_e_inv) - % NN; - - let t_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.t, &self.e, N_tilde), N_tilde); - let t_e_inv = match t_e_inv { - None => return false, - Some(c) => c, - }; - - let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) - * BigInt::mod_pow(h2, &self.t2, N_tilde) - * t_e_inv) - % N_tilde; - - let Gen = alice_ek.n.borrow() + 1; - - let hash = H::new() - .chain_bigint(&alice_ek.n) - .chain_bigint(&Gen) - .chain_bigint(a_enc) - .chain_bigint(mta_avc_out) - .chain_bigint(&self.z) - .chain_bigint(&z_prim) - .chain_bigint(&self.t) - .chain_bigint(&v) - .chain_bigint(&w); - - let e = match check { - Some(_) => { - let X_x_coor = check.unwrap().X.x_coord().unwrap(); - let X_y_coor = check.unwrap().X.y_coord().unwrap(); - let u_x_coor = check.unwrap().u.x_coord().unwrap(); - let u_y_coor = check.unwrap().u.y_coord().unwrap(); - hash.chain_bigint(&X_x_coor) - .chain_bigint(&X_y_coor) - .chain_bigint(&u_x_coor) - .chain_bigint(&u_y_coor) - .result_bigint() - } - None => hash.result_bigint(), - }; - - if e != self.e { - return false; - } - - true - } - - pub fn generate( - a_encrypted: &BigInt, - mta_encrypted: &BigInt, - b: &Scalar, - beta_prim: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - r: &Randomness, - check: bool, - ) -> (BobProof, Option>) { - let round1 = BobZkpRound1::from( - alice_ek, - dlog_statement, - b, - beta_prim, - a_encrypted, - &Scalar::::group_order(), - ); - - let Gen = alice_ek.n.borrow() + 1; - let hash = H::new() - .chain_bigint(&alice_ek.n) - .chain_bigint(&Gen) - .chain_bigint(a_encrypted) - .chain_bigint(mta_encrypted) - .chain_bigint(&round1.z) - .chain_bigint(&round1.z_prim) - .chain_bigint(&round1.t) - .chain_bigint(&round1.v) - .chain_bigint(&round1.w); - let mut check_u = None; - let e = if check { - let (X, u) = { - let ec_gen: Point = Point::::generator().to_point(); - let alpha: Scalar = Scalar::::from(&round1.alpha); - (ec_gen.clone() * b.clone(), ec_gen.clone() * alpha) - }; - check_u = Some(u.clone()); - let X_x_coor = X.x_coord().unwrap(); - let X_y_coor = X.y_coord().unwrap(); - let u_x_coor = u.x_coord().unwrap(); - let u_y_coor = u.y_coord().unwrap(); - hash.chain_bigint(&X_x_coor) - .chain_bigint(&X_y_coor) - .chain_bigint(&u_x_coor) - .chain_bigint(&u_y_coor) - .result_bigint() - } else { - hash.result_bigint() - }; - - let round2 = BobZkpRound2::from(alice_ek, &round1, &e, b, beta_prim, r); - - ( - BobProof { - t: round1.t.clone(), - z: round1.z.clone(), - e, - s: round2.s, - s1: round2.s1, - s2: round2.s2, - t1: round2.t1, - t2: round2.t2, - _phantom: PhantomData, - }, - check_u, - ) - } + pub fn verify( + &self, + a_enc: &BigInt, + mta_avc_out: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + check: Option<&BobCheck>, + ) -> bool { + let N = &alice_ek.n; + let NN = &alice_ek.nn; + let N_tilde = &dlog_statement.N; + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + + if self.s1 > Scalar::::group_order().pow(3) { + return false + } + + let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); + let z_e_inv = match z_e_inv { + // z must be invertible, yet the check is done here + None => return false, + Some(c) => c, + }; + + let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) * + BigInt::mod_pow(h2, &self.s2, N_tilde) * + z_e_inv) % N_tilde; + + let mta_e_inv = BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); + let mta_e_inv = match mta_e_inv { + None => return false, + Some(c) => c, + }; + + let v = (BigInt::mod_pow(a_enc, &self.s1, NN) * + BigInt::mod_pow(&self.s, N, NN) * + (self.t1.borrow() * N + 1) * + mta_e_inv) % NN; + + let t_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.t, &self.e, N_tilde), N_tilde); + let t_e_inv = match t_e_inv { + None => return false, + Some(c) => c, + }; + + let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) * + BigInt::mod_pow(h2, &self.t2, N_tilde) * + t_e_inv) % N_tilde; + + let Gen = alice_ek.n.borrow() + 1; + + let hash = H::new() + .chain_bigint(&alice_ek.n) + .chain_bigint(&Gen) + .chain_bigint(a_enc) + .chain_bigint(mta_avc_out) + .chain_bigint(&self.z) + .chain_bigint(&z_prim) + .chain_bigint(&self.t) + .chain_bigint(&v) + .chain_bigint(&w); + + let e = match check { + Some(_) => { + let X_x_coor = check.unwrap().X.x_coord().unwrap(); + let X_y_coor = check.unwrap().X.y_coord().unwrap(); + let u_x_coor = check.unwrap().u.x_coord().unwrap(); + let u_y_coor = check.unwrap().u.y_coord().unwrap(); + hash.chain_bigint(&X_x_coor) + .chain_bigint(&X_y_coor) + .chain_bigint(&u_x_coor) + .chain_bigint(&u_y_coor) + .result_bigint() + }, + None => hash.result_bigint(), + }; + + if e != self.e { + return false + } + + true + } + + pub fn generate( + a_encrypted: &BigInt, + mta_encrypted: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &Randomness, + check: bool, + ) -> (BobProof, Option>) { + let round1 = BobZkpRound1::from( + alice_ek, + dlog_statement, + b, + beta_prim, + a_encrypted, + &Scalar::::group_order(), + ); + + let Gen = alice_ek.n.borrow() + 1; + let hash = H::new() + .chain_bigint(&alice_ek.n) + .chain_bigint(&Gen) + .chain_bigint(a_encrypted) + .chain_bigint(mta_encrypted) + .chain_bigint(&round1.z) + .chain_bigint(&round1.z_prim) + .chain_bigint(&round1.t) + .chain_bigint(&round1.v) + .chain_bigint(&round1.w); + let mut check_u = None; + let e = if check { + let (X, u) = { + let ec_gen: Point = Point::::generator().to_point(); + let alpha: Scalar = Scalar::::from(&round1.alpha); + (ec_gen.clone() * b.clone(), ec_gen.clone() * alpha) + }; + check_u = Some(u.clone()); + let X_x_coor = X.x_coord().unwrap(); + let X_y_coor = X.y_coord().unwrap(); + let u_x_coor = u.x_coord().unwrap(); + let u_y_coor = u.y_coord().unwrap(); + hash.chain_bigint(&X_x_coor) + .chain_bigint(&X_y_coor) + .chain_bigint(&u_x_coor) + .chain_bigint(&u_y_coor) + .result_bigint() + } else { + hash.result_bigint() + }; + + let round2 = BobZkpRound2::from(alice_ek, &round1, &e, b, beta_prim, r); + + ( + BobProof { + t: round1.t.clone(), + z: round1.z.clone(), + e, + s: round2.s, + s1: round2.s1, + s2: round2.s2, + t1: round2.t1, + t2: round2.t2, + _phantom: PhantomData, + }, + check_u, + ) + } } /// Bob's extended proof, adds the knowledge of $`B = g^b \in \mathcal{G}`$ #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub struct BobProofExt { - proof: BobProof, - u: Point, + proof: BobProof, + u: Point, } #[allow(clippy::too_many_arguments)] impl BobProofExt { - pub fn verify( - &self, - a_enc: &BigInt, - mta_avc_out: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - X: &Point, - ) -> bool { - // check basic proof first - if !self.proof.verify( - a_enc, - mta_avc_out, - alice_ek, - dlog_statement, - Some(&BobCheck { - u: self.u.clone(), - X: X.clone(), - }), - ) { - return false; - } - - // fiddle with EC points - let (x1, x2) = { - let ec_gen: Point = Point::::generator().to_point(); - let s1: Scalar = Scalar::::from(&self.proof.s1); - let e: Scalar = Scalar::::from(&self.proof.e); - (ec_gen * s1, (X.clone() * e) + self.u.clone()) - }; - - if x1 != x2 { - return false; - } - - true - } - - fn generate( - a_encrypted: &BigInt, - mta_encrypted: &BigInt, - b: &Scalar, - beta_prim: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - r: &Randomness, - ) -> BobProofExt { - // proving a basic proof (with modified hash) - let (bob_proof, u) = BobProof::generate( - a_encrypted, - mta_encrypted, - b, - beta_prim, - alice_ek, - dlog_statement, - r, - true, - ); - - BobProofExt { - proof: bob_proof, - u: u.unwrap(), - } - } + pub fn verify( + &self, + a_enc: &BigInt, + mta_avc_out: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + X: &Point, + ) -> bool { + // check basic proof first + if !self.proof.verify( + a_enc, + mta_avc_out, + alice_ek, + dlog_statement, + Some(&BobCheck { u: self.u.clone(), X: X.clone() }), + ) { + return false + } + + // fiddle with EC points + let (x1, x2) = { + let ec_gen: Point = Point::::generator().to_point(); + let s1: Scalar = Scalar::::from(&self.proof.s1); + let e: Scalar = Scalar::::from(&self.proof.e); + (ec_gen * s1, (X.clone() * e) + self.u.clone()) + }; + + if x1 != x2 { + return false + } + + true + } + + fn generate( + a_encrypted: &BigInt, + mta_encrypted: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &Randomness, + ) -> BobProofExt { + // proving a basic proof (with modified hash) + let (bob_proof, u) = BobProof::generate( + a_encrypted, + mta_encrypted, + b, + beta_prim, + alice_ek, + dlog_statement, + r, + true, + ); + + BobProofExt { proof: bob_proof, u: u.unwrap() } + } } /// sample random value of an element of a multiplicative group pub trait SampleFromMultiplicativeGroup { - fn from_modulo(N: &BigInt) -> BigInt; - fn from_paillier_key(ek: &EncryptionKey) -> BigInt; + fn from_modulo(N: &BigInt) -> BigInt; + fn from_paillier_key(ek: &EncryptionKey) -> BigInt; } impl SampleFromMultiplicativeGroup for BigInt { - fn from_modulo(N: &BigInt) -> BigInt { - let One = BigInt::one(); - loop { - let r = Self::sample_below(N); - if r.gcd(N) == One { - return r; - } - } - } - - fn from_paillier_key(ek: &EncryptionKey) -> BigInt { - Self::from_modulo(ek.n.borrow()) - } + fn from_modulo(N: &BigInt) -> BigInt { + let One = BigInt::one(); + loop { + let r = Self::sample_below(N); + if r.gcd(N) == One { + return r + } + } + } + + fn from_paillier_key(ek: &EncryptionKey) -> BigInt { + Self::from_modulo(ek.n.borrow()) + } } #[cfg(test)] pub(crate) mod tests { - use super::*; - use curv::elliptic::curves::secp256_k1::{Secp256k1Point, Secp256k1Scalar}; - use curv::elliptic::curves::ECScalar; - use paillier::traits::{Encrypt, EncryptWithChosenRandomness, KeyGeneration}; - use paillier::{Add, DecryptionKey, Mul, Paillier, RawCiphertext, RawPlaintext}; - use sha2::Sha256; - - type GE = Secp256k1Point; - type FE = Secp256k1Scalar; - - pub(crate) fn generate_init() -> (DLogStatement, EncryptionKey, DecryptionKey) { - let (ek_tilde, dk_tilde) = - Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&ek_tilde.n); - let (xhi, _) = loop { - let xhi_ = BigInt::sample_below(&phi); - match BigInt::mod_inv(&xhi_, &phi) { - Some(inv) => break (xhi_, inv), - None => continue, - } - }; - let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); - - let (ek, dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - let dlog_statement = DLogStatement { - g: h1, - ni: h2, - N: ek_tilde.n, - }; - (dlog_statement, ek, dk) - } - - #[test] - fn alice_zkp() { - let (dlog_statement, ek, _) = generate_init(); - - // Alice's secret value - let a = FE::random().to_bigint(); - let r = BigInt::from_paillier_key(&ek); - let cipher = Paillier::encrypt_with_chosen_randomness( - &ek, - RawPlaintext::from(a.clone()), - &Randomness::from(&r), - ) - .0 - .clone() - .into_owned(); - - let alice_proof = - AliceProof::::generate(&a, &cipher, &ek, &dlog_statement, &r); - - assert!(alice_proof.verify(&cipher, &ek, &dlog_statement)); - } - - #[test] - fn bob_zkp() { - let (dlog_statement, ek, _) = generate_init(); - - (0..5).for_each(|_| { - let alice_public_key = &ek; - - // run MtA protocol with different inputs - (0..5).for_each(|_| { - // Simulate Alice - let a = FE::random().to_bigint(); - let encrypted_a = Paillier::encrypt(alice_public_key, RawPlaintext::from(a)) - .0 - .clone() - .into_owned(); - - // Bob follows MtA - let b: Scalar = Scalar::::random(); - // E(a) * b - let b_times_enc_a = Paillier::mul( - alice_public_key, - RawCiphertext::from(encrypted_a.clone()), - RawPlaintext::from(&b.to_bigint()), - ); - let beta_prim = BigInt::sample_below(&alice_public_key.n); - let r = Randomness::sample(alice_public_key); - let enc_beta_prim = Paillier::encrypt_with_chosen_randomness( - alice_public_key, - RawPlaintext::from(&beta_prim), - &r, - ); - - let mta_out = Paillier::add(alice_public_key, b_times_enc_a, enc_beta_prim); - - let (bob_proof, _) = BobProof::::generate( - &encrypted_a, - &mta_out.0.clone().into_owned(), - &b, - &beta_prim, - alice_public_key, - &dlog_statement, - &r, - false, - ); - assert!(bob_proof.verify( - &encrypted_a, - &mta_out.0.clone().into_owned(), - alice_public_key, - &dlog_statement, - None - )); - - // Bob follows MtAwc - let ec_gen: Point = Point::::generator().to_point(); - let X = ec_gen * b.clone(); - let bob_proof = BobProofExt::::generate( - &encrypted_a, - &mta_out.0.clone().into_owned(), - &b, - &beta_prim, - alice_public_key, - &dlog_statement, - &r, - ); - assert!(bob_proof.verify( - &encrypted_a, - &mta_out.0.clone().into_owned(), - alice_public_key, - &dlog_statement, - &X - )); - }); - }); - } + use super::*; + use curv::elliptic::curves::{ + secp256_k1::{Secp256k1Point, Secp256k1Scalar}, + ECScalar, + }; + use paillier::{ + traits::{Encrypt, EncryptWithChosenRandomness, KeyGeneration}, + Add, DecryptionKey, Mul, Paillier, RawCiphertext, RawPlaintext, + }; + use sha2::Sha256; + + type GE = Secp256k1Point; + type FE = Secp256k1Scalar; + + pub(crate) fn generate_init() -> (DLogStatement, EncryptionKey, DecryptionKey) { + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&ek_tilde.n); + let (xhi, _) = loop { + let xhi_ = BigInt::sample_below(&phi); + match BigInt::mod_inv(&xhi_, &phi) { + Some(inv) => break (xhi_, inv), + None => continue, + } + }; + let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); + + let (ek, dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + let dlog_statement = DLogStatement { g: h1, ni: h2, N: ek_tilde.n }; + (dlog_statement, ek, dk) + } + + #[test] + fn alice_zkp() { + let (dlog_statement, ek, _) = generate_init(); + + // Alice's secret value + let a = FE::random().to_bigint(); + let r = BigInt::from_paillier_key(&ek); + let cipher = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(a.clone()), + &Randomness::from(&r), + ) + .0 + .clone() + .into_owned(); + + let alice_proof = + AliceProof::::generate(&a, &cipher, &ek, &dlog_statement, &r); + + assert!(alice_proof.verify(&cipher, &ek, &dlog_statement)); + } + + #[test] + fn bob_zkp() { + let (dlog_statement, ek, _) = generate_init(); + + (0..5).for_each(|_| { + let alice_public_key = &ek; + + // run MtA protocol with different inputs + (0..5).for_each(|_| { + // Simulate Alice + let a = FE::random().to_bigint(); + let encrypted_a = Paillier::encrypt(alice_public_key, RawPlaintext::from(a)) + .0 + .clone() + .into_owned(); + + // Bob follows MtA + let b: Scalar = Scalar::::random(); + // E(a) * b + let b_times_enc_a = Paillier::mul( + alice_public_key, + RawCiphertext::from(encrypted_a.clone()), + RawPlaintext::from(&b.to_bigint()), + ); + let beta_prim = BigInt::sample_below(&alice_public_key.n); + let r = Randomness::sample(alice_public_key); + let enc_beta_prim = Paillier::encrypt_with_chosen_randomness( + alice_public_key, + RawPlaintext::from(&beta_prim), + &r, + ); + + let mta_out = Paillier::add(alice_public_key, b_times_enc_a, enc_beta_prim); + + let (bob_proof, _) = BobProof::::generate( + &encrypted_a, + &mta_out.0.clone().into_owned(), + &b, + &beta_prim, + alice_public_key, + &dlog_statement, + &r, + false, + ); + assert!(bob_proof.verify( + &encrypted_a, + &mta_out.0.clone().into_owned(), + alice_public_key, + &dlog_statement, + None + )); + + // Bob follows MtAwc + let ec_gen: Point = Point::::generator().to_point(); + let X = ec_gen * b.clone(); + let bob_proof = BobProofExt::::generate( + &encrypted_a, + &mta_out.0.clone().into_owned(), + &b, + &beta_prim, + alice_public_key, + &dlog_statement, + &r, + ); + assert!(bob_proof.verify( + &encrypted_a, + &mta_out.0.clone().into_owned(), + alice_public_key, + &dlog_statement, + &X + )); + }); + }); + } } diff --git a/fs-dkr/src/refresh_message.rs b/fs-dkr/src/refresh_message.rs index 446818bd..21508d74 100644 --- a/fs-dkr/src/refresh_message.rs +++ b/fs-dkr/src/refresh_message.rs @@ -1,25 +1,26 @@ -use crate::add_party_message::JoinMessage; -use crate::error::{FsDkrError, FsDkrResult}; -use crate::range_proofs::AliceProof; -use crate::zk_pdl_with_slack::{PDLwSlackProof, PDLwSlackStatement, PDLwSlackWitness}; -use curv::arithmetic::{BitManipulation, Samplable, Zero}; -use curv::cryptographic_primitives::hashing::Digest; -use curv::cryptographic_primitives::secret_sharing::feldman_vss::{ - ShamirSecretSharing, VerifiableSS, +use crate::{ + add_party_message::JoinMessage, + error::{FsDkrError, FsDkrResult}, + range_proofs::AliceProof, + zk_pdl_with_slack::{PDLwSlackProof, PDLwSlackStatement, PDLwSlackWitness}, +}; +use curv::{ + arithmetic::{BitManipulation, Samplable, Zero}, + cryptographic_primitives::{ + hashing::Digest, + secret_sharing::feldman_vss::{ShamirSecretSharing, VerifiableSS}, + }, + elliptic::curves::{Curve, Point, Scalar}, + BigInt, HashChoice, }; -use curv::elliptic::curves::{Curve, Point, Scalar}; -use curv::BigInt; -use curv::HashChoice; use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::keygen::LocalKey; pub use paillier::DecryptionKey; use paillier::{ - Add, Decrypt, Encrypt, EncryptWithChosenRandomness, EncryptionKey, KeyGeneration, Mul, - Paillier, Randomness, RawCiphertext, RawPlaintext, + Add, Decrypt, Encrypt, EncryptWithChosenRandomness, EncryptionKey, KeyGeneration, Mul, + Paillier, Randomness, RawCiphertext, RawPlaintext, }; use serde::{Deserialize, Serialize}; -use std::borrow::Borrow; -use std::collections::HashMap; -use std::fmt::Debug; +use std::{borrow::Borrow, collections::HashMap, fmt::Debug}; use zeroize::Zeroize; use zk_paillier::zkproofs::{DLogStatement, NiCorrectKeyProof, SALT_STRING}; @@ -29,447 +30,419 @@ use crate::ring_pedersen_proof::{RingPedersenProof, RingPedersenStatement}; #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(bound = "E: Curve, H: Digest + Clone")] pub struct RefreshMessage { - pub(crate) old_party_index: u16, - pub(crate) party_index: u16, - pdl_proof_vec: Vec>, - range_proofs: Vec>, - coefficients_committed_vec: VerifiableSS, - pub(crate) points_committed_vec: Vec>, - points_encrypted_vec: Vec, - dk_correctness_proof: NiCorrectKeyProof, - pub(crate) dlog_statement: DLogStatement, - pub(crate) ek: EncryptionKey, - pub(crate) remove_party_indices: Vec, - pub(crate) public_key: Point, - pub(crate) ring_pedersen_statement: RingPedersenStatement, - pub(crate) ring_pedersen_proof: RingPedersenProof, - #[serde(skip)] - pub hash_choice: HashChoice, + pub(crate) old_party_index: u16, + pub(crate) party_index: u16, + pdl_proof_vec: Vec>, + range_proofs: Vec>, + coefficients_committed_vec: VerifiableSS, + pub(crate) points_committed_vec: Vec>, + points_encrypted_vec: Vec, + dk_correctness_proof: NiCorrectKeyProof, + pub(crate) dlog_statement: DLogStatement, + pub(crate) ek: EncryptionKey, + pub(crate) remove_party_indices: Vec, + pub(crate) public_key: Point, + pub(crate) ring_pedersen_statement: RingPedersenStatement, + pub(crate) ring_pedersen_proof: RingPedersenProof, + #[serde(skip)] + pub hash_choice: HashChoice, } impl RefreshMessage { - pub fn distribute( - old_party_index: u16, - local_key: &mut LocalKey, - new_t: u16, - new_n: u16, - ) -> FsDkrResult<(RefreshMessage, DecryptionKey)> { - assert!(new_t <= new_n / 2); - let secret = local_key.keys_linear.x_i.clone(); - // secret share old key - if new_n <= new_t { - return Err(FsDkrError::NewPartyUnassignedIndexError); - } - let (vss_scheme, secret_shares) = VerifiableSS::::share(new_t, new_n, &secret); - - local_key.vss_scheme = vss_scheme.clone(); - - // commit to points on the polynomial - let points_committed_vec: Vec<_> = (0..secret_shares.len()) - .map(|i| Point::::generator() * &secret_shares[i].clone().into()) - .collect(); - - // encrypt points on the polynomial using Paillier keys - let (points_encrypted_vec, randomness_vec): (Vec<_>, Vec<_>) = (0..secret_shares.len()) - .map(|i| { - let randomness = BigInt::sample_below(&local_key.paillier_key_vec[i].n); - let ciphertext = Paillier::encrypt_with_chosen_randomness( - &local_key.paillier_key_vec[i], - RawPlaintext::from(secret_shares[i].to_bigint()), - &Randomness::from(randomness.clone()), - ) - .0 - .into_owned(); - (ciphertext, randomness) - }) - .unzip(); - - // generate PDL proofs for each {point_committed, point_encrypted} pair - let pdl_proof_vec: Vec<_> = (0..secret_shares.len()) - .map(|i| { - let witness = PDLwSlackWitness { - x: secret_shares[i].clone(), - r: randomness_vec[i].clone(), - }; - let statement = PDLwSlackStatement { - ciphertext: points_encrypted_vec[i].clone(), - ek: local_key.paillier_key_vec[i].clone(), - Q: points_committed_vec[i].clone(), - G: Point::::generator().to_point(), - h1: local_key.h1_h2_n_tilde_vec[i].g.clone(), - h2: local_key.h1_h2_n_tilde_vec[i].ni.clone(), - N_tilde: local_key.h1_h2_n_tilde_vec[i].N.clone(), - }; - PDLwSlackProof::prove(&witness, &statement) - }) - .collect(); - - let range_proofs = (0..secret_shares.len()) - .map(|i| { - AliceProof::generate( - &secret_shares[i].to_bigint(), - &points_encrypted_vec[i], - &local_key.paillier_key_vec[i], - &local_key.h1_h2_n_tilde_vec[i], - &randomness_vec[i], - ) - }) - .collect(); - - let (ek, dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - let dk_correctness_proof = NiCorrectKeyProof::proof(&dk, None); - - let (ring_pedersen_statement, ring_pedersen_witness) = RingPedersenStatement::generate(); - - let ring_pedersen_proof = - RingPedersenProof::prove(&ring_pedersen_witness, &ring_pedersen_statement); - Ok(( - RefreshMessage { - old_party_index, - party_index: local_key.i, - pdl_proof_vec, - range_proofs, - coefficients_committed_vec: vss_scheme, - points_committed_vec, - points_encrypted_vec, - dk_correctness_proof, - dlog_statement: local_key.h1_h2_n_tilde_vec[(local_key.i - 1) as usize].clone(), - ek, - remove_party_indices: Vec::new(), - public_key: local_key.y_sum_s.clone(), - ring_pedersen_statement, - ring_pedersen_proof, - hash_choice: HashChoice::new(), - }, - dk, - )) - } - - pub fn validate_collect(refresh_messages: &[Self], current_t: u16, new_n: u16) -> FsDkrResult<()> { - // check we got at least current threshold t + 1 refresh messages - // (i.e a quorum of existing parties has sent refresh messages). - if refresh_messages.len() <= current_t.into() { - return Err(FsDkrError::PartiesThresholdViolation { - threshold: current_t, - refreshed_keys: refresh_messages.len(), - }); - } - - // check all vectors are of same length - let reference_len = refresh_messages[0].pdl_proof_vec.len(); - - for (k, refresh_message) in refresh_messages.iter().enumerate() { - let pdl_proof_len = refresh_message.pdl_proof_vec.len(); - let points_commited_len = refresh_message.points_committed_vec.len(); - let points_encrypted_len = refresh_message.points_encrypted_vec.len(); - - if !(pdl_proof_len == reference_len - && points_commited_len == reference_len - && points_encrypted_len == reference_len) - { - return Err(FsDkrError::SizeMismatchError { - refresh_message_index: k, - pdl_proof_len, - points_commited_len, - points_encrypted_len, - }); - } - } - - for refresh_message in refresh_messages.iter() { - for i in 0..new_n as usize { - //TODO: we should handle the case of t( - refresh_messages: &'a [Self], - party_index: u16, - parameters: &'a ShamirSecretSharing, - ek: &'a EncryptionKey, - current_t: u16, - ) -> (RawCiphertext<'a>, Vec>) { - // TODO: check we have large enough qualified set , at least t+1 - //decrypt the new share - // we first homomorphically add all ciphertext encrypted using our encryption key - let ciphertext_vec: Vec<_> = (0..refresh_messages.len()) - .map(|k| refresh_messages[k].points_encrypted_vec[(party_index - 1) as usize].clone()) - .collect(); - - let indices: Vec = (0..(current_t + 1) as usize) - .map(|i| refresh_messages[i].old_party_index - 1) - .collect(); - - // optimization - one decryption - let li_vec: Vec<_> = (0..current_t as usize + 1) - .map(|i| { - VerifiableSS::::map_share_to_new_params( - parameters.clone().borrow(), - indices[i], - &indices, - ) - }) - .collect(); - - let ciphertext_vec_at_indices_mapped: Vec<_> = (0..(current_t + 1) as usize) - .map(|i| { - Paillier::mul( - ek, - RawCiphertext::from(ciphertext_vec[i].clone()), - RawPlaintext::from(li_vec[i].to_bigint()), - ) - }) - .collect(); - - let ciphertext_sum = ciphertext_vec_at_indices_mapped.iter().fold( - Paillier::encrypt(ek, RawPlaintext::from(BigInt::zero())), - |acc, x| Paillier::add(ek, acc, x.clone()), - ); - - (ciphertext_sum, li_vec) - } - - pub fn replace( - new_parties: &[JoinMessage], - key: &mut LocalKey, - old_to_new_map: &HashMap, - new_t: u16, - new_n: u16, - ) -> FsDkrResult<(Self, DecryptionKey)> { - let current_len = key.paillier_key_vec.len() as u16; - let mut paillier_key_h1_h2_n_tilde_hash_map: HashMap = - HashMap::new(); - for old_party_index in old_to_new_map.keys() { - let paillier_key = key - .paillier_key_vec - .get((old_party_index - 1) as usize) - .unwrap() - .clone(); - let h1_h2_n_tilde = key - .h1_h2_n_tilde_vec - .get((old_party_index - 1) as usize) - .unwrap() - .clone(); - paillier_key_h1_h2_n_tilde_hash_map.insert( - old_to_new_map.get(old_party_index).unwrap().clone(), - (paillier_key, h1_h2_n_tilde), - ); - } - - for new_party_index in paillier_key_h1_h2_n_tilde_hash_map.keys() { - if new_party_index.clone() <= current_len { - key.paillier_key_vec[(new_party_index - 1) as usize] = - paillier_key_h1_h2_n_tilde_hash_map - .get(new_party_index) - .unwrap() - .clone() - .0; - key.h1_h2_n_tilde_vec[(new_party_index - 1) as usize] = - paillier_key_h1_h2_n_tilde_hash_map - .get(new_party_index) - .unwrap() - .clone() - .1; - } else { - key.paillier_key_vec.insert( - (new_party_index - 1) as usize, - paillier_key_h1_h2_n_tilde_hash_map - .get(new_party_index) - .unwrap() - .clone() - .0, - ); - key.h1_h2_n_tilde_vec.insert( - (new_party_index - 1) as usize, - paillier_key_h1_h2_n_tilde_hash_map - .get(new_party_index) - .unwrap() - .clone() - .1, - ); - } - } - - for join_message in new_parties.iter() { - let party_index = join_message.get_party_index()?; - if party_index <= current_len { - key.paillier_key_vec[(party_index - 1) as usize] = join_message.ek.clone(); - key.h1_h2_n_tilde_vec[(party_index - 1) as usize] = - join_message.dlog_statement.clone(); - } else { - key.paillier_key_vec - .insert((party_index - 1) as usize, join_message.ek.clone()); - key.h1_h2_n_tilde_vec.insert( - (party_index - 1) as usize, - join_message.dlog_statement.clone(), - ); - } - } - let old_party_index = key.i; - key.i = *old_to_new_map.get(&key.i).unwrap(); - key.t = new_t; - key.n = new_n; - - RefreshMessage::distribute(old_party_index, key, new_t, new_n) - } - - pub fn collect( - refresh_messages: &[Self], - mut local_key: &mut LocalKey, - new_dk: DecryptionKey, - join_messages: &[JoinMessage], - current_t: u16, - ) -> FsDkrResult<()> { - let new_n = refresh_messages.len() + join_messages.len(); - RefreshMessage::validate_collect(refresh_messages, current_t, new_n as u16)?; - - for refresh_message in refresh_messages.iter() { - for i in 0..(new_n as usize) { - let statement = PDLwSlackStatement { - ciphertext: refresh_message.points_encrypted_vec[i].clone(), - ek: local_key.paillier_key_vec[i].clone(), - Q: refresh_message.points_committed_vec[i].clone(), - G: Point::::generator().to_point(), - h1: local_key.h1_h2_n_tilde_vec[i].g.clone(), - h2: local_key.h1_h2_n_tilde_vec[i].ni.clone(), - N_tilde: local_key.h1_h2_n_tilde_vec[i].N.clone(), - }; - refresh_message.pdl_proof_vec[i].verify(&statement)?; - if !refresh_message.range_proofs[i].verify( - &statement.ciphertext, - &statement.ek, - &local_key.h1_h2_n_tilde_vec[i], - ) { - return Err(FsDkrError::RangeProof { party_index: i }); - } - } - } - - // Verify ring-pedersen parameters - for refresh_message in refresh_messages.iter() { - RingPedersenProof::verify( - &refresh_message.ring_pedersen_proof, - &refresh_message.ring_pedersen_statement, - )?; - } - - for join_message in join_messages.iter() { - RingPedersenProof::verify( - &join_message.ring_pedersen_proof, - &join_message.ring_pedersen_statement, - )?; - } - - let old_ek = local_key.paillier_key_vec[(local_key.i - 1) as usize].clone(); - let (cipher_text_sum, li_vec) = RefreshMessage::get_ciphertext_sum( - refresh_messages, - local_key.i, - &local_key.vss_scheme.parameters, - &old_ek, - current_t - ); - - for refresh_message in refresh_messages.iter() { - if refresh_message - .dk_correctness_proof - .verify(&refresh_message.ek, SALT_STRING) - .is_err() - { - return Err(FsDkrError::PaillierVerificationError { - party_index: refresh_message.party_index, - }); - } - let n_length = refresh_message.ek.n.bit_length(); - if n_length > crate::PAILLIER_KEY_SIZE || n_length < crate::PAILLIER_KEY_SIZE - 1 { - return Err(FsDkrError::ModuliTooSmall { - party_index: refresh_message.party_index, - moduli_size: n_length, - }); - } - - // if the proof checks, we add the new paillier public key to the key - local_key.paillier_key_vec[(refresh_message.party_index - 1) as usize] = - refresh_message.ek.clone(); - } - - for join_message in join_messages { - let party_index = join_message.get_party_index()?; - - if join_message - .dk_correctness_proof - .verify(&join_message.ek, SALT_STRING) - .is_err() - { - return Err(FsDkrError::PaillierVerificationError { party_index }); - } - - // creating an inverse dlog statement - let dlog_statement_base_h2 = DLogStatement { - N: join_message.dlog_statement.N.clone(), - g: join_message.dlog_statement.ni.clone(), - ni: join_message.dlog_statement.g.clone(), - }; - if join_message - .composite_dlog_proof_base_h1 - .verify(&join_message.dlog_statement) - .is_err() - || join_message - .composite_dlog_proof_base_h2 - .verify(&dlog_statement_base_h2) - .is_err() - { - return Err(FsDkrError::DLogProofValidation { party_index }); - } - - let n_length = join_message.ek.n.bit_length(); - if n_length > crate::PAILLIER_KEY_SIZE || n_length < crate::PAILLIER_KEY_SIZE - 1 { - return Err(FsDkrError::ModuliTooSmall { - party_index: join_message.get_party_index()?, - moduli_size: n_length, - }); - } - - // if the proof checks, we add the new paillier public key to the key - local_key.paillier_key_vec[(party_index - 1) as usize] = join_message.ek.clone(); - } - - let new_share = Paillier::decrypt(&local_key.paillier_dk, cipher_text_sum) - .0 - .into_owned(); - - let new_share_fe: Scalar = Scalar::::from(&new_share); - - // zeroize the old dk key - local_key.paillier_dk.q.zeroize(); - local_key.paillier_dk.p.zeroize(); - local_key.paillier_dk = new_dk; - - // update old key and output new key - local_key.keys_linear.x_i = new_share_fe.clone(); - local_key.keys_linear.y = Point::::generator() * new_share_fe; - - // update local key list of local public keys (X_i = g^x_i is updated by adding all committed points to that party) - for i in 0..refresh_messages.len() + join_messages.len() as usize { - local_key.pk_vec.insert( - i, - refresh_messages[0].points_committed_vec[i].clone() * li_vec[0].clone(), - ); - for j in 1..current_t as usize + 1 { - local_key.pk_vec[i] = local_key.pk_vec[i].clone() - + refresh_messages[j].points_committed_vec[i].clone() * li_vec[j].clone(); - } - } - - Ok(()) - } + pub fn distribute( + old_party_index: u16, + local_key: &mut LocalKey, + new_t: u16, + new_n: u16, + ) -> FsDkrResult<(RefreshMessage, DecryptionKey)> { + assert!(new_t <= new_n / 2); + let secret = local_key.keys_linear.x_i.clone(); + // secret share old key + if new_n <= new_t { + return Err(FsDkrError::NewPartyUnassignedIndexError) + } + let (vss_scheme, secret_shares) = + VerifiableSS::::share(new_t, new_n, &secret); + + local_key.vss_scheme = vss_scheme.clone(); + + // commit to points on the polynomial + let points_committed_vec: Vec<_> = (0..secret_shares.len()) + .map(|i| Point::::generator() * &secret_shares[i].clone().into()) + .collect(); + + // encrypt points on the polynomial using Paillier keys + let (points_encrypted_vec, randomness_vec): (Vec<_>, Vec<_>) = (0..secret_shares.len()) + .map(|i| { + let randomness = BigInt::sample_below(&local_key.paillier_key_vec[i].n); + let ciphertext = Paillier::encrypt_with_chosen_randomness( + &local_key.paillier_key_vec[i], + RawPlaintext::from(secret_shares[i].to_bigint()), + &Randomness::from(randomness.clone()), + ) + .0 + .into_owned(); + (ciphertext, randomness) + }) + .unzip(); + + // generate PDL proofs for each {point_committed, point_encrypted} pair + let pdl_proof_vec: Vec<_> = (0..secret_shares.len()) + .map(|i| { + let witness = + PDLwSlackWitness { x: secret_shares[i].clone(), r: randomness_vec[i].clone() }; + let statement = PDLwSlackStatement { + ciphertext: points_encrypted_vec[i].clone(), + ek: local_key.paillier_key_vec[i].clone(), + Q: points_committed_vec[i].clone(), + G: Point::::generator().to_point(), + h1: local_key.h1_h2_n_tilde_vec[i].g.clone(), + h2: local_key.h1_h2_n_tilde_vec[i].ni.clone(), + N_tilde: local_key.h1_h2_n_tilde_vec[i].N.clone(), + }; + PDLwSlackProof::prove(&witness, &statement) + }) + .collect(); + + let range_proofs = (0..secret_shares.len()) + .map(|i| { + AliceProof::generate( + &secret_shares[i].to_bigint(), + &points_encrypted_vec[i], + &local_key.paillier_key_vec[i], + &local_key.h1_h2_n_tilde_vec[i], + &randomness_vec[i], + ) + }) + .collect(); + + let (ek, dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + let dk_correctness_proof = NiCorrectKeyProof::proof(&dk, None); + + let (ring_pedersen_statement, ring_pedersen_witness) = RingPedersenStatement::generate(); + + let ring_pedersen_proof = + RingPedersenProof::prove(&ring_pedersen_witness, &ring_pedersen_statement); + Ok(( + RefreshMessage { + old_party_index, + party_index: local_key.i, + pdl_proof_vec, + range_proofs, + coefficients_committed_vec: vss_scheme, + points_committed_vec, + points_encrypted_vec, + dk_correctness_proof, + dlog_statement: local_key.h1_h2_n_tilde_vec[(local_key.i - 1) as usize].clone(), + ek, + remove_party_indices: Vec::new(), + public_key: local_key.y_sum_s.clone(), + ring_pedersen_statement, + ring_pedersen_proof, + hash_choice: HashChoice::new(), + }, + dk, + )) + } + + pub fn validate_collect( + refresh_messages: &[Self], + current_t: u16, + new_n: u16, + ) -> FsDkrResult<()> { + // check we got at least current threshold t + 1 refresh messages + // (i.e a quorum of existing parties has sent refresh messages). + if refresh_messages.len() <= current_t.into() { + return Err(FsDkrError::PartiesThresholdViolation { + threshold: current_t, + refreshed_keys: refresh_messages.len(), + }) + } + + // check all vectors are of same length + let reference_len = refresh_messages[0].pdl_proof_vec.len(); + + for (k, refresh_message) in refresh_messages.iter().enumerate() { + let pdl_proof_len = refresh_message.pdl_proof_vec.len(); + let points_commited_len = refresh_message.points_committed_vec.len(); + let points_encrypted_len = refresh_message.points_encrypted_vec.len(); + + if !(pdl_proof_len == reference_len && + points_commited_len == reference_len && + points_encrypted_len == reference_len) + { + return Err(FsDkrError::SizeMismatchError { + refresh_message_index: k, + pdl_proof_len, + points_commited_len, + points_encrypted_len, + }) + } + } + + for refresh_message in refresh_messages.iter() { + for i in 0..new_n as usize { + //TODO: we should handle the case of t( + refresh_messages: &'a [Self], + party_index: u16, + parameters: &'a ShamirSecretSharing, + ek: &'a EncryptionKey, + current_t: u16, + ) -> (RawCiphertext<'a>, Vec>) { + // TODO: check we have large enough qualified set , at least t+1 + //decrypt the new share + // we first homomorphically add all ciphertext encrypted using our encryption key + let ciphertext_vec: Vec<_> = (0..refresh_messages.len()) + .map(|k| refresh_messages[k].points_encrypted_vec[(party_index - 1) as usize].clone()) + .collect(); + + let indices: Vec = (0..(current_t + 1) as usize) + .map(|i| refresh_messages[i].old_party_index - 1) + .collect(); + + // optimization - one decryption + let li_vec: Vec<_> = (0..current_t as usize + 1) + .map(|i| { + VerifiableSS::::map_share_to_new_params( + parameters.clone().borrow(), + indices[i], + &indices, + ) + }) + .collect(); + + let ciphertext_vec_at_indices_mapped: Vec<_> = (0..(current_t + 1) as usize) + .map(|i| { + Paillier::mul( + ek, + RawCiphertext::from(ciphertext_vec[i].clone()), + RawPlaintext::from(li_vec[i].to_bigint()), + ) + }) + .collect(); + + let ciphertext_sum = ciphertext_vec_at_indices_mapped + .iter() + .fold(Paillier::encrypt(ek, RawPlaintext::from(BigInt::zero())), |acc, x| { + Paillier::add(ek, acc, x.clone()) + }); + + (ciphertext_sum, li_vec) + } + + pub fn replace( + new_parties: &[JoinMessage], + key: &mut LocalKey, + old_to_new_map: &HashMap, + new_t: u16, + new_n: u16, + ) -> FsDkrResult<(Self, DecryptionKey)> { + let current_len = key.paillier_key_vec.len() as u16; + let mut paillier_key_h1_h2_n_tilde_hash_map: HashMap = + HashMap::new(); + for old_party_index in old_to_new_map.keys() { + let paillier_key = + key.paillier_key_vec.get((old_party_index - 1) as usize).unwrap().clone(); + let h1_h2_n_tilde = + key.h1_h2_n_tilde_vec.get((old_party_index - 1) as usize).unwrap().clone(); + paillier_key_h1_h2_n_tilde_hash_map.insert( + old_to_new_map.get(old_party_index).unwrap().clone(), + (paillier_key, h1_h2_n_tilde), + ); + } + + for new_party_index in paillier_key_h1_h2_n_tilde_hash_map.keys() { + if new_party_index.clone() <= current_len { + key.paillier_key_vec[(new_party_index - 1) as usize] = + paillier_key_h1_h2_n_tilde_hash_map.get(new_party_index).unwrap().clone().0; + key.h1_h2_n_tilde_vec[(new_party_index - 1) as usize] = + paillier_key_h1_h2_n_tilde_hash_map.get(new_party_index).unwrap().clone().1; + } else { + key.paillier_key_vec.insert( + (new_party_index - 1) as usize, + paillier_key_h1_h2_n_tilde_hash_map.get(new_party_index).unwrap().clone().0, + ); + key.h1_h2_n_tilde_vec.insert( + (new_party_index - 1) as usize, + paillier_key_h1_h2_n_tilde_hash_map.get(new_party_index).unwrap().clone().1, + ); + } + } + + for join_message in new_parties.iter() { + let party_index = join_message.get_party_index()?; + if party_index <= current_len { + key.paillier_key_vec[(party_index - 1) as usize] = join_message.ek.clone(); + key.h1_h2_n_tilde_vec[(party_index - 1) as usize] = + join_message.dlog_statement.clone(); + } else { + key.paillier_key_vec.insert((party_index - 1) as usize, join_message.ek.clone()); + key.h1_h2_n_tilde_vec + .insert((party_index - 1) as usize, join_message.dlog_statement.clone()); + } + } + let old_party_index = key.i; + key.i = *old_to_new_map.get(&key.i).unwrap(); + key.t = new_t; + key.n = new_n; + + RefreshMessage::distribute(old_party_index, key, new_t, new_n) + } + + pub fn collect( + refresh_messages: &[Self], + mut local_key: &mut LocalKey, + new_dk: DecryptionKey, + join_messages: &[JoinMessage], + current_t: u16, + ) -> FsDkrResult<()> { + let new_n = refresh_messages.len() + join_messages.len(); + RefreshMessage::validate_collect(refresh_messages, current_t, new_n as u16)?; + + for refresh_message in refresh_messages.iter() { + for i in 0..(new_n as usize) { + let statement = PDLwSlackStatement { + ciphertext: refresh_message.points_encrypted_vec[i].clone(), + ek: local_key.paillier_key_vec[i].clone(), + Q: refresh_message.points_committed_vec[i].clone(), + G: Point::::generator().to_point(), + h1: local_key.h1_h2_n_tilde_vec[i].g.clone(), + h2: local_key.h1_h2_n_tilde_vec[i].ni.clone(), + N_tilde: local_key.h1_h2_n_tilde_vec[i].N.clone(), + }; + refresh_message.pdl_proof_vec[i].verify(&statement)?; + if !refresh_message.range_proofs[i].verify( + &statement.ciphertext, + &statement.ek, + &local_key.h1_h2_n_tilde_vec[i], + ) { + return Err(FsDkrError::RangeProof { party_index: i }) + } + } + } + + // Verify ring-pedersen parameters + for refresh_message in refresh_messages.iter() { + RingPedersenProof::verify( + &refresh_message.ring_pedersen_proof, + &refresh_message.ring_pedersen_statement, + )?; + } + + for join_message in join_messages.iter() { + RingPedersenProof::verify( + &join_message.ring_pedersen_proof, + &join_message.ring_pedersen_statement, + )?; + } + + let old_ek = local_key.paillier_key_vec[(local_key.i - 1) as usize].clone(); + let (cipher_text_sum, li_vec) = RefreshMessage::get_ciphertext_sum( + refresh_messages, + local_key.i, + &local_key.vss_scheme.parameters, + &old_ek, + current_t, + ); + + for refresh_message in refresh_messages.iter() { + if refresh_message + .dk_correctness_proof + .verify(&refresh_message.ek, SALT_STRING) + .is_err() + { + return Err(FsDkrError::PaillierVerificationError { + party_index: refresh_message.party_index, + }) + } + let n_length = refresh_message.ek.n.bit_length(); + if n_length > crate::PAILLIER_KEY_SIZE || n_length < crate::PAILLIER_KEY_SIZE - 1 { + return Err(FsDkrError::ModuliTooSmall { + party_index: refresh_message.party_index, + moduli_size: n_length, + }) + } + + // if the proof checks, we add the new paillier public key to the key + local_key.paillier_key_vec[(refresh_message.party_index - 1) as usize] = + refresh_message.ek.clone(); + } + + for join_message in join_messages { + let party_index = join_message.get_party_index()?; + + if join_message.dk_correctness_proof.verify(&join_message.ek, SALT_STRING).is_err() { + return Err(FsDkrError::PaillierVerificationError { party_index }) + } + + // creating an inverse dlog statement + let dlog_statement_base_h2 = DLogStatement { + N: join_message.dlog_statement.N.clone(), + g: join_message.dlog_statement.ni.clone(), + ni: join_message.dlog_statement.g.clone(), + }; + if join_message + .composite_dlog_proof_base_h1 + .verify(&join_message.dlog_statement) + .is_err() || join_message + .composite_dlog_proof_base_h2 + .verify(&dlog_statement_base_h2) + .is_err() + { + return Err(FsDkrError::DLogProofValidation { party_index }) + } + + let n_length = join_message.ek.n.bit_length(); + if n_length > crate::PAILLIER_KEY_SIZE || n_length < crate::PAILLIER_KEY_SIZE - 1 { + return Err(FsDkrError::ModuliTooSmall { + party_index: join_message.get_party_index()?, + moduli_size: n_length, + }) + } + + // if the proof checks, we add the new paillier public key to the key + local_key.paillier_key_vec[(party_index - 1) as usize] = join_message.ek.clone(); + } + + let new_share = Paillier::decrypt(&local_key.paillier_dk, cipher_text_sum).0.into_owned(); + + let new_share_fe: Scalar = Scalar::::from(&new_share); + + // zeroize the old dk key + local_key.paillier_dk.q.zeroize(); + local_key.paillier_dk.p.zeroize(); + local_key.paillier_dk = new_dk; + + // update old key and output new key + local_key.keys_linear.x_i = new_share_fe.clone(); + local_key.keys_linear.y = Point::::generator() * new_share_fe; + + // update local key list of local public keys (X_i = g^x_i is updated by adding all + // committed points to that party) + for i in 0..refresh_messages.len() + join_messages.len() as usize { + local_key + .pk_vec + .insert(i, refresh_messages[0].points_committed_vec[i].clone() * li_vec[0].clone()); + for j in 1..current_t as usize + 1 { + local_key.pk_vec[i] = local_key.pk_vec[i].clone() + + refresh_messages[j].points_committed_vec[i].clone() * li_vec[j].clone(); + } + } + + Ok(()) + } } diff --git a/fs-dkr/src/ring_pedersen_proof.rs b/fs-dkr/src/ring_pedersen_proof.rs index c3cfe17a..cd21ac7b 100644 --- a/fs-dkr/src/ring_pedersen_proof.rs +++ b/fs-dkr/src/ring_pedersen_proof.rs @@ -1,179 +1,155 @@ #![allow(non_snake_case)] /* - Ring Pedersen Proof - Copyright 2022 by Webb Technologies. - - ring_pedersen_proof.rs is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + Ring Pedersen Proof + Copyright 2022 by Webb Technologies. + + ring_pedersen_proof.rs is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + @license GPL-3.0+ */ use bitvec::prelude::*; -use curv::arithmetic::traits::*; -use curv::cryptographic_primitives::hashing::Digest; -use curv::cryptographic_primitives::hashing::DigestExt; -use curv::elliptic::curves::Curve; -use curv::BigInt; -use paillier::EncryptionKey; -use paillier::{KeyGeneration, Paillier}; +use curv::{ + arithmetic::traits::*, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::Curve, + BigInt, +}; +use paillier::{EncryptionKey, KeyGeneration, Paillier}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; -use crate::error::FsDkrError; -use crate::error::FsDkrResult; +use crate::error::{FsDkrError, FsDkrResult}; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(bound = "E: Curve, H: Digest + Clone")] pub struct RingPedersenStatement { - pub S: BigInt, - pub T: BigInt, - pub N: BigInt, - phi: BigInt, - pub ek: EncryptionKey, - #[serde(skip)] - phantom: PhantomData<(E, H)>, + pub S: BigInt, + pub T: BigInt, + pub N: BigInt, + phi: BigInt, + pub ek: EncryptionKey, + #[serde(skip)] + phantom: PhantomData<(E, H)>, } pub struct RingPedersenWitness { - p: BigInt, - q: BigInt, - lambda: BigInt, - phantom: PhantomData<(E, H)>, + p: BigInt, + q: BigInt, + lambda: BigInt, + phantom: PhantomData<(E, H)>, } impl RingPedersenStatement { - pub fn generate() -> (Self, RingPedersenWitness) { - let (ek_tilde, dk_tilde) = - Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let r = BigInt::sample_below(&ek_tilde.n); - let lambda = BigInt::sample_below(&phi); - let t = BigInt::mod_pow(&r, &BigInt::from(2), &ek_tilde.n); - let s = BigInt::mod_pow(&t, &lambda, &ek_tilde.n); - - ( - Self { - S: s, - T: t, - N: ek_tilde.clone().n, - phi: phi, - ek: ek_tilde, - phantom: PhantomData, - }, - RingPedersenWitness { - p: dk_tilde.p, - q: dk_tilde.q, - lambda, - phantom: PhantomData, - }, - ) - } + pub fn generate() -> (Self, RingPedersenWitness) { + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let r = BigInt::sample_below(&ek_tilde.n); + let lambda = BigInt::sample_below(&phi); + let t = BigInt::mod_pow(&r, &BigInt::from(2), &ek_tilde.n); + let s = BigInt::mod_pow(&t, &lambda, &ek_tilde.n); + + ( + Self { S: s, T: t, N: ek_tilde.clone().n, phi, ek: ek_tilde, phantom: PhantomData }, + RingPedersenWitness { p: dk_tilde.p, q: dk_tilde.q, lambda, phantom: PhantomData }, + ) + } } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(bound = "E: Curve, H: Digest + Clone")] pub struct RingPedersenProof { - A: Vec, - Z: Vec, - #[serde(skip)] - phantom: PhantomData<(E, H)>, + A: Vec, + Z: Vec, + #[serde(skip)] + phantom: PhantomData<(E, H)>, } // Link to the UC non-interactive threshold ECDSA paper impl RingPedersenProof { - pub fn prove( - witness: &RingPedersenWitness, - statement: &RingPedersenStatement, - ) -> RingPedersenProof { - // 1. Sample alphas from 1 -> m from \phi(N) - let mut a = [(); M].map(|_| BigInt::zero()); - let mut A = [(); M].map(|_| BigInt::zero()); - let mut hash = H::new(); - for i in 0..M { - // TODO: Consider ensuring we get a unit element of this subgroup - let a_i = BigInt::sample_below(&statement.phi); - a[i] = a_i.clone(); - let A_i = BigInt::mod_pow(&statement.T, &a_i, &statement.N); - A[i] = A_i.clone(); - hash = H::chain_bigint(hash, &A_i); - } - - let e: BigInt = hash.result_bigint(); - let bitwise_e: BitVec = BitVec::from_vec(e.to_bytes()); - - let mut Z = [(); M].map(|_| BigInt::zero()); - for i in 0..M { - let e_i = if bitwise_e[i] { - BigInt::one() - } else { - BigInt::zero() - }; - let z_i = BigInt::mod_add(&a[i], &(e_i * &witness.lambda), &statement.phi); - Z[i] = z_i; - } - - Self { - A: A.to_vec(), - Z: Z.to_vec(), - phantom: PhantomData, - } - } - - pub fn verify( - proof: &RingPedersenProof, - statement: &RingPedersenStatement, - ) -> FsDkrResult<()> { - let mut hash = H::new(); - for i in 0..M { - hash = H::chain_bigint(hash, &proof.A[i]); - } - - let e: BigInt = hash.result_bigint(); - let bitwise_e: BitVec = BitVec::from_vec(e.to_bytes()); - - for i in 0..M { - let mut e_i = 0; - if bitwise_e[i] { - e_i = 1; - } - - if BigInt::mod_pow(&statement.T, &proof.Z[i], &statement.N) - == BigInt::mod_mul( - &proof.A[i], - &BigInt::mod_pow(&statement.S, &BigInt::from(e_i), &statement.N), - &statement.N, - ) - { - continue; - } else { - return Err(FsDkrError::RingPedersenProofError); - } - } - Ok(()) - } + pub fn prove( + witness: &RingPedersenWitness, + statement: &RingPedersenStatement, + ) -> RingPedersenProof { + // 1. Sample alphas from 1 -> m from \phi(N) + let mut a = [(); M].map(|_| BigInt::zero()); + let mut A = [(); M].map(|_| BigInt::zero()); + let mut hash = H::new(); + for i in 0..M { + // TODO: Consider ensuring we get a unit element of this subgroup + let a_i = BigInt::sample_below(&statement.phi); + a[i] = a_i.clone(); + let A_i = BigInt::mod_pow(&statement.T, &a_i, &statement.N); + A[i] = A_i.clone(); + hash = H::chain_bigint(hash, &A_i); + } + + let e: BigInt = hash.result_bigint(); + let bitwise_e: BitVec = BitVec::from_vec(e.to_bytes()); + + let mut Z = [(); M].map(|_| BigInt::zero()); + for i in 0..M { + let e_i = if bitwise_e[i] { BigInt::one() } else { BigInt::zero() }; + let z_i = BigInt::mod_add(&a[i], &(e_i * &witness.lambda), &statement.phi); + Z[i] = z_i; + } + + Self { A: A.to_vec(), Z: Z.to_vec(), phantom: PhantomData } + } + + pub fn verify( + proof: &RingPedersenProof, + statement: &RingPedersenStatement, + ) -> FsDkrResult<()> { + let mut hash = H::new(); + for i in 0..M { + hash = H::chain_bigint(hash, &proof.A[i]); + } + + let e: BigInt = hash.result_bigint(); + let bitwise_e: BitVec = BitVec::from_vec(e.to_bytes()); + + for i in 0..M { + let mut e_i = 0; + if bitwise_e[i] { + e_i = 1; + } + + if BigInt::mod_pow(&statement.T, &proof.Z[i], &statement.N) == + BigInt::mod_mul( + &proof.A[i], + &BigInt::mod_pow(&statement.S, &BigInt::from(e_i), &statement.N), + &statement.N, + ) { + continue + } else { + return Err(FsDkrError::RingPedersenProofError) + } + } + Ok(()) + } } #[cfg(test)] mod tests { - use super::*; - use curv::elliptic::curves::secp256_k1::Secp256k1; - use sha2::Sha256; - - #[test] - fn test_ring_pedersen() { - let (statement, witness) = RingPedersenStatement::::generate(); - let proof = RingPedersenProof::::prove( - &witness, &statement, - ); - assert!( - RingPedersenProof::::verify( - &proof, &statement - ) - .is_ok() - ); - } + use super::*; + use curv::elliptic::curves::secp256_k1::Secp256k1; + use sha2::Sha256; + + #[test] + fn test_ring_pedersen() { + let (statement, witness) = RingPedersenStatement::::generate(); + let proof = RingPedersenProof::::prove( + &witness, &statement, + ); + assert!(RingPedersenProof::::verify( + &proof, &statement + ) + .is_ok()); + } } diff --git a/fs-dkr/src/test.rs b/fs-dkr/src/test.rs index a9fc8410..add0a629 100644 --- a/fs-dkr/src/test.rs +++ b/fs-dkr/src/test.rs @@ -1,419 +1,396 @@ #[cfg(test)] mod tests { - use crate::refresh_message::RefreshMessage; - use curv::arithmetic::Converter; - use curv::cryptographic_primitives::secret_sharing::feldman_vss::{ - ShamirSecretSharing, VerifiableSS, - }; - use curv::elliptic::curves::secp256_k1::Secp256k1Point; - use curv::elliptic::curves::Secp256k1; - use curv::BigInt; - use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::party_i::verify; - use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::party_i::Keys; - use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::keygen::{ - Keygen, LocalKey, - }; - use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::sign::{ - CompletedOfflineStage, OfflineStage, SignManual, - }; - use sha2::Sha256; - - use crate::add_party_message::JoinMessage; - use crate::error::FsDkrResult; - use curv::{ - elliptic::curves::Scalar, - cryptographic_primitives::{ - hashing::Digest, proofs::sigma_dlog::DLogProof} - }; - use paillier::DecryptionKey; - use round_based::dev::Simulation; - use std::collections::HashMap; - - type GE = Secp256k1Point; - - #[test] - fn test1() { - //simulate keygen - let t = 3; - let n = 6; - let mut keys = simulate_keygen(t, n); - - let old_keys = keys.clone(); - simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); - - // check that sum of old keys is equal to sum of new keys - let old_linear_secret_key: Vec<_> = (0..old_keys.len()) - .map(|i| old_keys[i].keys_linear.x_i.clone()) - .collect(); - - let new_linear_secret_key: Vec<_> = (0..keys.len()) - .map(|i| keys[i].keys_linear.x_i.clone()) - .collect(); - let indices: Vec<_> = (0..(t + 1) as u16).collect(); - let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { - threshold: t, - share_count: n, - }, - commitments: Vec::new(), - proof: DLogProof::::prove( - &Scalar::random()), - }; - assert_eq!( - vss.reconstruct(&indices[..], &old_linear_secret_key[0..(t + 1) as usize]), - vss.reconstruct(&indices[..], &new_linear_secret_key[0..(t + 1) as usize]) - ); - assert_ne!(old_linear_secret_key, new_linear_secret_key); - } - - #[test] - fn test_sign_rotate_sign() { - let mut keys = simulate_keygen(2, 5); - let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); - simulate_signing(offline_sign, b"ZenGo"); - simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); - let offline_sign = simulate_offline_stage(keys.clone(), &[2, 3, 4]); - simulate_signing(offline_sign, b"ZenGo"); - simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); - let offline_sign = simulate_offline_stage(keys, &[1, 3, 5]); - simulate_signing(offline_sign, b"ZenGo"); - } - - #[test] - fn test_remove_sign_rotate_sign() { - let mut keys = simulate_keygen(2, 5); - let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); - simulate_signing(offline_sign, b"ZenGo"); - simulate_dkr_removal::<{ crate::M_SECURITY }>(&mut keys, [1].to_vec(), None); - let offline_sign = simulate_offline_stage(keys.clone(), &[2, 3, 4]); - simulate_signing(offline_sign, b"ZenGo"); - simulate_dkr_removal::<{ crate::M_SECURITY }>(&mut keys, [1, 2].to_vec(), None); - let offline_sign = simulate_offline_stage(keys, &[3, 4, 5]); - simulate_signing(offline_sign, b"ZenGo"); - } - - #[test] - fn test_add_party_with_permute() { - fn simulate_replace( - keys: &mut Vec>, - party_indices: &[u16], - old_to_new_map: &HashMap, - t: u16, - n: u16, - ) -> FsDkrResult<()> { - fn generate_join_messages_and_keys( - number_of_new_parties: usize, - ) -> (Vec>, Vec) { - // the new party generates it's join message to start joining the computation - (0..number_of_new_parties) - .map(|_| JoinMessage::distribute()) - .unzip() - } - - fn generate_refresh_parties_replace( - keys: &mut [LocalKey], - old_to_new_map: &HashMap, - join_messages: &[JoinMessage], - ) -> ( - Vec>, - Vec, - ) { - let new_n = (&keys.len() + join_messages.len()) as u16; - keys.iter_mut() - .map(|key| { - RefreshMessage::replace(join_messages, key, old_to_new_map, key.t, new_n).unwrap() - }) - .unzip() - } - - // each party that wants to join generates a join message and a pair of paillier keys. - let (mut join_messages, new_keys) = - generate_join_messages_and_keys::<{ crate::M_SECURITY }>(party_indices.len()); - - // each new party has to be informed through offchannel communication what party index - // it has been assigned (the information is public). - for (join_message, party_index) in join_messages.iter_mut().zip(party_indices) { - join_message.party_index = Some(*party_index); - } - - // each existing party has to generate it's refresh message aware of the new parties - let (refresh_messages, dk_keys) = - generate_refresh_parties_replace(keys, &old_to_new_map, join_messages.as_slice()); - let mut new_keys_vec: Vec<(u16, LocalKey)> = - Vec::with_capacity(keys.len() + join_messages.len()); - // all existing parties rotate aware of the join_messages - for i in 0..keys.len() as usize { - RefreshMessage::collect( - refresh_messages.as_slice(), - &mut keys[i], - dk_keys[i].clone(), - join_messages.as_slice(), - t, - ) - .expect(""); - new_keys_vec.push((keys[i].i - 1, keys[i].clone())); - } - - // all new parties generate a local key - for (join_message, dk) in join_messages.iter().zip(new_keys) { - let party_index = join_message.party_index.unwrap(); - let local_key = join_message.collect( - refresh_messages.as_slice(), - dk, - join_messages.as_slice(), - t, - n, - t, - )?; - - new_keys_vec.push((party_index - 1, local_key)); - } - - new_keys_vec.sort_by(|a, b| a.0.cmp(&b.0)); - let keys_replacements = new_keys_vec - .iter() - .map(|a| a.1.clone()) - .collect::>>(); - *keys = keys_replacements; - Ok(()) - } - - let t = 2; - let n = 7; - - let all_keys = simulate_keygen(t, n); - // Remove the 2nd and 7th party - let mut keys = all_keys.clone(); - keys.remove(6); - keys.remove(1); - - let mut old_to_new_map: HashMap = HashMap::new(); - old_to_new_map.insert(1, 4); - old_to_new_map.insert(3, 1); - old_to_new_map.insert(4, 3); - old_to_new_map.insert(5, 6); - old_to_new_map.insert(6, 5); - - // Simulate the replace - simulate_replace::<{ crate::M_SECURITY }>(&mut keys, &[2, 7], &old_to_new_map, t, n) - .unwrap(); - // check that sum of old keys is equal to sum of new keys - let old_linear_secret_key: Vec<_> = (0..all_keys.len()) - .map(|i| all_keys[i].keys_linear.x_i.clone()) - .collect(); - - let new_linear_secret_key: Vec<_> = (0..keys.len()) - .map(|i| keys[i].keys_linear.x_i.clone()) - .collect(); - let indices: Vec<_> = (0..(t + 1) as u16).collect(); - let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { - threshold: t, - share_count: n, - }, - commitments: Vec::new(), - proof: DLogProof::::prove( - &Scalar::random()), - }; - assert_eq!( - vss.reconstruct(&indices[..], &old_linear_secret_key[0..(t + 1) as usize]), - vss.reconstruct(&indices[..], &new_linear_secret_key[0..(t + 1) as usize]) - ); - assert_ne!(old_linear_secret_key, new_linear_secret_key); - - let offline_sign = simulate_offline_stage(keys, &[1, 2, 7]); - simulate_signing(offline_sign, b"ZenGo"); - } - - #[test] - fn test_change_threshold_sign_rotate_sign() { - let mut keys = simulate_keygen(2, 5); - let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); - simulate_signing(offline_sign, b"ZenGo"); - // Change threshold to 1 (i.e quorum size = 2). - simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, Some(1)); - let offline_sign = simulate_offline_stage(keys.clone(), &[3, 4]); - simulate_signing(offline_sign, b"ZenGo"); - // Change threshold to back to 2 (i.e quorum size = 3). - simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, Some(2)); - let offline_sign = simulate_offline_stage(keys, &[1, 3, 5]); - simulate_signing(offline_sign, b"ZenGo"); - } - - fn simulate_keygen(t: u16, n: u16) -> Vec> { - //simulate keygen - let mut simulation = Simulation::new(); - simulation.enable_benchmarks(false); - - for i in 1..=n { - simulation.add_party(Keygen::new(i, t, n).unwrap()); - } - - simulation.run().unwrap() - } - - fn simulate_dkr_removal( - keys: &mut Vec>, - remove_party_indices: Vec, - new_t_option: Option, - ) { - let mut broadcast_messages: HashMap>> = - HashMap::new(); - let mut new_dks: HashMap = HashMap::new(); - let mut refresh_messages: Vec> = Vec::new(); - let mut party_key: HashMap> = HashMap::new(); - // TODO: Verify this is correct - let new_n = keys.len() as u16; - let current_t = keys[0].t; - let new_t = new_t_option.unwrap_or(current_t); - for key in keys.iter_mut() { - let (refresh_message, new_dk) = RefreshMessage::distribute(key.i, key, new_t, new_n).unwrap(); - refresh_messages.push(refresh_message.clone()); - new_dks.insert(refresh_message.party_index.into(), new_dk); - party_key.insert(refresh_message.party_index.into(), key.clone()); - } - - for refresh_message in refresh_messages.iter() { - broadcast_messages.insert(refresh_message.party_index.into(), Vec::new()); - } - - for refresh_message in refresh_messages.iter_mut() { - if !remove_party_indices.contains(&refresh_message.party_index.into()) { - refresh_message.remove_party_indices = remove_party_indices.clone(); - } else { - let mut new_remove_party_indices = remove_party_indices.clone(); - new_remove_party_indices - .retain(|value| *value != refresh_message.party_index.into()); - refresh_message.remove_party_indices = new_remove_party_indices; - } - - for (party_index, refresh_bucket) in broadcast_messages.iter_mut() { - if refresh_message - .remove_party_indices - .contains(&(*party_index as u16)) - { - continue; - } - refresh_bucket.push(refresh_message.clone()); - } - } - - for remove_party_index in remove_party_indices.iter() { - assert_eq!(broadcast_messages[&(*remove_party_index as usize)].len(), 1); - } - - // keys will be updated to refreshed values - for (party, key) in party_key.iter_mut() { - if remove_party_indices.contains(&(*party as u16)) { - continue; - } - - RefreshMessage::collect( - broadcast_messages[party].clone().as_slice(), - key, - new_dks[party].clone(), - &[], - current_t - ) - .expect(""); - } - - for remove_party_index in remove_party_indices { - let result = RefreshMessage::collect( - &broadcast_messages[&(remove_party_index as usize)], - &mut keys[remove_party_index as usize], - new_dks[&(remove_party_index as usize)].clone(), - &[], - current_t - ); - assert!(result.is_err()); - } - } - - fn simulate_dkr( - keys: &mut Vec>, - new_t_option: Option, - ) -> ( - Vec>, - Vec, - ) { - let mut broadcast_vec: Vec> = Vec::new(); - let mut new_dks: Vec = Vec::new(); - let keys_len = keys.len(); - let current_t = keys[0].t; - let new_t = new_t_option.unwrap_or(current_t); - for key in keys.iter_mut() { - let (refresh_message, new_dk) = - RefreshMessage::distribute(key.i, key, new_t, keys_len as u16).unwrap(); - broadcast_vec.push(refresh_message); - new_dks.push(new_dk); - } - - // keys will be updated to refreshed values - for i in 0..keys.len() as usize { - RefreshMessage::collect(&broadcast_vec, &mut keys[i], new_dks[i].clone(), &[], current_t) - .expect(""); - } - - (broadcast_vec, new_dks) - } - - fn simulate_offline_stage( - local_keys: Vec>, - s_l: &[u16], - ) -> Vec { - let mut simulation = Simulation::new(); - simulation.enable_benchmarks(false); - - for (i, &keygen_i) in (1..).zip(s_l) { - simulation.add_party( - OfflineStage::new( - i, - s_l.to_vec(), - local_keys[usize::from(keygen_i - 1)].clone(), - ) - .unwrap(), - ); - } - - simulation.run().unwrap() - } - - fn simulate_signing(offline: Vec, message: &[u8]) { - let message = create_hash(&[&BigInt::from_bytes(message)]); - let pk = &offline[0].public_key(); - - let parties = offline - .iter() - .map(|o| SignManual::new(message.clone(), o.clone())) - .collect::, _>>() - .unwrap(); - let (parties, local_sigs): (Vec<_>, Vec<_>) = parties.into_iter().unzip(); - // parties.remove(0).complete(&local_sigs[1..]).unwrap(); - let local_sigs_except = |i: usize| { - let mut v = vec![]; - v.extend_from_slice(&local_sigs[..i]); - if i + 1 < local_sigs.len() { - v.extend_from_slice(&local_sigs[i + 1..]); - } - v - }; - - assert!(parties - .into_iter() - .enumerate() - .map(|(i, p)| p.complete(&local_sigs_except(i)).unwrap()) - .all(|signature| verify(&signature, &pk, &message).is_ok())); - } - - fn create_hash(big_ints: &[&BigInt]) -> BigInt { - let hasher = Sha256::new(); - - for value in big_ints { - hasher.clone().chain(&BigInt::to_bytes(value)); - } - - let result_hex = hasher.finalize(); - BigInt::from_bytes(&result_hex[..]) - } + use crate::refresh_message::RefreshMessage; + use curv::{ + arithmetic::Converter, + cryptographic_primitives::secret_sharing::feldman_vss::{ + ShamirSecretSharing, VerifiableSS, + }, + elliptic::curves::{secp256_k1::Secp256k1Point, Secp256k1}, + BigInt, + }; + use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::{ + party_i::{verify, Keys}, + state_machine::{ + keygen::{Keygen, LocalKey}, + sign::{CompletedOfflineStage, OfflineStage, SignManual}, + }, + }; + use sha2::Sha256; + + use crate::{add_party_message::JoinMessage, error::FsDkrResult}; + use curv::{ + cryptographic_primitives::{hashing::Digest, proofs::sigma_dlog::DLogProof}, + elliptic::curves::Scalar, + }; + use paillier::DecryptionKey; + use round_based::dev::Simulation; + use std::collections::HashMap; + + type GE = Secp256k1Point; + + #[test] + fn test1() { + //simulate keygen + let t = 3; + let n = 6; + let mut keys = simulate_keygen(t, n); + + let old_keys = keys.clone(); + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); + + // check that sum of old keys is equal to sum of new keys + let old_linear_secret_key: Vec<_> = + (0..old_keys.len()).map(|i| old_keys[i].keys_linear.x_i.clone()).collect(); + + let new_linear_secret_key: Vec<_> = + (0..keys.len()).map(|i| keys[i].keys_linear.x_i.clone()).collect(); + let indices: Vec<_> = (0..(t + 1) as u16).collect(); + let vss = VerifiableSS:: { + parameters: ShamirSecretSharing { threshold: t, share_count: n }, + commitments: Vec::new(), + proof: DLogProof::::prove(&Scalar::random()), + }; + assert_eq!( + vss.reconstruct(&indices[..], &old_linear_secret_key[0..(t + 1) as usize]), + vss.reconstruct(&indices[..], &new_linear_secret_key[0..(t + 1) as usize]) + ); + assert_ne!(old_linear_secret_key, new_linear_secret_key); + } + + #[test] + fn test_sign_rotate_sign() { + let mut keys = simulate_keygen(2, 5); + let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); + let offline_sign = simulate_offline_stage(keys.clone(), &[2, 3, 4]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); + let offline_sign = simulate_offline_stage(keys, &[1, 3, 5]); + simulate_signing(offline_sign, b"ZenGo"); + } + + #[test] + fn test_remove_sign_rotate_sign() { + let mut keys = simulate_keygen(2, 5); + let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr_removal::<{ crate::M_SECURITY }>(&mut keys, [1].to_vec(), None); + let offline_sign = simulate_offline_stage(keys.clone(), &[2, 3, 4]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr_removal::<{ crate::M_SECURITY }>(&mut keys, [1, 2].to_vec(), None); + let offline_sign = simulate_offline_stage(keys, &[3, 4, 5]); + simulate_signing(offline_sign, b"ZenGo"); + } + + #[test] + fn test_add_party_with_permute() { + fn simulate_replace( + keys: &mut Vec>, + party_indices: &[u16], + old_to_new_map: &HashMap, + t: u16, + n: u16, + ) -> FsDkrResult<()> { + fn generate_join_messages_and_keys( + number_of_new_parties: usize, + ) -> (Vec>, Vec) { + // the new party generates it's join message to start joining the computation + (0..number_of_new_parties).map(|_| JoinMessage::distribute()).unzip() + } + + fn generate_refresh_parties_replace( + keys: &mut [LocalKey], + old_to_new_map: &HashMap, + join_messages: &[JoinMessage], + ) -> (Vec>, Vec) { + let new_n = (&keys.len() + join_messages.len()) as u16; + keys.iter_mut() + .map(|key| { + RefreshMessage::replace(join_messages, key, old_to_new_map, key.t, new_n) + .unwrap() + }) + .unzip() + } + + // each party that wants to join generates a join message and a pair of paillier keys. + let (mut join_messages, new_keys) = + generate_join_messages_and_keys::<{ crate::M_SECURITY }>(party_indices.len()); + + // each new party has to be informed through offchannel communication what party index + // it has been assigned (the information is public). + for (join_message, party_index) in join_messages.iter_mut().zip(party_indices) { + join_message.party_index = Some(*party_index); + } + + // each existing party has to generate it's refresh message aware of the new parties + let (refresh_messages, dk_keys) = + generate_refresh_parties_replace(keys, &old_to_new_map, join_messages.as_slice()); + let mut new_keys_vec: Vec<(u16, LocalKey)> = + Vec::with_capacity(keys.len() + join_messages.len()); + // all existing parties rotate aware of the join_messages + for i in 0..keys.len() as usize { + RefreshMessage::collect( + refresh_messages.as_slice(), + &mut keys[i], + dk_keys[i].clone(), + join_messages.as_slice(), + t, + ) + .expect(""); + new_keys_vec.push((keys[i].i - 1, keys[i].clone())); + } + + // all new parties generate a local key + for (join_message, dk) in join_messages.iter().zip(new_keys) { + let party_index = join_message.party_index.unwrap(); + let local_key = join_message.collect( + refresh_messages.as_slice(), + dk, + join_messages.as_slice(), + t, + n, + t, + )?; + + new_keys_vec.push((party_index - 1, local_key)); + } + + new_keys_vec.sort_by(|a, b| a.0.cmp(&b.0)); + let keys_replacements = + new_keys_vec.iter().map(|a| a.1.clone()).collect::>>(); + *keys = keys_replacements; + Ok(()) + } + + let t = 2; + let n = 7; + + let all_keys = simulate_keygen(t, n); + // Remove the 2nd and 7th party + let mut keys = all_keys.clone(); + keys.remove(6); + keys.remove(1); + + let mut old_to_new_map: HashMap = HashMap::new(); + old_to_new_map.insert(1, 4); + old_to_new_map.insert(3, 1); + old_to_new_map.insert(4, 3); + old_to_new_map.insert(5, 6); + old_to_new_map.insert(6, 5); + + // Simulate the replace + simulate_replace::<{ crate::M_SECURITY }>(&mut keys, &[2, 7], &old_to_new_map, t, n) + .unwrap(); + // check that sum of old keys is equal to sum of new keys + let old_linear_secret_key: Vec<_> = + (0..all_keys.len()).map(|i| all_keys[i].keys_linear.x_i.clone()).collect(); + + let new_linear_secret_key: Vec<_> = + (0..keys.len()).map(|i| keys[i].keys_linear.x_i.clone()).collect(); + let indices: Vec<_> = (0..(t + 1) as u16).collect(); + let vss = VerifiableSS:: { + parameters: ShamirSecretSharing { threshold: t, share_count: n }, + commitments: Vec::new(), + proof: DLogProof::::prove(&Scalar::random()), + }; + assert_eq!( + vss.reconstruct(&indices[..], &old_linear_secret_key[0..(t + 1) as usize]), + vss.reconstruct(&indices[..], &new_linear_secret_key[0..(t + 1) as usize]) + ); + assert_ne!(old_linear_secret_key, new_linear_secret_key); + + let offline_sign = simulate_offline_stage(keys, &[1, 2, 7]); + simulate_signing(offline_sign, b"ZenGo"); + } + + #[test] + fn test_change_threshold_sign_rotate_sign() { + let mut keys = simulate_keygen(2, 5); + let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); + simulate_signing(offline_sign, b"ZenGo"); + // Change threshold to 1 (i.e quorum size = 2). + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, Some(1)); + let offline_sign = simulate_offline_stage(keys.clone(), &[3, 4]); + simulate_signing(offline_sign, b"ZenGo"); + // Change threshold to back to 2 (i.e quorum size = 3). + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, Some(2)); + let offline_sign = simulate_offline_stage(keys, &[1, 3, 5]); + simulate_signing(offline_sign, b"ZenGo"); + } + + fn simulate_keygen(t: u16, n: u16) -> Vec> { + //simulate keygen + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + for i in 1..=n { + simulation.add_party(Keygen::new(i, t, n).unwrap()); + } + + simulation.run().unwrap() + } + + fn simulate_dkr_removal( + keys: &mut Vec>, + remove_party_indices: Vec, + new_t_option: Option, + ) { + let mut broadcast_messages: HashMap>> = + HashMap::new(); + let mut new_dks: HashMap = HashMap::new(); + let mut refresh_messages: Vec> = Vec::new(); + let mut party_key: HashMap> = HashMap::new(); + // TODO: Verify this is correct + let new_n = keys.len() as u16; + let current_t = keys[0].t; + let new_t = new_t_option.unwrap_or(current_t); + for key in keys.iter_mut() { + let (refresh_message, new_dk) = + RefreshMessage::distribute(key.i, key, new_t, new_n).unwrap(); + refresh_messages.push(refresh_message.clone()); + new_dks.insert(refresh_message.party_index.into(), new_dk); + party_key.insert(refresh_message.party_index.into(), key.clone()); + } + + for refresh_message in refresh_messages.iter() { + broadcast_messages.insert(refresh_message.party_index.into(), Vec::new()); + } + + for refresh_message in refresh_messages.iter_mut() { + if !remove_party_indices.contains(&refresh_message.party_index.into()) { + refresh_message.remove_party_indices = remove_party_indices.clone(); + } else { + let mut new_remove_party_indices = remove_party_indices.clone(); + new_remove_party_indices + .retain(|value| *value != refresh_message.party_index.into()); + refresh_message.remove_party_indices = new_remove_party_indices; + } + + for (party_index, refresh_bucket) in broadcast_messages.iter_mut() { + if refresh_message.remove_party_indices.contains(&(*party_index as u16)) { + continue + } + refresh_bucket.push(refresh_message.clone()); + } + } + + for remove_party_index in remove_party_indices.iter() { + assert_eq!(broadcast_messages[&(*remove_party_index as usize)].len(), 1); + } + + // keys will be updated to refreshed values + for (party, key) in party_key.iter_mut() { + if remove_party_indices.contains(&(*party as u16)) { + continue + } + + RefreshMessage::collect( + broadcast_messages[party].clone().as_slice(), + key, + new_dks[party].clone(), + &[], + current_t, + ) + .expect(""); + } + + for remove_party_index in remove_party_indices { + let result = RefreshMessage::collect( + &broadcast_messages[&(remove_party_index as usize)], + &mut keys[remove_party_index as usize], + new_dks[&(remove_party_index as usize)].clone(), + &[], + current_t, + ); + assert!(result.is_err()); + } + } + + fn simulate_dkr( + keys: &mut Vec>, + new_t_option: Option, + ) -> (Vec>, Vec) { + let mut broadcast_vec: Vec> = Vec::new(); + let mut new_dks: Vec = Vec::new(); + let keys_len = keys.len(); + let current_t = keys[0].t; + let new_t = new_t_option.unwrap_or(current_t); + for key in keys.iter_mut() { + let (refresh_message, new_dk) = + RefreshMessage::distribute(key.i, key, new_t, keys_len as u16).unwrap(); + broadcast_vec.push(refresh_message); + new_dks.push(new_dk); + } + + // keys will be updated to refreshed values + for i in 0..keys.len() as usize { + RefreshMessage::collect( + &broadcast_vec, + &mut keys[i], + new_dks[i].clone(), + &[], + current_t, + ) + .expect(""); + } + + (broadcast_vec, new_dks) + } + + fn simulate_offline_stage( + local_keys: Vec>, + s_l: &[u16], + ) -> Vec { + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + for (i, &keygen_i) in (1..).zip(s_l) { + simulation.add_party( + OfflineStage::new(i, s_l.to_vec(), local_keys[usize::from(keygen_i - 1)].clone()) + .unwrap(), + ); + } + + simulation.run().unwrap() + } + + fn simulate_signing(offline: Vec, message: &[u8]) { + let message = create_hash(&[&BigInt::from_bytes(message)]); + let pk = &offline[0].public_key(); + + let parties = offline + .iter() + .map(|o| SignManual::new(message.clone(), o.clone())) + .collect::, _>>() + .unwrap(); + let (parties, local_sigs): (Vec<_>, Vec<_>) = parties.into_iter().unzip(); + // parties.remove(0).complete(&local_sigs[1..]).unwrap(); + let local_sigs_except = |i: usize| { + let mut v = vec![]; + v.extend_from_slice(&local_sigs[..i]); + if i + 1 < local_sigs.len() { + v.extend_from_slice(&local_sigs[i + 1..]); + } + v + }; + + assert!(parties + .into_iter() + .enumerate() + .map(|(i, p)| p.complete(&local_sigs_except(i)).unwrap()) + .all(|signature| verify(&signature, &pk, &message).is_ok())); + } + + fn create_hash(big_ints: &[&BigInt]) -> BigInt { + let hasher = Sha256::new(); + + for value in big_ints { + hasher.clone().chain(&BigInt::to_bytes(value)); + } + + let result_hex = hasher.finalize(); + BigInt::from_bytes(&result_hex[..]) + } } diff --git a/fs-dkr/src/zk_pdl_with_slack.rs b/fs-dkr/src/zk_pdl_with_slack.rs index e17017b2..9ee1b562 100644 --- a/fs-dkr/src/zk_pdl_with_slack.rs +++ b/fs-dkr/src/zk_pdl_with_slack.rs @@ -10,323 +10,308 @@ use std::marker::PhantomData; use crate::error::{FsDkrError, FsDkrResult}; -use curv::arithmetic::traits::*; -use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; -use curv::elliptic::curves::Curve; -use curv::elliptic::curves::Point; -use curv::elliptic::curves::Scalar; -use curv::elliptic::curves::Secp256k1; -use curv::BigInt; +use curv::{ + arithmetic::traits::*, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{Curve, Point, Scalar, Secp256k1}, + BigInt, +}; use paillier::EncryptionKey; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PDLwSlackStatement { - pub ciphertext: BigInt, - pub ek: EncryptionKey, - pub Q: Point, - pub G: Point, - pub h1: BigInt, - pub h2: BigInt, - pub N_tilde: BigInt, + pub ciphertext: BigInt, + pub ek: EncryptionKey, + pub Q: Point, + pub G: Point, + pub h1: BigInt, + pub h2: BigInt, + pub N_tilde: BigInt, } #[derive(Clone)] pub struct PDLwSlackWitness { - pub x: Scalar, - pub r: BigInt, + pub x: Scalar, + pub r: BigInt, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(bound = "E: Curve, H: Digest + Clone")] pub struct PDLwSlackProof { - z: BigInt, - u1: Point, - u2: BigInt, - u3: BigInt, - s1: BigInt, - s2: BigInt, - s3: BigInt, - _phantom: PhantomData, + z: BigInt, + u1: Point, + u2: BigInt, + u3: BigInt, + s1: BigInt, + s2: BigInt, + s3: BigInt, + _phantom: PhantomData, } impl PDLwSlackProof { - pub fn prove(witness: &PDLwSlackWitness, statement: &PDLwSlackStatement) -> Self { - let q3 = Scalar::::group_order().pow(3); - let q_N_tilde = Scalar::::group_order() * &statement.N_tilde; - let q3_N_tilde = &q3 * &statement.N_tilde; - - let alpha = BigInt::sample_below(&q3); - let one = BigInt::one(); - let beta = BigInt::sample_range(&one, &(&statement.ek.n - &one)); - let rho = BigInt::sample_below(&q_N_tilde); - let gamma = BigInt::sample_below(&q3_N_tilde); - - let z = commitment_unknown_order( - &statement.h1, - &statement.h2, - &statement.N_tilde, - &witness.x.to_bigint(), - &rho, - ); - let u1 = statement.G.clone() * Scalar::::from(&alpha); - let u2 = commitment_unknown_order( - &(&statement.ek.n + BigInt::one()), - &beta, - &statement.ek.nn, - &alpha, - &statement.ek.n, - ); - let u3 = commitment_unknown_order( - &statement.h1, - &statement.h2, - &statement.N_tilde, - &alpha, - &gamma, - ); - - let e = H::new() - .chain_bigint(&BigInt::from_bytes(&statement.G.to_bytes(true))) - .chain_bigint(&BigInt::from_bytes(&statement.Q.to_bytes(true))) - .chain_bigint(&statement.ciphertext) - .chain_bigint(&z) - .chain_bigint(&BigInt::from_bytes(&u1.to_bytes(true))) - .chain_bigint(&u2) - .chain_bigint(&u3) - .result_bigint(); - - let s1 = &e * witness.x.to_bigint() + alpha; - let s2 = commitment_unknown_order(&witness.r, &beta, &statement.ek.n, &e, &BigInt::one()); - let s3 = &e * rho + gamma; - - PDLwSlackProof { - z, - u1, - u2, - u3, - s1, - s2, - s3, - _phantom: PhantomData, - } - } - - pub fn verify(&self, statement: &PDLwSlackStatement) -> FsDkrResult<()> { - let e = H::new() - .chain_bigint(&BigInt::from_bytes(&statement.G.to_bytes(true))) - .chain_bigint(&BigInt::from_bytes(&statement.Q.to_bytes(true))) - .chain_bigint(&statement.ciphertext) - .chain_bigint(&self.z) - .chain_bigint(&BigInt::from_bytes(&self.u1.to_bytes(true))) - .chain_bigint(&self.u2) - .chain_bigint(&self.u3) - .result_bigint(); - - let g_s1 = statement.G.clone() * Scalar::::from(&self.s1); - let e_fe_neg = Scalar::::from(&(Scalar::::group_order() - &e)); - let y_minus_e = statement.Q.clone() * e_fe_neg; - let u1_test = g_s1 + y_minus_e; - - let u2_test_tmp = commitment_unknown_order( - &(&statement.ek.n + BigInt::one()), - &self.s2, - &statement.ek.nn, - &self.s1, - &statement.ek.n, - ); - let u2_test = commitment_unknown_order( - &u2_test_tmp, - &statement.ciphertext, - &statement.ek.nn, - &BigInt::one(), - &(-&e), - ); - - let u3_test_tmp = commitment_unknown_order( - &statement.h1, - &statement.h2, - &statement.N_tilde, - &self.s1, - &self.s3, - ); - let u3_test = commitment_unknown_order( - &u3_test_tmp, - &self.z, - &statement.N_tilde, - &BigInt::one(), - &(-&e), - ); - if &self.u1 == &u1_test && &self.u2 == &u2_test && &self.u3 == &u3_test { - Ok(()) - } else { - Err(FsDkrError::PDLwSlackProof { - is_u1_eq: self.u1 == u1_test, - is_u2_eq: self.u2 == u2_test, - is_u3_eq: self.u3 == u3_test, - }) - } - } + pub fn prove(witness: &PDLwSlackWitness, statement: &PDLwSlackStatement) -> Self { + let q3 = Scalar::::group_order().pow(3); + let q_N_tilde = Scalar::::group_order() * &statement.N_tilde; + let q3_N_tilde = &q3 * &statement.N_tilde; + + let alpha = BigInt::sample_below(&q3); + let one = BigInt::one(); + let beta = BigInt::sample_range(&one, &(&statement.ek.n - &one)); + let rho = BigInt::sample_below(&q_N_tilde); + let gamma = BigInt::sample_below(&q3_N_tilde); + + let z = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &witness.x.to_bigint(), + &rho, + ); + let u1 = statement.G.clone() * Scalar::::from(&alpha); + let u2 = commitment_unknown_order( + &(&statement.ek.n + BigInt::one()), + &beta, + &statement.ek.nn, + &alpha, + &statement.ek.n, + ); + let u3 = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &alpha, + &gamma, + ); + + let e = H::new() + .chain_bigint(&BigInt::from_bytes(&statement.G.to_bytes(true))) + .chain_bigint(&BigInt::from_bytes(&statement.Q.to_bytes(true))) + .chain_bigint(&statement.ciphertext) + .chain_bigint(&z) + .chain_bigint(&BigInt::from_bytes(&u1.to_bytes(true))) + .chain_bigint(&u2) + .chain_bigint(&u3) + .result_bigint(); + + let s1 = &e * witness.x.to_bigint() + alpha; + let s2 = commitment_unknown_order(&witness.r, &beta, &statement.ek.n, &e, &BigInt::one()); + let s3 = &e * rho + gamma; + + PDLwSlackProof { z, u1, u2, u3, s1, s2, s3, _phantom: PhantomData } + } + + pub fn verify(&self, statement: &PDLwSlackStatement) -> FsDkrResult<()> { + let e = H::new() + .chain_bigint(&BigInt::from_bytes(&statement.G.to_bytes(true))) + .chain_bigint(&BigInt::from_bytes(&statement.Q.to_bytes(true))) + .chain_bigint(&statement.ciphertext) + .chain_bigint(&self.z) + .chain_bigint(&BigInt::from_bytes(&self.u1.to_bytes(true))) + .chain_bigint(&self.u2) + .chain_bigint(&self.u3) + .result_bigint(); + + let g_s1 = statement.G.clone() * Scalar::::from(&self.s1); + let e_fe_neg = Scalar::::from(&(Scalar::::group_order() - &e)); + let y_minus_e = statement.Q.clone() * e_fe_neg; + let u1_test = g_s1 + y_minus_e; + + let u2_test_tmp = commitment_unknown_order( + &(&statement.ek.n + BigInt::one()), + &self.s2, + &statement.ek.nn, + &self.s1, + &statement.ek.n, + ); + let u2_test = commitment_unknown_order( + &u2_test_tmp, + &statement.ciphertext, + &statement.ek.nn, + &BigInt::one(), + &(-&e), + ); + + let u3_test_tmp = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &self.s1, + &self.s3, + ); + let u3_test = commitment_unknown_order( + &u3_test_tmp, + &self.z, + &statement.N_tilde, + &BigInt::one(), + &(-&e), + ); + if &self.u1 == &u1_test && &self.u2 == &u2_test && &self.u3 == &u3_test { + Ok(()) + } else { + Err(FsDkrError::PDLwSlackProof { + is_u1_eq: self.u1 == u1_test, + is_u2_eq: self.u2 == u2_test, + is_u3_eq: self.u3 == u3_test, + }) + } + } } pub fn commitment_unknown_order( - h1: &BigInt, - h2: &BigInt, - N_tilde: &BigInt, - x: &BigInt, - r: &BigInt, + h1: &BigInt, + h2: &BigInt, + N_tilde: &BigInt, + x: &BigInt, + r: &BigInt, ) -> BigInt { - let h1_x = BigInt::mod_pow(h1, &x, &N_tilde); - let h2_r = { - if r < &BigInt::zero() { - let h2_inv = BigInt::mod_inv(h2, &N_tilde).unwrap(); - BigInt::mod_pow(&h2_inv, &(-r), &N_tilde) - } else { - BigInt::mod_pow(h2, &r, &N_tilde) - } - }; - let com = BigInt::mod_mul(&h1_x, &h2_r, &N_tilde); - com + let h1_x = BigInt::mod_pow(h1, &x, &N_tilde); + let h2_r = { + if r < &BigInt::zero() { + let h2_inv = BigInt::mod_inv(h2, &N_tilde).unwrap(); + BigInt::mod_pow(&h2_inv, &(-r), &N_tilde) + } else { + BigInt::mod_pow(h2, &r, &N_tilde) + } + }; + let com = BigInt::mod_mul(&h1_x, &h2_r, &N_tilde); + com } #[cfg(test)] mod test { - use super::*; - use curv::elliptic::curves::secp256_k1::{Secp256k1Point, Secp256k1Scalar}; - use curv::BigInt; - use paillier::core::Randomness; - use paillier::traits::{EncryptWithChosenRandomness, KeyGeneration}; - use paillier::Paillier; - use paillier::RawPlaintext; - use sha2::Sha256; - use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; - - type GE = Secp256k1Point; - type FE = Secp256k1Scalar; - - #[test] - fn test_zk_pdl_with_slack() { - // N_tilde, h1, h2 generation - let (ek_tilde, dk_tilde) = - Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - // note: safe primes should be used: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&phi); - let S = BigInt::from(2).pow(256 as u32); - let xhi = BigInt::sample_below(&S); - let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); - let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = DLogStatement { - N: ek_tilde.n.clone(), - g: h1.clone(), - ni: h2.clone(), - }; - - let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); - - // generate the scalar secret and Paillier encrypt it - let (ek, _dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - // note: safe primes should be used here as well: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let randomness = Randomness::sample(&ek); - let x: Scalar = Scalar::::random(); - - let Q = Point::::generator().to_point() * &x; - - let c = Paillier::encrypt_with_chosen_randomness( - &ek, - RawPlaintext::from(x.to_bigint().clone()), - &randomness, - ) - .0 - .into_owned(); - - // Generate PDL with slack statement, witness and proof - let pdl_w_slack_statement = PDLwSlackStatement { - ciphertext: c, - ek, - Q, - G: Point::::generator().to_point(), - h1, - h2, - N_tilde: ek_tilde.n, - }; - - let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; - - let proof = PDLwSlackProof::::prove( - &pdl_w_slack_witness, - &pdl_w_slack_statement, - ); - // verify h1,h2, N_tilde - let setup_result = composite_dlog_proof.verify(&statement); - assert!(setup_result.is_ok()); - let result = proof.verify(&pdl_w_slack_statement); - assert!(result.is_ok()); - } - - #[test] - #[should_panic] - fn test_zk_pdl_with_slack_soundness() { - // N_tilde, h1, h2 generation - let (ek_tilde, dk_tilde) = - Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - // note: safe primes should be used: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&phi); - let S = BigInt::from(2).pow(256 as u32); - let xhi = BigInt::sample_below(&S); - let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); - let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = DLogStatement { - N: ek_tilde.n.clone(), - g: h1.clone(), - ni: h2.clone(), - }; - - let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); - - // generate the scalar secret and Paillier encrypt it - let (ek, _dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - // note: safe primes should be used here as well: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let randomness = Randomness::sample(&ek); - let x: Scalar = Scalar::::random(); - - let Q = Point::::generator().to_point() * &x; - - // here we encrypt x + 1 instead of x: - let c = Paillier::encrypt_with_chosen_randomness( - &ek, - RawPlaintext::from(x.to_bigint().clone() + BigInt::one()), - &randomness, - ) - .0 - .into_owned(); - - // Generate PDL with slack statement, witness and proof - let pdl_w_slack_statement = PDLwSlackStatement { - ciphertext: c, - ek, - Q, - G: Point::::generator().to_point(), - h1, - h2, - N_tilde: ek_tilde.n, - }; - - let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; - - let proof = PDLwSlackProof::::prove( - &pdl_w_slack_witness, - &pdl_w_slack_statement, - ); - // verify h1,h2, N_tilde - let setup_result = composite_dlog_proof.verify(&statement); - assert!(setup_result.is_ok()); - let result = proof.verify(&pdl_w_slack_statement); - assert!(result.is_ok()); - } + use super::*; + use curv::{ + elliptic::curves::secp256_k1::{Secp256k1Point, Secp256k1Scalar}, + BigInt, + }; + use paillier::{ + core::Randomness, + traits::{EncryptWithChosenRandomness, KeyGeneration}, + Paillier, RawPlaintext, + }; + use sha2::Sha256; + use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; + + type GE = Secp256k1Point; + type FE = Secp256k1Scalar; + + #[test] + fn test_zk_pdl_with_slack() { + // N_tilde, h1, h2 generation + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + // note: safe primes should be used: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&phi); + let S = BigInt::from(2).pow(256 as u32); + let xhi = BigInt::sample_below(&S); + let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); + let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); + let statement = DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; + + let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); + + // generate the scalar secret and Paillier encrypt it + let (ek, _dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + // note: safe primes should be used here as well: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let randomness = Randomness::sample(&ek); + let x: Scalar = Scalar::::random(); + + let Q = Point::::generator().to_point() * &x; + + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint().clone()), + &randomness, + ) + .0 + .into_owned(); + + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: c, + ek, + Q, + G: Point::::generator().to_point(), + h1, + h2, + N_tilde: ek_tilde.n, + }; + + let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; + + let proof = PDLwSlackProof::::prove( + &pdl_w_slack_witness, + &pdl_w_slack_statement, + ); + // verify h1,h2, N_tilde + let setup_result = composite_dlog_proof.verify(&statement); + assert!(setup_result.is_ok()); + let result = proof.verify(&pdl_w_slack_statement); + assert!(result.is_ok()); + } + + #[test] + #[should_panic] + fn test_zk_pdl_with_slack_soundness() { + // N_tilde, h1, h2 generation + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + // note: safe primes should be used: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&phi); + let S = BigInt::from(2).pow(256 as u32); + let xhi = BigInt::sample_below(&S); + let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); + let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); + let statement = DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; + + let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); + + // generate the scalar secret and Paillier encrypt it + let (ek, _dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + // note: safe primes should be used here as well: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let randomness = Randomness::sample(&ek); + let x: Scalar = Scalar::::random(); + + let Q = Point::::generator().to_point() * &x; + + // here we encrypt x + 1 instead of x: + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint().clone() + BigInt::one()), + &randomness, + ) + .0 + .into_owned(); + + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: c, + ek, + Q, + G: Point::::generator().to_point(), + h1, + h2, + N_tilde: ek_tilde.n, + }; + + let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; + + let proof = PDLwSlackProof::::prove( + &pdl_w_slack_witness, + &pdl_w_slack_statement, + ); + // verify h1,h2, N_tilde + let setup_result = composite_dlog_proof.verify(&statement); + assert!(setup_result.is_ok()); + let result = proof.verify(&pdl_w_slack_statement); + assert!(result.is_ok()); + } } From d427cc460922161d7e1a069e30ef8061e5e5bbc6 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 09:40:22 +0800 Subject: [PATCH 07/41] Add rust-toolchain file for CI. --- rust-toolchain | 1 + 1 file changed, 1 insertion(+) create mode 100644 rust-toolchain diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 00000000..e205afa2 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +nightly-2023-10-01 From f365c89a34b129e4528ad02b39f061333c0c960b Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 09:42:56 +0800 Subject: [PATCH 08/41] Update for specific nightly version. --- .github/workflows/checks.yml | 4 ++-- .github/workflows/tests.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 98c6e1e4..51d9d76a 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -48,10 +48,10 @@ jobs: access_token: ${{ github.token }} - uses: actions/checkout@v2 - - name: Install latest nightly + - name: Install nightly toolchain uses: actions-rs/toolchain@v1 with: - toolchain: nightly + toolchain: nightly-2023-10-01 components: rustfmt, clippy - name: Rust Cache diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3ba39d40..2c6413a2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,7 @@ jobs: - name: Install Toolchain uses: actions-rs/toolchain@v1 with: - #toolchain: nightly-2022-05-15 + toolchain: nightly-2023-10-01 components: rustfmt, clippy target: wasm32-unknown-unknown override: true From 43b5c781a6a4c0cf19c39f2feffe708cbfcae75a Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 09:48:04 +0800 Subject: [PATCH 09/41] Fixing dead links. --- .github/CONTRIBUTING.md | 57 ------------------------------------ .github/workflows/checks.yml | 2 +- multi-party-ecdsa/README.md | 1 - 3 files changed, 1 insertion(+), 59 deletions(-) delete mode 100644 .github/CONTRIBUTING.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 256b5956..00000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,57 +0,0 @@ - - -# Contributing to Webb Protocol Projects - -A big welcome and thank you for considering contributing to Webb Protocol open source projects! It’s people like you that make it a reality for users in our community. - -Reading and following these guidelines will help us make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing these open source projects. In return, we will reciprocate that respect by addressing your issue, assessing changes, and helping you finalize your pull requests. - -## Quicklinks - -- [Contributing to Webb Protocol Projects](#contributing-to-webb-protocol-projects) - - [Quicklinks](#quicklinks) - - [Getting Started](#getting-started) - - [Issues](#issues) - - [Pull Requests](#pull-requests) - - [Getting Help](#getting-help) - - -## Getting Started - -Contributions are made to this repo via Issues and Pull Requests (PRs). A few general guidelines that cover both: - -- To report security vulnerabilities, please direct message
code maintainers which will promptly be answered. -- Search for existing Issues and PRs before creating your own. -- We work hard to makes sure issues are handled in a timely manner but, depending on the impact, it could take a while to investigate the root cause. A friendly ping in the comment thread to the submitter or a contributor can help draw attention if your issue is blocking. - -### Issues - -Issues should be used to report problems with the implementation, request a new feature, or to discuss potential changes before a PR is created. When you create a new Issue, a template will be loaded that will guide you through collecting and providing the information we need to investigate. - -If you find an Issue that addresses the problem you're having, please add your own reproduction information to the existing issue rather than creating a new one. Adding a [reaction](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) can also help be indicating to our maintainers that a particular problem is affecting more than just the reporter. - -### Pull Requests - -PRs to our libraries are always welcome and can be a quick way to get your fix or improvement slated for the next release. In general, PRs should: - -- Only fix/add the functionality in question **OR** address wide-spread whitespace/style issues, not both. -- Add unit or integration tests for fixed or changed functionality (if a test suite already exists). -- Address a single concern in the least number of changed lines as possible. -- Include documentation in the repo -- Be accompanied by a complete Pull Request template (loaded automatically when a PR is created). - -For changes that address core functionality or would require breaking changes (e.g. a major release), it's best to open an Issue to discuss your proposal first. This is not required but can save time creating and reviewing changes. - -In general, we follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr) - -1. Fork the repository to your own Github account -2. Clone the project to your machine -3. Create a branch locally with a succinct but descriptive name -4. Commit changes to the branch -5. Following any formatting and testing guidelines specific to this repo -6. Push changes to your fork -7. Open a PR in our repository and follow the PR template so that we can efficiently review the changes. - -## Getting Help - -Join us in the [Webb Community](https://discord.gg/cv8EfJu3Tn) and post your question there. \ No newline at end of file diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 51d9d76a..7d9b32c4 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -59,5 +59,5 @@ jobs: - name: Run Linters run: | - cargo +nightly fmt --all -- --check + cargo fmt --all -- --check cargo clippy -Zunstable-options -- -D warnings diff --git a/multi-party-ecdsa/README.md b/multi-party-ecdsa/README.md index 6b4fa464..d04312e8 100644 --- a/multi-party-ecdsa/README.md +++ b/multi-party-ecdsa/README.md @@ -1,6 +1,5 @@ # Multi-party ECDSA -[![Build Status](https://travis-ci.com/ZenGo-X/multi-party-ecdsa.svg?branch=master)](https://travis-ci.com/zengo-x/multi-party-ecdsa) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) This project is a Rust implementation of {t,n}-threshold ECDSA (elliptic curve digital signature algorithm). From f23cfbe2dbfe5f9bc282009a7ca824f77f317d1d Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 09:55:13 +0800 Subject: [PATCH 10/41] Fix clippy warnings. --- .github/CONTRIBUTING.md | 57 ++++++++++++++++++ multi-party-ecdsa/CONTRIBUTING.md | 59 ------------------- .../multi_party_ecdsa/gg_2020/blame.rs | 2 +- .../multi_party_ecdsa/gg_2020/party_i.rs | 2 +- .../gg_2020/state_machine/keygen/rounds.rs | 2 +- .../gg_2020/state_machine/sign/rounds.rs | 4 +- .../two_party_ecdsa/lindell_2017/party_one.rs | 4 +- rust-toolchain | 1 - 8 files changed, 64 insertions(+), 67 deletions(-) create mode 100644 .github/CONTRIBUTING.md delete mode 100644 multi-party-ecdsa/CONTRIBUTING.md delete mode 100644 rust-toolchain diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..256b5956 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,57 @@ + + +# Contributing to Webb Protocol Projects + +A big welcome and thank you for considering contributing to Webb Protocol open source projects! It’s people like you that make it a reality for users in our community. + +Reading and following these guidelines will help us make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing these open source projects. In return, we will reciprocate that respect by addressing your issue, assessing changes, and helping you finalize your pull requests. + +## Quicklinks + +- [Contributing to Webb Protocol Projects](#contributing-to-webb-protocol-projects) + - [Quicklinks](#quicklinks) + - [Getting Started](#getting-started) + - [Issues](#issues) + - [Pull Requests](#pull-requests) + - [Getting Help](#getting-help) + + +## Getting Started + +Contributions are made to this repo via Issues and Pull Requests (PRs). A few general guidelines that cover both: + +- To report security vulnerabilities, please direct message code maintainers which will promptly be answered. +- Search for existing Issues and PRs before creating your own. +- We work hard to makes sure issues are handled in a timely manner but, depending on the impact, it could take a while to investigate the root cause. A friendly ping in the comment thread to the submitter or a contributor can help draw attention if your issue is blocking. + +### Issues + +Issues should be used to report problems with the implementation, request a new feature, or to discuss potential changes before a PR is created. When you create a new Issue, a template will be loaded that will guide you through collecting and providing the information we need to investigate. + +If you find an Issue that addresses the problem you're having, please add your own reproduction information to the existing issue rather than creating a new one. Adding a [reaction](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) can also help be indicating to our maintainers that a particular problem is affecting more than just the reporter. + +### Pull Requests + +PRs to our libraries are always welcome and can be a quick way to get your fix or improvement slated for the next release. In general, PRs should: + +- Only fix/add the functionality in question **OR** address wide-spread whitespace/style issues, not both. +- Add unit or integration tests for fixed or changed functionality (if a test suite already exists). +- Address a single concern in the least number of changed lines as possible. +- Include documentation in the repo +- Be accompanied by a complete Pull Request template (loaded automatically when a PR is created). + +For changes that address core functionality or would require breaking changes (e.g. a major release), it's best to open an Issue to discuss your proposal first. This is not required but can save time creating and reviewing changes. + +In general, we follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr) + +1. Fork the repository to your own Github account +2. Clone the project to your machine +3. Create a branch locally with a succinct but descriptive name +4. Commit changes to the branch +5. Following any formatting and testing guidelines specific to this repo +6. Push changes to your fork +7. Open a PR in our repository and follow the PR template so that we can efficiently review the changes. + +## Getting Help + +Join us in the [Webb Community](https://discord.gg/cv8EfJu3Tn) and post your question there. \ No newline at end of file diff --git a/multi-party-ecdsa/CONTRIBUTING.md b/multi-party-ecdsa/CONTRIBUTING.md deleted file mode 100644 index 05a89004..00000000 --- a/multi-party-ecdsa/CONTRIBUTING.md +++ /dev/null @@ -1,59 +0,0 @@ -Contributing to the Multi-party ECDSA project -===================================== - -Pull requests are always welcome, and the KZen dev team appreciates any help the community can -give to help make Multi-party ECDSA project better. - -Contributor Agreement (CA) ----------------- - -Any contributor must sign the Contributor Agreement (CA). - -### How to sign the Contributor Agreement (CA)? - -Please send an email to [github@kzencorp.com](mailto:github@kzencorp.com) containing your github username, the CA will be send to you by email. -After signature you will be added to the team as a contributor. - -Communication Channels ----------------- - -* Most communication about KZen cryptography happens on Telegram, feel free to send us an email with your contact details. - -* Discussion about code base improvements happens in GitHub issues and on pull requests. - -Contributor Workflow ----------------- - -The codebase is maintained using the "contributor workflow" where everyone contributes patch proposals using "pull requests". This facilitates social contribution, easy testing and peer review. - -To contribute a patch, the workflow is as follows: - -* Fork repository -* Create topic branch -* Commit patches -* Push changes to your fork -* Create pull request - -Make sure to provide a clear description in your Pull Request (PR). - -### Header - -Make sure to include the following header (by configuring your IDE) in all files: - -```rust -/* - Multi-party ECDSA - - Copyright 2018 by Kzen Networks - - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) - - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - - @license GPL-3.0+ -*/ -``` diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs index f468b616..4ed04443 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/blame.rs @@ -379,7 +379,7 @@ impl GlobalStatePhase6 { .map(|i| { let g_wi_ki = &self.g_w_vec[i] * &self.k_vec[i]; let sum = self.miu_vec[i].iter().fold(g_wi_ki, |acc, x| { - acc + (Point::generator() * &Scalar::::from(&*x)) + acc + (Point::generator() * &Scalar::::from(x)) }); sum }) diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs index 873ab4b6..cf64924b 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs @@ -423,7 +423,7 @@ impl Keys { let mut global_coefficients = head[0].commitments.clone(); for vss in tail { for (i, coefficient_commitment) in vss.commitments.iter().enumerate() { - global_coefficients[i] = &global_coefficients[i] + &*coefficient_commitment; + global_coefficients[i] = &global_coefficients[i] + coefficient_commitment; } } diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs index 5529e60d..5c23a867 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs @@ -201,7 +201,7 @@ impl Round3 { let v = BigInt::from_bytes(&encrypted_share); let c = RawCiphertext::from(v); let raw_share: RawPlaintext<'_> = Paillier::decrypt(&self.keys.dk, c); - let share = Scalar::from_bigint(&raw_share.0.into_owned()); + let share = Scalar::from_bigint(&raw_share.0); let _ = decrypted_input.push_msg(Msg { sender: i, receiver: Some(self.party_i), diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs index 3d681b7f..79cf80b3 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/sign/rounds.rs @@ -452,7 +452,7 @@ impl Round4 { &self.bc_vec, usize::from(self.i - 1), ) - .map_err(|e| Error::Round5(e))?; + .map_err(Error::Round5)?; let R_dash = &R * &self.sign_keys.k_i; @@ -559,7 +559,7 @@ impl Round5 { &l_s, i, ) - .map_err(|e| Error::Round5(e))?; + .map_err(Error::Round5)?; } LocalSignature::phase5_check_R_dash_sum(&r_dash_vec).map_err(|e| { Error::Round5(ErrorType { diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs index 072d32f1..ed8a51ad 100644 --- a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs +++ b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs @@ -257,7 +257,7 @@ impl Party1Private { ) { let (ek_new, dk_new) = Paillier::keypair().keys(); let randomness = Randomness::sample(&ek_new); - let factor_fe = Scalar::::from(&*factor); + let factor_fe = Scalar::::from(factor); let x1_new = &party_one_private.x1 * factor_fe; let c_key_new = Paillier::encrypt_with_chosen_randomness( &ek_new, @@ -576,7 +576,7 @@ pub fn verify( let e_fe: Scalar = Scalar::::from(&message.mod_floor(Scalar::::group_order())); let u1 = Point::generator() * e_fe * &s_inv_fe; - let u2 = &*pubkey * rx_fe * &s_inv_fe; + let u2 = pubkey * rx_fe * &s_inv_fe; // second condition is against malleability let rx_bytes = &BigInt::to_bytes(&signature.r)[..]; diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index e205afa2..00000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -nightly-2023-10-01 From 99ee8589ac74af8ae5b1c7d338967a4ce3f42de4 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 10:01:13 +0800 Subject: [PATCH 11/41] Fixing clippy warnings in fs-dkr. --- fs-dkr/src/range_proofs.rs | 8 ++++---- fs-dkr/src/refresh_message.rs | 10 +++++----- fs-dkr/src/zk_pdl_with_slack.rs | 13 ++++++------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/fs-dkr/src/range_proofs.rs b/fs-dkr/src/range_proofs.rs index e2aea4b4..b8d5e25b 100644 --- a/fs-dkr/src/range_proofs.rs +++ b/fs-dkr/src/range_proofs.rs @@ -54,7 +54,7 @@ impl AliceZkpRound1 { let beta = BigInt::from_paillier_key(alice_ek); let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); let ro = BigInt::sample_below(&(q * N_tilde)); - let z = (BigInt::mod_pow(h1, &a, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; + let z = (BigInt::mod_pow(h1, a, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; let u = ((alpha.borrow() * &alice_ek.n + 1) * BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % &alice_ek.nn; @@ -129,7 +129,7 @@ impl AliceProof { z_e_inv) % N_tilde; let gs1 = (self.s1.borrow() * N + 1) % NN; - let cipher_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&cipher, &self.e, NN), NN); + let cipher_e_inv = BigInt::mod_inv(&BigInt::mod_pow(cipher, &self.e, NN), NN); let cipher_e_inv = match cipher_e_inv { None => return false, Some(c) => c, @@ -138,7 +138,7 @@ impl AliceProof { let u = (gs1 * BigInt::mod_pow(&self.s, N, NN) * cipher_e_inv) % NN; let e = H::new() - .chain_bigint(&N) + .chain_bigint(N) .chain_bigint(&Gen) .chain_bigint(cipher) .chain_bigint(&self.z) @@ -167,7 +167,7 @@ impl AliceProof { q.bit_length() <= 256, "We use SHA256 so we don't currently support moduli bigger than 256" ); - let round1 = AliceZkpRound1::from(alice_ek, dlog_statement, a, &q); + let round1 = AliceZkpRound1::from(alice_ek, dlog_statement, a, q); let Gen = alice_ek.n.borrow() + 1; let e = H::new() diff --git a/fs-dkr/src/refresh_message.rs b/fs-dkr/src/refresh_message.rs index 21508d74..f120c2c0 100644 --- a/fs-dkr/src/refresh_message.rs +++ b/fs-dkr/src/refresh_message.rs @@ -68,7 +68,7 @@ impl RefreshMessage { // commit to points on the polynomial let points_committed_vec: Vec<_> = (0..secret_shares.len()) - .map(|i| Point::::generator() * &secret_shares[i].clone().into()) + .map(|i| Point::::generator() * &secret_shares[i].clone()) .collect(); // encrypt points on the polynomial using Paillier keys @@ -260,13 +260,13 @@ impl RefreshMessage { let h1_h2_n_tilde = key.h1_h2_n_tilde_vec.get((old_party_index - 1) as usize).unwrap().clone(); paillier_key_h1_h2_n_tilde_hash_map.insert( - old_to_new_map.get(old_party_index).unwrap().clone(), + *old_to_new_map.get(old_party_index).unwrap(), (paillier_key, h1_h2_n_tilde), ); } for new_party_index in paillier_key_h1_h2_n_tilde_hash_map.keys() { - if new_party_index.clone() <= current_len { + if *new_party_index <= current_len { key.paillier_key_vec[(new_party_index - 1) as usize] = paillier_key_h1_h2_n_tilde_hash_map.get(new_party_index).unwrap().clone().0; key.h1_h2_n_tilde_vec[(new_party_index - 1) as usize] = @@ -305,7 +305,7 @@ impl RefreshMessage { pub fn collect( refresh_messages: &[Self], - mut local_key: &mut LocalKey, + local_key: &mut LocalKey, new_dk: DecryptionKey, join_messages: &[JoinMessage], current_t: u16, @@ -433,7 +433,7 @@ impl RefreshMessage { // update local key list of local public keys (X_i = g^x_i is updated by adding all // committed points to that party) - for i in 0..refresh_messages.len() + join_messages.len() as usize { + for i in 0..refresh_messages.len() + join_messages.len() { local_key .pk_vec .insert(i, refresh_messages[0].points_committed_vec[i].clone() * li_vec[0].clone()); diff --git a/fs-dkr/src/zk_pdl_with_slack.rs b/fs-dkr/src/zk_pdl_with_slack.rs index 9ee1b562..63b7f158 100644 --- a/fs-dkr/src/zk_pdl_with_slack.rs +++ b/fs-dkr/src/zk_pdl_with_slack.rs @@ -145,7 +145,7 @@ impl PDLwSlackProof { &BigInt::one(), &(-&e), ); - if &self.u1 == &u1_test && &self.u2 == &u2_test && &self.u3 == &u3_test { + if self.u1 == u1_test && self.u2 == u2_test && self.u3 == u3_test { Ok(()) } else { Err(FsDkrError::PDLwSlackProof { @@ -164,17 +164,16 @@ pub fn commitment_unknown_order( x: &BigInt, r: &BigInt, ) -> BigInt { - let h1_x = BigInt::mod_pow(h1, &x, &N_tilde); + let h1_x = BigInt::mod_pow(h1, x, N_tilde); let h2_r = { if r < &BigInt::zero() { - let h2_inv = BigInt::mod_inv(h2, &N_tilde).unwrap(); - BigInt::mod_pow(&h2_inv, &(-r), &N_tilde) + let h2_inv = BigInt::mod_inv(h2, N_tilde).unwrap(); + BigInt::mod_pow(&h2_inv, &(-r), N_tilde) } else { - BigInt::mod_pow(h2, &r, &N_tilde) + BigInt::mod_pow(h2, r, N_tilde) } }; - let com = BigInt::mod_mul(&h1_x, &h2_r, &N_tilde); - com + BigInt::mod_mul(&h1_x, &h2_r, N_tilde) } #[cfg(test)] From eb8f17f401605a999eb78180b8c37b2d9136d9ed Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 10:12:01 +0800 Subject: [PATCH 12/41] Format and complete fs-dkr clippy warnings. Change some formatting parameters for smaller screens. --- fs-dkr/src/add_party_message.rs | 527 +++--- fs-dkr/src/error.rs | 70 +- fs-dkr/src/lib.rs | 11 +- fs-dkr/src/range_proofs.rs | 1289 +++++++------ fs-dkr/src/refresh_message.rs | 903 +++++---- fs-dkr/src/ring_pedersen_proof.rs | 249 +-- fs-dkr/src/test.rs | 864 +++++---- fs-dkr/src/zk_pdl_with_slack.rs | 580 +++--- rustfmt.toml | 4 +- src/lib.rs | 48 +- src/party_i.rs | 1515 ++++++++------- src/presign/mod.rs | 328 ++-- src/presign/rounds.rs | 2301 ++++++++++++----------- src/presign/state_machine.rs | 1281 +++++++------ src/refresh/rounds.rs | 452 +++-- src/refresh/state_machine.rs | 1560 ++++++++------- src/sign/mod.rs | 72 +- src/sign/rounds.rs | 962 +++++----- src/sign/state_machine.rs | 855 +++++---- src/traits.rs | 32 +- src/utilities/aff_g/mod.rs | 895 +++++---- src/utilities/dec_q/mod.rs | 510 ++--- src/utilities/enc/mod.rs | 468 ++--- src/utilities/log_star/mod.rs | 536 +++--- src/utilities/mod.rs | 47 +- src/utilities/mta/mod.rs | 365 ++-- src/utilities/mta/range_proofs.rs | 1190 ++++++------ src/utilities/mta/test.rs | 26 +- src/utilities/mul/mod.rs | 390 ++-- src/utilities/mul_star/mod.rs | 511 ++--- src/utilities/sha2.rs | 76 +- src/utilities/zk_pdl/mod.rs | 333 ++-- src/utilities/zk_pdl/test.rs | 74 +- src/utilities/zk_pdl_with_slack/mod.rs | 324 ++-- src/utilities/zk_pdl_with_slack/test.rs | 220 +-- 35 files changed, 10710 insertions(+), 9158 deletions(-) diff --git a/fs-dkr/src/add_party_message.rs b/fs-dkr/src/add_party_message.rs index d998cf6c..a33563e8 100644 --- a/fs-dkr/src/add_party_message.rs +++ b/fs-dkr/src/add_party_message.rs @@ -1,37 +1,44 @@ //! Message definitions for new parties that can join the protocol //! Key points about a new party joining the refresh protocol: -//! * A new party wants to join, broadcasting a paillier ek, correctness of the ek generation, +//! * A new party wants to join, broadcasting a paillier ek, correctness of the +//! ek generation, //! dlog statements and dlog proofs. -//! * All the existing parties receives the join message. We assume for now that everyone accepts -//! the new party. All parties pick an index and add the new ek to their LocalKey at the given -//! index. -//! * The party index of the new party is transmitted back to the joining party offchannel (it's +//! * All the existing parties receives the join message. We assume for now that +//! everyone accepts +//! the new party. All parties pick an index and add the new ek to their +//! LocalKey at the given index. +//! * The party index of the new party is transmitted back to the joining party +//! offchannel (it's //! public information). -//! * All the existing parties enter the distribute phase, in which they start refreshing their +//! * All the existing parties enter the distribute phase, in which they start +//! refreshing their //! existing keys taking into the account the join messages that they received. -//! ** All parties (including new ones) collect the refresh messages and the join messages. +//! ** All parties (including new ones) collect the refresh messages and the +//! join messages. use crate::{ - error::{FsDkrError, FsDkrResult}, - refresh_message::RefreshMessage, + error::{FsDkrError, FsDkrResult}, + refresh_message::RefreshMessage, }; use curv::{ - arithmetic::{BasicOps, Modulo, One, Samplable, Zero}, - cryptographic_primitives::{ - hashing::Digest, - secret_sharing::feldman_vss::{ShamirSecretSharing, VerifiableSS}, - }, - elliptic::curves::{Curve, Point, Scalar}, - BigInt, + arithmetic::{BasicOps, Modulo, One, Samplable, Zero}, + cryptographic_primitives::{ + hashing::Digest, + secret_sharing::feldman_vss::{ShamirSecretSharing, VerifiableSS}, + }, + elliptic::curves::{Curve, Point, Scalar}, + BigInt, }; use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::{ - party_i::{Keys, SharedKeys}, - state_machine::keygen::LocalKey, + party_i::{Keys, SharedKeys}, + state_machine::keygen::LocalKey, }; use paillier::{Decrypt, EncryptionKey, KeyGeneration, Paillier}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fmt::Debug}; -use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement, NiCorrectKeyProof}; +use zk_paillier::zkproofs::{ + CompositeDLogProof, DLogStatement, NiCorrectKeyProof, +}; use crate::ring_pedersen_proof::{RingPedersenProof, RingPedersenStatement}; @@ -39,239 +46,275 @@ use crate::ring_pedersen_proof::{RingPedersenProof, RingPedersenStatement}; #[derive(Clone, Deserialize, Serialize, Debug)] #[serde(bound = "E: Curve, H: Digest + Clone")] pub struct JoinMessage { - pub(crate) ek: EncryptionKey, - pub(crate) dk_correctness_proof: NiCorrectKeyProof, - pub(crate) party_index: Option, - pub(crate) dlog_statement: DLogStatement, - pub(crate) composite_dlog_proof_base_h1: CompositeDLogProof, - pub(crate) composite_dlog_proof_base_h2: CompositeDLogProof, - pub(crate) ring_pedersen_statement: RingPedersenStatement, - pub(crate) ring_pedersen_proof: RingPedersenProof, + pub(crate) ek: EncryptionKey, + pub(crate) dk_correctness_proof: NiCorrectKeyProof, + pub(crate) party_index: Option, + pub(crate) dlog_statement: DLogStatement, + pub(crate) composite_dlog_proof_base_h1: CompositeDLogProof, + pub(crate) composite_dlog_proof_base_h2: CompositeDLogProof, + pub(crate) ring_pedersen_statement: RingPedersenStatement, + pub(crate) ring_pedersen_proof: RingPedersenProof, } -/// Generates the parameters needed for the h1_h2_N_tilde_vec. These parameters can be seen as -/// environment variables for each party that they agree on. In this case, each new party generates -/// it's own DlogStatements and submits it's proofs +/// Generates the parameters needed for the h1_h2_N_tilde_vec. These parameters +/// can be seen as environment variables for each party that they agree on. In +/// this case, each new party generates it's own DlogStatements and submits it's +/// proofs fn generate_h1_h2_n_tilde() -> (BigInt, BigInt, BigInt, BigInt, BigInt) { - let (ek_tilde, dk_tilde) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&ek_tilde.n); - let (mut xhi, mut xhi_inv) = loop { - let xhi_ = BigInt::sample_below(&phi); - match BigInt::mod_inv(&xhi_, &phi) { - Some(inv) => break (xhi_, inv), - None => continue, - } - }; - let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); - xhi = BigInt::sub(&phi, &xhi); - xhi_inv = BigInt::sub(&phi, &xhi_inv); - (ek_tilde.n, h1, h2, xhi, xhi_inv) + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&ek_tilde.n); + let (mut xhi, mut xhi_inv) = loop { + let xhi_ = BigInt::sample_below(&phi); + match BigInt::mod_inv(&xhi_, &phi) { + Some(inv) => break (xhi_, inv), + None => continue, + } + }; + let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); + xhi = BigInt::sub(&phi, &xhi); + xhi_inv = BigInt::sub(&phi, &xhi_inv); + (ek_tilde.n, h1, h2, xhi, xhi_inv) } -/// Generates the DlogStatement and CompositeProofs using the parameters generated by -/// [generate_h1_h2_n_tilde] -fn generate_dlog_statement_proofs() -> (DLogStatement, CompositeDLogProof, CompositeDLogProof) { - let (n_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_n_tilde(); +/// Generates the DlogStatement and CompositeProofs using the parameters +/// generated by [generate_h1_h2_n_tilde] +fn generate_dlog_statement_proofs( +) -> (DLogStatement, CompositeDLogProof, CompositeDLogProof) { + let (n_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_n_tilde(); - let dlog_statement_base_h1 = - DLogStatement { N: n_tilde.clone(), g: h1.clone(), ni: h2.clone() }; + let dlog_statement_base_h1 = + DLogStatement { N: n_tilde.clone(), g: h1.clone(), ni: h2.clone() }; - let dlog_statement_base_h2 = DLogStatement { N: n_tilde, g: h2, ni: h1 }; + let dlog_statement_base_h2 = DLogStatement { N: n_tilde, g: h2, ni: h1 }; - let composite_dlog_proof_base_h1 = CompositeDLogProof::prove(&dlog_statement_base_h1, &xhi); - let composite_dlog_proof_base_h2 = CompositeDLogProof::prove(&dlog_statement_base_h2, &xhi_inv); + let composite_dlog_proof_base_h1 = + CompositeDLogProof::prove(&dlog_statement_base_h1, &xhi); + let composite_dlog_proof_base_h2 = + CompositeDLogProof::prove(&dlog_statement_base_h2, &xhi_inv); - (dlog_statement_base_h1, composite_dlog_proof_base_h1, composite_dlog_proof_base_h2) + ( + dlog_statement_base_h1, + composite_dlog_proof_base_h1, + composite_dlog_proof_base_h2, + ) } impl JoinMessage { - pub fn set_party_index(&mut self, new_party_index: u16) { - self.party_index = Some(new_party_index); - } - /// The distribute phase for a new party. This distribute phase has to happen before the - /// existing parties distribute. Calling this function will generate a JoinMessage and a pair of - /// Paillier [Keys] that are going to be used when generating the [LocalKey]. - pub fn distribute() -> (Self, Keys) { - let paillier_key_pair = Keys::create(0); - let (dlog_statement, composite_dlog_proof_base_h1, composite_dlog_proof_base_h2) = - generate_dlog_statement_proofs(); - - let (ring_pedersen_statement, ring_pedersen_witness) = RingPedersenStatement::generate(); - - let ring_pedersen_proof = - RingPedersenProof::prove(&ring_pedersen_witness, &ring_pedersen_statement); - - let join_message = JoinMessage { - // in a join message, we only care about the ek and the correctness proof - ek: paillier_key_pair.ek.clone(), - dk_correctness_proof: NiCorrectKeyProof::proof(&paillier_key_pair.dk, None), - dlog_statement, - composite_dlog_proof_base_h1, - composite_dlog_proof_base_h2, - ring_pedersen_statement, - ring_pedersen_proof, - party_index: None, - }; - - (join_message, paillier_key_pair) - } - /// Returns the party index if it has been assigned one, throws - /// [FsDkrError::NewPartyUnassignedIndexError] otherwise - pub fn get_party_index(&self) -> FsDkrResult { - self.party_index.ok_or(FsDkrError::NewPartyUnassignedIndexError) - } - - /// Collect phase of the protocol. Compared to the [RefreshMessage::collect], this has to be - /// tailored for a sent JoinMessage on which we assigned party_index. In this collect, a - /// [LocalKey] is filled with the information provided by the [RefreshMessage]s from the other - /// parties and the other join messages (multiple parties can be added/replaced at once). - pub fn collect( - &self, - refresh_messages: &[RefreshMessage], - paillier_key: Keys, - join_messages: &[JoinMessage], - new_t: u16, - new_n: u16, - current_t: u16, - ) -> FsDkrResult> { - RefreshMessage::validate_collect(refresh_messages, current_t, new_n)?; - - for refresh_message in refresh_messages.iter() { - RingPedersenProof::verify( - &refresh_message.ring_pedersen_proof, - &refresh_message.ring_pedersen_statement, - ) - .map_err(|_| FsDkrError::RingPedersenProofValidation { - party_index: refresh_message.party_index, - })?; - } - - for join_message in join_messages.iter() { - RingPedersenProof::verify( - &join_message.ring_pedersen_proof, - &join_message.ring_pedersen_statement, - ) - .map_err(|e| { - if let Some(party_index) = join_message.party_index { - FsDkrError::RingPedersenProofValidation { party_index } - } else { - e - } - })?; - } - - // check if a party_index has been assigned to the current party - let party_index = self.get_party_index()?; - - // check if a party_index has been assigned to all other new parties - // TODO: Check if no party_index collision exists - for join_message in join_messages.iter() { - join_message.get_party_index()?; - } - - let parameters = ShamirSecretSharing { threshold: new_t, share_count: new_n }; - - // generate a new share, the details can be found here https://hackmd.io/@omershlo/Hy1jBo6JY. - let (cipher_text_sum, li_vec) = RefreshMessage::get_ciphertext_sum( - refresh_messages, - party_index, - ¶meters, - &paillier_key.ek, - current_t, - ); - let new_share = Paillier::decrypt(&paillier_key.dk, cipher_text_sum).0.into_owned(); - - let new_share_fe: Scalar = Scalar::::from(&new_share); - let paillier_dk = paillier_key.dk.clone(); - let key_linear_x_i = new_share_fe.clone(); - let key_linear_y = Point::::generator() * new_share_fe.clone(); - let keys_linear = SharedKeys { x_i: key_linear_x_i, y: key_linear_y }; - let mut pk_vec: Vec<_> = (0..new_n as usize) - .map(|i| refresh_messages[0].points_committed_vec[i].clone() * li_vec[0].clone()) - .collect(); - - for i in 0..new_n as usize { - for j in 1..(current_t + 1) as usize { - pk_vec[i] = pk_vec[i].clone() + - refresh_messages[j].points_committed_vec[i].clone() * li_vec[j].clone(); - } - } - - // check what parties are assigned in the current rotation and associate their paillier - // ek to each available party index. - - let available_parties: HashMap = refresh_messages - .iter() - .map(|msg| (msg.party_index, &msg.ek)) - .chain(std::iter::once((party_index, &paillier_key.ek))) - .chain( - join_messages - .iter() - .map(|join_message| (join_message.party_index.unwrap(), &join_message.ek)), - ) - .collect(); - - // TODO: submit the statement the dlog proof as well! - // check what parties are assigned in the current rotation and associate their - // DLogStatements and check their CompositeDlogProofs. - let available_h1_h2_ntilde_vec: HashMap = refresh_messages - .iter() - .map(|msg| (msg.party_index, &msg.dlog_statement)) - .chain(std::iter::once((party_index, &self.dlog_statement))) - .chain(join_messages.iter().map(|join_message| { - (join_message.party_index.unwrap(), &join_message.dlog_statement) - })) - .collect(); - - // generate the paillier public key vec needed for the LocalKey generation. - let paillier_key_vec: Vec = (1..new_n + 1) - .map(|party| { - let ek = available_parties.get(&party); - match ek { - None => EncryptionKey { n: BigInt::zero(), nn: BigInt::zero() }, - Some(key) => (*key).clone(), - } - }) - .collect(); - // generate the DLogStatement vec needed for the LocalKey generation. - let h1_h2_ntilde_vec: Vec = (1..new_n + 1) - .map(|party| { - let statement = available_h1_h2_ntilde_vec.get(&party); - - match statement { - None => generate_dlog_statement_proofs().0, - Some(dlog_statement) => (*dlog_statement).clone(), - } - }) - .collect(); - - // check if all the existing parties submitted the same public key. If they differ, abort. - // TODO: this should be verifiable? - for refresh_message in refresh_messages.iter() { - if refresh_message.public_key != refresh_messages[0].public_key { - return Err(FsDkrError::BroadcastedPublicKeyError) - } - } - - // generate the vss_scheme for the LocalKey - let (vss_scheme, _) = VerifiableSS::::share(new_t, new_n, &new_share_fe); - // TODO: secret cleanup might be needed. - - let local_key = LocalKey { - paillier_dk, - pk_vec, - keys_linear, - paillier_key_vec, - y_sum_s: refresh_messages[0].public_key.clone(), - h1_h2_n_tilde_vec: h1_h2_ntilde_vec, - vss_scheme, - i: party_index, - t: new_t, - n: new_n, - }; - - Ok(local_key) - } + pub fn set_party_index(&mut self, new_party_index: u16) { + self.party_index = Some(new_party_index); + } + /// The distribute phase for a new party. This distribute phase has to + /// happen before the existing parties distribute. Calling this function + /// will generate a JoinMessage and a pair of Paillier [Keys] that are + /// going to be used when generating the [LocalKey]. + pub fn distribute() -> (Self, Keys) { + let paillier_key_pair = Keys::create(0); + let ( + dlog_statement, + composite_dlog_proof_base_h1, + composite_dlog_proof_base_h2, + ) = generate_dlog_statement_proofs(); + + let (ring_pedersen_statement, ring_pedersen_witness) = + RingPedersenStatement::generate(); + + let ring_pedersen_proof = RingPedersenProof::prove( + &ring_pedersen_witness, + &ring_pedersen_statement, + ); + + let join_message = JoinMessage { + // in a join message, we only care about the ek and the correctness + // proof + ek: paillier_key_pair.ek.clone(), + dk_correctness_proof: NiCorrectKeyProof::proof( + &paillier_key_pair.dk, + None, + ), + dlog_statement, + composite_dlog_proof_base_h1, + composite_dlog_proof_base_h2, + ring_pedersen_statement, + ring_pedersen_proof, + party_index: None, + }; + + (join_message, paillier_key_pair) + } + /// Returns the party index if it has been assigned one, throws + /// [FsDkrError::NewPartyUnassignedIndexError] otherwise + pub fn get_party_index(&self) -> FsDkrResult { + self.party_index.ok_or(FsDkrError::NewPartyUnassignedIndexError) + } + + /// Collect phase of the protocol. Compared to the + /// [RefreshMessage::collect], this has to be tailored for a sent + /// JoinMessage on which we assigned party_index. In this collect, a + /// [LocalKey] is filled with the information provided by the + /// [RefreshMessage]s from the other parties and the other join messages + /// (multiple parties can be added/replaced at once). + pub fn collect( + &self, + refresh_messages: &[RefreshMessage], + paillier_key: Keys, + join_messages: &[JoinMessage], + new_t: u16, + new_n: u16, + current_t: u16, + ) -> FsDkrResult> { + RefreshMessage::validate_collect(refresh_messages, current_t, new_n)?; + + for refresh_message in refresh_messages.iter() { + RingPedersenProof::verify( + &refresh_message.ring_pedersen_proof, + &refresh_message.ring_pedersen_statement, + ) + .map_err(|_| { + FsDkrError::RingPedersenProofValidation { + party_index: refresh_message.party_index, + } + })?; + } + + for join_message in join_messages.iter() { + RingPedersenProof::verify( + &join_message.ring_pedersen_proof, + &join_message.ring_pedersen_statement, + ) + .map_err(|e| { + if let Some(party_index) = join_message.party_index { + FsDkrError::RingPedersenProofValidation { party_index } + } else { + e + } + })?; + } + + // check if a party_index has been assigned to the current party + let party_index = self.get_party_index()?; + + // check if a party_index has been assigned to all other new parties + // TODO: Check if no party_index collision exists + for join_message in join_messages.iter() { + join_message.get_party_index()?; + } + + let parameters = + ShamirSecretSharing { threshold: new_t, share_count: new_n }; + + // generate a new share, the details can be found here https://hackmd.io/@omershlo/Hy1jBo6JY. + let (cipher_text_sum, li_vec) = RefreshMessage::get_ciphertext_sum( + refresh_messages, + party_index, + ¶meters, + &paillier_key.ek, + current_t, + ); + let new_share = + Paillier::decrypt(&paillier_key.dk, cipher_text_sum).0.into_owned(); + + let new_share_fe: Scalar = Scalar::::from(&new_share); + let paillier_dk = paillier_key.dk.clone(); + let key_linear_x_i = new_share_fe.clone(); + let key_linear_y = Point::::generator() * new_share_fe.clone(); + let keys_linear = SharedKeys { x_i: key_linear_x_i, y: key_linear_y }; + let mut pk_vec: Vec<_> = (0..new_n as usize) + .map(|i| { + refresh_messages[0].points_committed_vec[i].clone() * + li_vec[0].clone() + }) + .collect(); + + #[allow(clippy::needless_range_loop)] + for i in 0..new_n as usize { + for j in 1..(current_t + 1) as usize { + pk_vec[i] = pk_vec[i].clone() + + refresh_messages[j].points_committed_vec[i].clone() * + li_vec[j].clone(); + } + } + + // check what parties are assigned in the current rotation and associate + // their paillier ek to each available party index. + + let available_parties: HashMap = refresh_messages + .iter() + .map(|msg| (msg.party_index, &msg.ek)) + .chain(std::iter::once((party_index, &paillier_key.ek))) + .chain(join_messages.iter().map(|join_message| { + (join_message.party_index.unwrap(), &join_message.ek) + })) + .collect(); + + // TODO: submit the statement the dlog proof as well! + // check what parties are assigned in the current rotation and associate + // their DLogStatements and check their CompositeDlogProofs. + let available_h1_h2_ntilde_vec: HashMap = + refresh_messages + .iter() + .map(|msg| (msg.party_index, &msg.dlog_statement)) + .chain(std::iter::once((party_index, &self.dlog_statement))) + .chain(join_messages.iter().map(|join_message| { + ( + join_message.party_index.unwrap(), + &join_message.dlog_statement, + ) + })) + .collect(); + + // generate the paillier public key vec needed for the LocalKey + // generation. + let paillier_key_vec: Vec = (1..new_n + 1) + .map(|party| { + let ek = available_parties.get(&party); + match ek { + None => + EncryptionKey { n: BigInt::zero(), nn: BigInt::zero() }, + Some(key) => (*key).clone(), + } + }) + .collect(); + // generate the DLogStatement vec needed for the LocalKey generation. + let h1_h2_ntilde_vec: Vec = (1..new_n + 1) + .map(|party| { + let statement = available_h1_h2_ntilde_vec.get(&party); + + match statement { + None => generate_dlog_statement_proofs().0, + Some(dlog_statement) => (*dlog_statement).clone(), + } + }) + .collect(); + + // check if all the existing parties submitted the same public key. If + // they differ, abort. TODO: this should be verifiable? + for refresh_message in refresh_messages.iter() { + if refresh_message.public_key != refresh_messages[0].public_key { + return Err(FsDkrError::BroadcastedPublicKeyError) + } + } + + // generate the vss_scheme for the LocalKey + let (vss_scheme, _) = + VerifiableSS::::share(new_t, new_n, &new_share_fe); + // TODO: secret cleanup might be needed. + + let local_key = LocalKey { + paillier_dk, + pk_vec, + keys_linear, + paillier_key_vec, + y_sum_s: refresh_messages[0].public_key.clone(), + h1_h2_n_tilde_vec: h1_h2_ntilde_vec, + vss_scheme, + i: party_index, + t: new_t, + n: new_n, + }; + + Ok(local_key) + } } diff --git a/fs-dkr/src/error.rs b/fs-dkr/src/error.rs index 6a1bfa8c..064d79c5 100644 --- a/fs-dkr/src/error.rs +++ b/fs-dkr/src/error.rs @@ -5,49 +5,51 @@ pub type FsDkrResult = Result; #[derive(Error, Debug, Clone, Serialize, Deserialize)] pub enum FsDkrError { - #[error("Too many malicious parties detected! Threshold {threshold:?}, Number of Refreshed Messages: {refreshed_keys:?}, Malicious parties detected when trying to refresh: malicious_parties:?")] - PartiesThresholdViolation { - threshold: u16, - refreshed_keys: usize, - // TODO: figure out how to retrieve the malicious parties indexes and add them to the err. - // malicious_parties: [usize] - }, + #[error("Too many malicious parties detected! Threshold {threshold:?}, Number of Refreshed Messages: {refreshed_keys:?}, Malicious parties detected when trying to refresh: malicious_parties:?")] + PartiesThresholdViolation { + threshold: u16, + refreshed_keys: usize, + // TODO: figure out how to retrieve the malicious parties indexes and + // add them to the err. malicious_parties: [usize] + }, - #[error("Shares did not pass verification.")] - PublicShareValidationError, + #[error("Shares did not pass verification.")] + PublicShareValidationError, - #[error("SizeMismatch error for the refresh message {refresh_message_index:?} - pdl proof length: {pdl_proof_len:?}, Points Commited Length: {points_commited_len:?}, Points Encrypted Length: {points_encrypted_len:?}")] - SizeMismatchError { - refresh_message_index: usize, - pdl_proof_len: usize, - points_commited_len: usize, - points_encrypted_len: usize, - }, + #[error("SizeMismatch error for the refresh message {refresh_message_index:?} - pdl proof length: {pdl_proof_len:?}, Points Commited Length: {points_commited_len:?}, Points Encrypted Length: {points_encrypted_len:?}")] + SizeMismatchError { + refresh_message_index: usize, + pdl_proof_len: usize, + points_commited_len: usize, + points_encrypted_len: usize, + }, - #[error("PDLwSlack proof verification failed, results: u1 == u1_test: {is_u1_eq:?}, u2 == u2_test: {is_u2_eq:?}, u3 == u3_test: {is_u3_eq:?}")] - PDLwSlackProof { is_u1_eq: bool, is_u2_eq: bool, is_u3_eq: bool }, + #[error("PDLwSlack proof verification failed, results: u1 == u1_test: {is_u1_eq:?}, u2 == u2_test: {is_u2_eq:?}, u3 == u3_test: {is_u3_eq:?}")] + PDLwSlackProof { is_u1_eq: bool, is_u2_eq: bool, is_u3_eq: bool }, - #[error("Ring Pedersen Proof Failed")] - RingPedersenProofError, + #[error("Ring Pedersen Proof Failed")] + RingPedersenProofError, - #[error("Range Proof failed for party: {party_index:?}")] - RangeProof { party_index: usize }, + #[error("Range Proof failed for party: {party_index:?}")] + RangeProof { party_index: usize }, - #[error("The Paillier moduli size of party: {party_index:?} is {moduli_size:?} bits, when it should be 2047-2048 bits")] - ModuliTooSmall { party_index: u16, moduli_size: usize }, + #[error("The Paillier moduli size of party: {party_index:?} is {moduli_size:?} bits, when it should be 2047-2048 bits")] + ModuliTooSmall { party_index: u16, moduli_size: usize }, - #[error("Paillier verification proof failed for party {party_index:?}")] - PaillierVerificationError { party_index: u16 }, + #[error("Paillier verification proof failed for party {party_index:?}")] + PaillierVerificationError { party_index: u16 }, - #[error("A new party did not receive a valid index.")] - NewPartyUnassignedIndexError, + #[error("A new party did not receive a valid index.")] + NewPartyUnassignedIndexError, - #[error("The broadcasted public key is not the same from everyone, aborting")] - BroadcastedPublicKeyError, + #[error( + "The broadcasted public key is not the same from everyone, aborting" + )] + BroadcastedPublicKeyError, - #[error("DLog proof failed for party {party_index:?}")] - DLogProofValidation { party_index: u16 }, + #[error("DLog proof failed for party {party_index:?}")] + DLogProofValidation { party_index: u16 }, - #[error("Ring pedersen proof failed for party {party_index:?}")] - RingPedersenProofValidation { party_index: u16 }, + #[error("Ring pedersen proof failed for party {party_index:?}")] + RingPedersenProofValidation { party_index: u16 }, } diff --git a/fs-dkr/src/lib.rs b/fs-dkr/src/lib.rs index 6f7378c4..f98b985f 100644 --- a/fs-dkr/src/lib.rs +++ b/fs-dkr/src/lib.rs @@ -3,11 +3,12 @@ //! FS-DKR is a protocol for rotation of threshold ECDSA keys. //! //! We use standard proactive security assumptions. The protocol will be run -//! by $n$ parties. We assume honest majority, that is, number of corruptions is $t<=n/2$. -//! The adversary is malicious, and rushing. For communication, the parties have access -//! to a broadcast channel (can be implemented via a bulletin board). For threshold ECDSA, -//! we focus on GG20 protocol, currently considered state of the art and most widely deployed -//! threshold ecdsa scheme (e.g. multi-party-ecdsa, tss-lib). +//! by $n$ parties. We assume honest majority, that is, number of corruptions is +//! $t<=n/2$. The adversary is malicious, and rushing. For communication, the +//! parties have access to a broadcast channel (can be implemented via a +//! bulletin board). For threshold ECDSA, we focus on GG20 protocol, currently +//! considered state of the art and most widely deployed threshold ecdsa scheme +//! (e.g. multi-party-ecdsa, tss-lib). //! //! Components of the library: //! diff --git a/fs-dkr/src/range_proofs.rs b/fs-dkr/src/range_proofs.rs index b8d5e25b..ea93b329 100644 --- a/fs-dkr/src/range_proofs.rs +++ b/fs-dkr/src/range_proofs.rs @@ -6,16 +6,18 @@ //! Zero knowledge range proofs for MtA protocol are implemented here. //! Formal description can be found in Appendix A of https://eprint.iacr.org/2019/114.pdf //! There are some deviations from the original specification: -//! 1) In Bob's proofs `gamma` is sampled from `[0;q^2 * N]` and `tau` from `[0;q^3 * N_tilde]`. -//! 2) A non-interactive version is implemented, with challenge `e` computed via Fiat-Shamir. +//! 1) In Bob's proofs `gamma` is sampled from `[0;q^2 * N]` and `tau` from +//! `[0;q^3 * N_tilde]`. +//! 2) A non-interactive version is implemented, with challenge `e` computed via +//! Fiat-Shamir. // TODO: Verify this matches (if possible) range proofs from multi-party-ecdsa use curv::{ - arithmetic::traits::*, - cryptographic_primitives::hashing::{Digest, DigestExt}, - elliptic::curves::{Curve, Point, Scalar, Secp256k1}, - BigInt, + arithmetic::traits::*, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{Curve, Point, Scalar, Secp256k1}, + BigInt, }; use paillier::{EncryptionKey, Randomness}; use serde::{Deserialize, Serialize}; @@ -27,701 +29,742 @@ use zk_paillier::zkproofs::DLogStatement; #[derive(Zeroize)] #[zeroize(drop)] struct AliceZkpRound1 { - alpha: BigInt, - beta: BigInt, - gamma: BigInt, - ro: BigInt, - z: BigInt, - u: BigInt, - w: BigInt, + alpha: BigInt, + beta: BigInt, + gamma: BigInt, + ro: BigInt, + z: BigInt, + u: BigInt, + w: BigInt, } impl AliceZkpRound1 { - fn from( - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - a: &BigInt, - q: &BigInt, - ) -> Self { - assert!( + fn from( + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + a: &BigInt, + q: &BigInt, + ) -> Self { + assert!( q.bit_length() <= 256, "We use SHA256 so we don't currently support moduli bigger than 256" ); - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - let N_tilde = &dlog_statement.N; - let alpha = BigInt::sample_below(&q.pow(3)); - let beta = BigInt::from_paillier_key(alice_ek); - let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let ro = BigInt::sample_below(&(q * N_tilde)); - let z = (BigInt::mod_pow(h1, a, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; - let u = ((alpha.borrow() * &alice_ek.n + 1) * - BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % - &alice_ek.nn; - let w = - (BigInt::mod_pow(h1, &alpha, N_tilde) * BigInt::mod_pow(h2, &gamma, N_tilde)) % N_tilde; - Self { alpha, beta, gamma, ro, z, u, w } - } + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let N_tilde = &dlog_statement.N; + let alpha = BigInt::sample_below(&q.pow(3)); + let beta = BigInt::from_paillier_key(alice_ek); + let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let ro = BigInt::sample_below(&(q * N_tilde)); + let z = (BigInt::mod_pow(h1, a, N_tilde) * + BigInt::mod_pow(h2, &ro, N_tilde)) % + N_tilde; + let u = ((alpha.borrow() * &alice_ek.n + 1) * + BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % + &alice_ek.nn; + let w = (BigInt::mod_pow(h1, &alpha, N_tilde) * + BigInt::mod_pow(h2, &gamma, N_tilde)) % + N_tilde; + Self { alpha, beta, gamma, ro, z, u, w } + } } /// Represents the second round of the interactive version of the proof struct AliceZkpRound2 { - s: BigInt, - s1: BigInt, - s2: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, } impl AliceZkpRound2 { - fn from( - alice_ek: &EncryptionKey, - round1: &AliceZkpRound1, - e: &BigInt, - a: &BigInt, - r: &BigInt, - ) -> Self { - Self { - s: (BigInt::mod_pow(r, &e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, - s1: (e * a) + round1.alpha.borrow(), - s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), - } - } + fn from( + alice_ek: &EncryptionKey, + round1: &AliceZkpRound1, + e: &BigInt, + a: &BigInt, + r: &BigInt, + ) -> Self { + Self { + s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) % + &alice_ek.n, + s1: (e * a) + round1.alpha.borrow(), + s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), + } + } } /// Alice's proof #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AliceProof { - z: BigInt, - e: BigInt, - s: BigInt, - s1: BigInt, - s2: BigInt, - _phantom: PhantomData<(E, H)>, + z: BigInt, + e: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, + _phantom: PhantomData<(E, H)>, } impl AliceProof { - /// verify Alice's proof using the proof and public keys - pub fn verify( - &self, - cipher: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - ) -> bool { - let N = &alice_ek.n; - let NN = &alice_ek.nn; - let N_tilde = &dlog_statement.N; - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - let Gen = alice_ek.n.borrow() + 1; - - if self.s1 > Scalar::::group_order().pow(3) { - return false - } - - let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); - let z_e_inv = match z_e_inv { - // z must be invertible, yet the check is done here - None => return false, - Some(c) => c, - }; - - let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) * - BigInt::mod_pow(h2, &self.s2, N_tilde) * - z_e_inv) % N_tilde; - - let gs1 = (self.s1.borrow() * N + 1) % NN; - let cipher_e_inv = BigInt::mod_inv(&BigInt::mod_pow(cipher, &self.e, NN), NN); - let cipher_e_inv = match cipher_e_inv { - None => return false, - Some(c) => c, - }; - - let u = (gs1 * BigInt::mod_pow(&self.s, N, NN) * cipher_e_inv) % NN; - - let e = H::new() - .chain_bigint(N) - .chain_bigint(&Gen) - .chain_bigint(cipher) - .chain_bigint(&self.z) - .chain_bigint(&u) - .chain_bigint(&w) - .result_bigint(); - - if e != self.e { - return false - } - - true - } - /// Create the proof using Alice's Paillier private keys and public ZKP setup. - /// Requires randomness used for encrypting Alice's secret a. - /// It is assumed that a curve order smaller than 2^256 is used.. - pub fn generate( - a: &BigInt, - cipher: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - r: &BigInt, - ) -> Self { - let q = Scalar::::group_order(); - assert!( + /// verify Alice's proof using the proof and public keys + pub fn verify( + &self, + cipher: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + ) -> bool { + let N = &alice_ek.n; + let NN = &alice_ek.nn; + let N_tilde = &dlog_statement.N; + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let Gen = alice_ek.n.borrow() + 1; + + if self.s1 > Scalar::::group_order().pow(3) { + return false + } + + let z_e_inv = BigInt::mod_inv( + &BigInt::mod_pow(&self.z, &self.e, N_tilde), + N_tilde, + ); + let z_e_inv = match z_e_inv { + // z must be invertible, yet the check is done here + None => return false, + Some(c) => c, + }; + + let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) * + BigInt::mod_pow(h2, &self.s2, N_tilde) * + z_e_inv) % + N_tilde; + + let gs1 = (self.s1.borrow() * N + 1) % NN; + let cipher_e_inv = + BigInt::mod_inv(&BigInt::mod_pow(cipher, &self.e, NN), NN); + let cipher_e_inv = match cipher_e_inv { + None => return false, + Some(c) => c, + }; + + let u = (gs1 * BigInt::mod_pow(&self.s, N, NN) * cipher_e_inv) % NN; + + let e = H::new() + .chain_bigint(N) + .chain_bigint(&Gen) + .chain_bigint(cipher) + .chain_bigint(&self.z) + .chain_bigint(&u) + .chain_bigint(&w) + .result_bigint(); + + if e != self.e { + return false + } + + true + } + /// Create the proof using Alice's Paillier private keys and public ZKP + /// setup. Requires randomness used for encrypting Alice's secret a. + /// It is assumed that a curve order smaller than 2^256 is used.. + pub fn generate( + a: &BigInt, + cipher: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &BigInt, + ) -> Self { + let q = Scalar::::group_order(); + assert!( q.bit_length() <= 256, "We use SHA256 so we don't currently support moduli bigger than 256" ); - let round1 = AliceZkpRound1::from(alice_ek, dlog_statement, a, q); - - let Gen = alice_ek.n.borrow() + 1; - let e = H::new() - .chain_bigint(&alice_ek.n) - .chain_bigint(&Gen) - .chain_bigint(cipher) - .chain_bigint(&round1.z) - .chain_bigint(&round1.u) - .chain_bigint(&round1.w) - .result_bigint(); - - let round2 = AliceZkpRound2::from(alice_ek, &round1, &e, a, r); - - Self { - z: round1.z.clone(), - e, - s: round2.s, - s1: round2.s1, - s2: round2.s2, - _phantom: PhantomData, - } - } + let round1 = AliceZkpRound1::from(alice_ek, dlog_statement, a, q); + + let Gen = alice_ek.n.borrow() + 1; + let e = H::new() + .chain_bigint(&alice_ek.n) + .chain_bigint(&Gen) + .chain_bigint(cipher) + .chain_bigint(&round1.z) + .chain_bigint(&round1.u) + .chain_bigint(&round1.w) + .result_bigint(); + + let round2 = AliceZkpRound2::from(alice_ek, &round1, &e, a, r); + + Self { + z: round1.z.clone(), + e, + s: round2.s, + s1: round2.s1, + s2: round2.s2, + _phantom: PhantomData, + } + } } /// Represents first round of the interactive version of the proof struct BobZkpRound1 { - pub alpha: BigInt, - pub beta: BigInt, - pub gamma: BigInt, - pub ro: BigInt, - pub ro_prim: BigInt, - pub sigma: BigInt, - pub tau: BigInt, - pub z: BigInt, - pub z_prim: BigInt, - pub t: BigInt, - pub w: BigInt, - pub v: BigInt, - _phantom: PhantomData, + pub alpha: BigInt, + pub beta: BigInt, + pub gamma: BigInt, + pub ro: BigInt, + pub ro_prim: BigInt, + pub sigma: BigInt, + pub tau: BigInt, + pub z: BigInt, + pub z_prim: BigInt, + pub t: BigInt, + pub w: BigInt, + pub v: BigInt, + _phantom: PhantomData, } impl Zeroize for BobZkpRound1 { - fn zeroize(&mut self) { - self.alpha.zeroize(); - self.beta.zeroize(); - self.gamma.zeroize(); - self.ro.zeroize(); - self.ro_prim.zeroize(); - self.sigma.zeroize(); - self.tau.zeroize(); - self.z.zeroize(); - self.z_prim.zeroize(); - self.t.zeroize(); - self.w.zeroize(); - self.v.zeroize(); - } + fn zeroize(&mut self) { + self.alpha.zeroize(); + self.beta.zeroize(); + self.gamma.zeroize(); + self.ro.zeroize(); + self.ro_prim.zeroize(); + self.sigma.zeroize(); + self.tau.zeroize(); + self.z.zeroize(); + self.z_prim.zeroize(); + self.t.zeroize(); + self.w.zeroize(); + self.v.zeroize(); + } } impl Drop for BobZkpRound1 { - fn drop(&mut self) { - self.zeroize(); - } + fn drop(&mut self) { + self.zeroize(); + } } impl BobZkpRound1 { - /// `b` - Bob's secret - /// `beta_prim` - randomly chosen in `MtA` by Bob - /// `a_encrypted` - Alice's secret encrypted by Alice - fn from( - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - b: &Scalar, - beta_prim: &BigInt, - a_encrypted: &BigInt, - q: &BigInt, - ) -> Self { - assert!( + /// `b` - Bob's secret + /// `beta_prim` - randomly chosen in `MtA` by Bob + /// `a_encrypted` - Alice's secret encrypted by Alice + fn from( + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + b: &Scalar, + beta_prim: &BigInt, + a_encrypted: &BigInt, + q: &BigInt, + ) -> Self { + assert!( q.bit_length() <= 256, "We use SHA256 so we don't currently support moduli bigger than 256" ); - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - let N_tilde = &dlog_statement.N; - let b_bn = b.to_bigint(); - - let alpha = BigInt::sample_below(&q.pow(3)); - let beta = BigInt::from_paillier_key(alice_ek); - let gamma = BigInt::sample_below(&(q.pow(2) * &alice_ek.n)); - let ro = BigInt::sample_below(&(q * N_tilde)); - let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let sigma = BigInt::sample_below(&(q * N_tilde)); - let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; - let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) * - BigInt::mod_pow(h2, &ro_prim, N_tilde)) % - N_tilde; - let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * BigInt::mod_pow(h2, &sigma, N_tilde)) % - N_tilde; - let w = - (BigInt::mod_pow(h1, &gamma, N_tilde) * BigInt::mod_pow(h2, &tau, N_tilde)) % N_tilde; - let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) * - (gamma.borrow() * &alice_ek.n + 1) * - BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % - &alice_ek.nn; - Self { - alpha, - beta, - gamma, - ro, - ro_prim, - sigma, - tau, - z, - z_prim, - t, - w, - v, - _phantom: PhantomData, - } - } + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let N_tilde = &dlog_statement.N; + let b_bn = b.to_bigint(); + + let alpha = BigInt::sample_below(&q.pow(3)); + let beta = BigInt::from_paillier_key(alice_ek); + let gamma = BigInt::sample_below(&(q.pow(2) * &alice_ek.n)); + let ro = BigInt::sample_below(&(q * N_tilde)); + let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let sigma = BigInt::sample_below(&(q * N_tilde)); + let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * + BigInt::mod_pow(h2, &ro, N_tilde)) % + N_tilde; + let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) * + BigInt::mod_pow(h2, &ro_prim, N_tilde)) % + N_tilde; + let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * + BigInt::mod_pow(h2, &sigma, N_tilde)) % + N_tilde; + let w = (BigInt::mod_pow(h1, &gamma, N_tilde) * + BigInt::mod_pow(h2, &tau, N_tilde)) % + N_tilde; + let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) * + (gamma.borrow() * &alice_ek.n + 1) * + BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % + &alice_ek.nn; + Self { + alpha, + beta, + gamma, + ro, + ro_prim, + sigma, + tau, + z, + z_prim, + t, + w, + v, + _phantom: PhantomData, + } + } } /// represents second round of the interactive version of the proof struct BobZkpRound2 { - pub s: BigInt, - pub s1: BigInt, - pub s2: BigInt, - pub t1: BigInt, - pub t2: BigInt, - _phantom: PhantomData, + pub s: BigInt, + pub s1: BigInt, + pub s2: BigInt, + pub t1: BigInt, + pub t2: BigInt, + _phantom: PhantomData, } impl BobZkpRound2 { - /// `e` - the challenge in interactive ZKP, the hash in non-interactive ZKP - /// `b` - Bob's secret - /// `beta_prim` - randomly chosen in `MtA` by Bob - /// `r` - randomness used by Bob on Alice's public Paillier key to encrypt `beta_prim` in `MtA` - fn from( - alice_ek: &EncryptionKey, - round1: &BobZkpRound1, - e: &BigInt, - b: &Scalar, - beta_prim: &BigInt, - r: &Randomness, - ) -> Self { - let b_bn = b.to_bigint(); - Self { - s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, - s1: (e * b_bn) + round1.alpha.borrow(), - s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), - t1: (e * beta_prim) + round1.gamma.borrow(), - t2: (e * round1.sigma.borrow()) + round1.tau.borrow(), - _phantom: PhantomData, - } - } + /// `e` - the challenge in interactive ZKP, the hash in non-interactive ZKP + /// `b` - Bob's secret + /// `beta_prim` - randomly chosen in `MtA` by Bob + /// `r` - randomness used by Bob on Alice's public Paillier key to encrypt + /// `beta_prim` in `MtA` + fn from( + alice_ek: &EncryptionKey, + round1: &BobZkpRound1, + e: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + r: &Randomness, + ) -> Self { + let b_bn = b.to_bigint(); + Self { + s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * + round1.beta.borrow()) % + &alice_ek.n, + s1: (e * b_bn) + round1.alpha.borrow(), + s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), + t1: (e * beta_prim) + round1.gamma.borrow(), + t2: (e * round1.sigma.borrow()) + round1.tau.borrow(), + _phantom: PhantomData, + } + } } /// Additional fields in Bob's proof if MtA is run with check pub struct BobCheck { - u: Point, - X: Point, + u: Point, + X: Point, } /// Bob's regular proof #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub struct BobProof { - t: BigInt, - z: BigInt, - e: BigInt, - s: BigInt, - s1: BigInt, - s2: BigInt, - t1: BigInt, - t2: BigInt, - _phantom: PhantomData<(E, H)>, + t: BigInt, + z: BigInt, + e: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, + t1: BigInt, + t2: BigInt, + _phantom: PhantomData<(E, H)>, } #[allow(clippy::too_many_arguments)] impl BobProof { - pub fn verify( - &self, - a_enc: &BigInt, - mta_avc_out: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - check: Option<&BobCheck>, - ) -> bool { - let N = &alice_ek.n; - let NN = &alice_ek.nn; - let N_tilde = &dlog_statement.N; - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - - if self.s1 > Scalar::::group_order().pow(3) { - return false - } - - let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); - let z_e_inv = match z_e_inv { - // z must be invertible, yet the check is done here - None => return false, - Some(c) => c, - }; - - let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) * - BigInt::mod_pow(h2, &self.s2, N_tilde) * - z_e_inv) % N_tilde; - - let mta_e_inv = BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); - let mta_e_inv = match mta_e_inv { - None => return false, - Some(c) => c, - }; - - let v = (BigInt::mod_pow(a_enc, &self.s1, NN) * - BigInt::mod_pow(&self.s, N, NN) * - (self.t1.borrow() * N + 1) * - mta_e_inv) % NN; - - let t_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.t, &self.e, N_tilde), N_tilde); - let t_e_inv = match t_e_inv { - None => return false, - Some(c) => c, - }; - - let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) * - BigInt::mod_pow(h2, &self.t2, N_tilde) * - t_e_inv) % N_tilde; - - let Gen = alice_ek.n.borrow() + 1; - - let hash = H::new() - .chain_bigint(&alice_ek.n) - .chain_bigint(&Gen) - .chain_bigint(a_enc) - .chain_bigint(mta_avc_out) - .chain_bigint(&self.z) - .chain_bigint(&z_prim) - .chain_bigint(&self.t) - .chain_bigint(&v) - .chain_bigint(&w); - - let e = match check { - Some(_) => { - let X_x_coor = check.unwrap().X.x_coord().unwrap(); - let X_y_coor = check.unwrap().X.y_coord().unwrap(); - let u_x_coor = check.unwrap().u.x_coord().unwrap(); - let u_y_coor = check.unwrap().u.y_coord().unwrap(); - hash.chain_bigint(&X_x_coor) - .chain_bigint(&X_y_coor) - .chain_bigint(&u_x_coor) - .chain_bigint(&u_y_coor) - .result_bigint() - }, - None => hash.result_bigint(), - }; - - if e != self.e { - return false - } - - true - } - - pub fn generate( - a_encrypted: &BigInt, - mta_encrypted: &BigInt, - b: &Scalar, - beta_prim: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - r: &Randomness, - check: bool, - ) -> (BobProof, Option>) { - let round1 = BobZkpRound1::from( - alice_ek, - dlog_statement, - b, - beta_prim, - a_encrypted, - &Scalar::::group_order(), - ); - - let Gen = alice_ek.n.borrow() + 1; - let hash = H::new() - .chain_bigint(&alice_ek.n) - .chain_bigint(&Gen) - .chain_bigint(a_encrypted) - .chain_bigint(mta_encrypted) - .chain_bigint(&round1.z) - .chain_bigint(&round1.z_prim) - .chain_bigint(&round1.t) - .chain_bigint(&round1.v) - .chain_bigint(&round1.w); - let mut check_u = None; - let e = if check { - let (X, u) = { - let ec_gen: Point = Point::::generator().to_point(); - let alpha: Scalar = Scalar::::from(&round1.alpha); - (ec_gen.clone() * b.clone(), ec_gen.clone() * alpha) - }; - check_u = Some(u.clone()); - let X_x_coor = X.x_coord().unwrap(); - let X_y_coor = X.y_coord().unwrap(); - let u_x_coor = u.x_coord().unwrap(); - let u_y_coor = u.y_coord().unwrap(); - hash.chain_bigint(&X_x_coor) - .chain_bigint(&X_y_coor) - .chain_bigint(&u_x_coor) - .chain_bigint(&u_y_coor) - .result_bigint() - } else { - hash.result_bigint() - }; - - let round2 = BobZkpRound2::from(alice_ek, &round1, &e, b, beta_prim, r); - - ( - BobProof { - t: round1.t.clone(), - z: round1.z.clone(), - e, - s: round2.s, - s1: round2.s1, - s2: round2.s2, - t1: round2.t1, - t2: round2.t2, - _phantom: PhantomData, - }, - check_u, - ) - } + pub fn verify( + &self, + a_enc: &BigInt, + mta_avc_out: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + check: Option<&BobCheck>, + ) -> bool { + let N = &alice_ek.n; + let NN = &alice_ek.nn; + let N_tilde = &dlog_statement.N; + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + + if self.s1 > Scalar::::group_order().pow(3) { + return false + } + + let z_e_inv = BigInt::mod_inv( + &BigInt::mod_pow(&self.z, &self.e, N_tilde), + N_tilde, + ); + let z_e_inv = match z_e_inv { + // z must be invertible, yet the check is done here + None => return false, + Some(c) => c, + }; + + let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) * + BigInt::mod_pow(h2, &self.s2, N_tilde) * + z_e_inv) % + N_tilde; + + let mta_e_inv = + BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); + let mta_e_inv = match mta_e_inv { + None => return false, + Some(c) => c, + }; + + let v = (BigInt::mod_pow(a_enc, &self.s1, NN) * + BigInt::mod_pow(&self.s, N, NN) * + (self.t1.borrow() * N + 1) * + mta_e_inv) % + NN; + + let t_e_inv = BigInt::mod_inv( + &BigInt::mod_pow(&self.t, &self.e, N_tilde), + N_tilde, + ); + let t_e_inv = match t_e_inv { + None => return false, + Some(c) => c, + }; + + let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) * + BigInt::mod_pow(h2, &self.t2, N_tilde) * + t_e_inv) % + N_tilde; + + let Gen = alice_ek.n.borrow() + 1; + + let hash = H::new() + .chain_bigint(&alice_ek.n) + .chain_bigint(&Gen) + .chain_bigint(a_enc) + .chain_bigint(mta_avc_out) + .chain_bigint(&self.z) + .chain_bigint(&z_prim) + .chain_bigint(&self.t) + .chain_bigint(&v) + .chain_bigint(&w); + + let e = match check { + Some(_) => { + let X_x_coor = check.unwrap().X.x_coord().unwrap(); + let X_y_coor = check.unwrap().X.y_coord().unwrap(); + let u_x_coor = check.unwrap().u.x_coord().unwrap(); + let u_y_coor = check.unwrap().u.y_coord().unwrap(); + hash.chain_bigint(&X_x_coor) + .chain_bigint(&X_y_coor) + .chain_bigint(&u_x_coor) + .chain_bigint(&u_y_coor) + .result_bigint() + }, + None => hash.result_bigint(), + }; + + if e != self.e { + return false + } + + true + } + + pub fn generate( + a_encrypted: &BigInt, + mta_encrypted: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &Randomness, + check: bool, + ) -> (BobProof, Option>) { + let round1 = BobZkpRound1::from( + alice_ek, + dlog_statement, + b, + beta_prim, + a_encrypted, + Scalar::::group_order(), + ); + + let Gen = alice_ek.n.borrow() + 1; + let hash = H::new() + .chain_bigint(&alice_ek.n) + .chain_bigint(&Gen) + .chain_bigint(a_encrypted) + .chain_bigint(mta_encrypted) + .chain_bigint(&round1.z) + .chain_bigint(&round1.z_prim) + .chain_bigint(&round1.t) + .chain_bigint(&round1.v) + .chain_bigint(&round1.w); + let mut check_u = None; + let e = if check { + let (X, u) = { + let ec_gen: Point = Point::::generator().to_point(); + let alpha: Scalar = Scalar::::from(&round1.alpha); + (ec_gen.clone() * b.clone(), ec_gen.clone() * alpha) + }; + check_u = Some(u.clone()); + let X_x_coor = X.x_coord().unwrap(); + let X_y_coor = X.y_coord().unwrap(); + let u_x_coor = u.x_coord().unwrap(); + let u_y_coor = u.y_coord().unwrap(); + hash.chain_bigint(&X_x_coor) + .chain_bigint(&X_y_coor) + .chain_bigint(&u_x_coor) + .chain_bigint(&u_y_coor) + .result_bigint() + } else { + hash.result_bigint() + }; + + let round2 = BobZkpRound2::from(alice_ek, &round1, &e, b, beta_prim, r); + + ( + BobProof { + t: round1.t.clone(), + z: round1.z.clone(), + e, + s: round2.s, + s1: round2.s1, + s2: round2.s2, + t1: round2.t1, + t2: round2.t2, + _phantom: PhantomData, + }, + check_u, + ) + } } /// Bob's extended proof, adds the knowledge of $`B = g^b \in \mathcal{G}`$ #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub struct BobProofExt { - proof: BobProof, - u: Point, + proof: BobProof, + u: Point, } #[allow(clippy::too_many_arguments)] impl BobProofExt { - pub fn verify( - &self, - a_enc: &BigInt, - mta_avc_out: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - X: &Point, - ) -> bool { - // check basic proof first - if !self.proof.verify( - a_enc, - mta_avc_out, - alice_ek, - dlog_statement, - Some(&BobCheck { u: self.u.clone(), X: X.clone() }), - ) { - return false - } - - // fiddle with EC points - let (x1, x2) = { - let ec_gen: Point = Point::::generator().to_point(); - let s1: Scalar = Scalar::::from(&self.proof.s1); - let e: Scalar = Scalar::::from(&self.proof.e); - (ec_gen * s1, (X.clone() * e) + self.u.clone()) - }; - - if x1 != x2 { - return false - } - - true - } - - fn generate( - a_encrypted: &BigInt, - mta_encrypted: &BigInt, - b: &Scalar, - beta_prim: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - r: &Randomness, - ) -> BobProofExt { - // proving a basic proof (with modified hash) - let (bob_proof, u) = BobProof::generate( - a_encrypted, - mta_encrypted, - b, - beta_prim, - alice_ek, - dlog_statement, - r, - true, - ); - - BobProofExt { proof: bob_proof, u: u.unwrap() } - } + pub fn verify( + &self, + a_enc: &BigInt, + mta_avc_out: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + X: &Point, + ) -> bool { + // check basic proof first + if !self.proof.verify( + a_enc, + mta_avc_out, + alice_ek, + dlog_statement, + Some(&BobCheck { u: self.u.clone(), X: X.clone() }), + ) { + return false + } + + // fiddle with EC points + let (x1, x2) = { + let ec_gen: Point = Point::::generator().to_point(); + let s1: Scalar = Scalar::::from(&self.proof.s1); + let e: Scalar = Scalar::::from(&self.proof.e); + (ec_gen * s1, (X.clone() * e) + self.u.clone()) + }; + + if x1 != x2 { + return false + } + + true + } + + fn generate( + a_encrypted: &BigInt, + mta_encrypted: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &Randomness, + ) -> BobProofExt { + // proving a basic proof (with modified hash) + let (bob_proof, u) = BobProof::generate( + a_encrypted, + mta_encrypted, + b, + beta_prim, + alice_ek, + dlog_statement, + r, + true, + ); + + BobProofExt { proof: bob_proof, u: u.unwrap() } + } } /// sample random value of an element of a multiplicative group pub trait SampleFromMultiplicativeGroup { - fn from_modulo(N: &BigInt) -> BigInt; - fn from_paillier_key(ek: &EncryptionKey) -> BigInt; + fn from_modulo(N: &BigInt) -> BigInt; + fn from_paillier_key(ek: &EncryptionKey) -> BigInt; } impl SampleFromMultiplicativeGroup for BigInt { - fn from_modulo(N: &BigInt) -> BigInt { - let One = BigInt::one(); - loop { - let r = Self::sample_below(N); - if r.gcd(N) == One { - return r - } - } - } - - fn from_paillier_key(ek: &EncryptionKey) -> BigInt { - Self::from_modulo(ek.n.borrow()) - } + fn from_modulo(N: &BigInt) -> BigInt { + let One = BigInt::one(); + loop { + let r = Self::sample_below(N); + if r.gcd(N) == One { + return r + } + } + } + + fn from_paillier_key(ek: &EncryptionKey) -> BigInt { + Self::from_modulo(ek.n.borrow()) + } } #[cfg(test)] pub(crate) mod tests { - use super::*; - use curv::elliptic::curves::{ - secp256_k1::{Secp256k1Point, Secp256k1Scalar}, - ECScalar, - }; - use paillier::{ - traits::{Encrypt, EncryptWithChosenRandomness, KeyGeneration}, - Add, DecryptionKey, Mul, Paillier, RawCiphertext, RawPlaintext, - }; - use sha2::Sha256; - - type GE = Secp256k1Point; - type FE = Secp256k1Scalar; - - pub(crate) fn generate_init() -> (DLogStatement, EncryptionKey, DecryptionKey) { - let (ek_tilde, dk_tilde) = - Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&ek_tilde.n); - let (xhi, _) = loop { - let xhi_ = BigInt::sample_below(&phi); - match BigInt::mod_inv(&xhi_, &phi) { - Some(inv) => break (xhi_, inv), - None => continue, - } - }; - let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); - - let (ek, dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - let dlog_statement = DLogStatement { g: h1, ni: h2, N: ek_tilde.n }; - (dlog_statement, ek, dk) - } - - #[test] - fn alice_zkp() { - let (dlog_statement, ek, _) = generate_init(); - - // Alice's secret value - let a = FE::random().to_bigint(); - let r = BigInt::from_paillier_key(&ek); - let cipher = Paillier::encrypt_with_chosen_randomness( - &ek, - RawPlaintext::from(a.clone()), - &Randomness::from(&r), - ) - .0 - .clone() - .into_owned(); - - let alice_proof = - AliceProof::::generate(&a, &cipher, &ek, &dlog_statement, &r); - - assert!(alice_proof.verify(&cipher, &ek, &dlog_statement)); - } - - #[test] - fn bob_zkp() { - let (dlog_statement, ek, _) = generate_init(); - - (0..5).for_each(|_| { - let alice_public_key = &ek; - - // run MtA protocol with different inputs - (0..5).for_each(|_| { - // Simulate Alice - let a = FE::random().to_bigint(); - let encrypted_a = Paillier::encrypt(alice_public_key, RawPlaintext::from(a)) - .0 - .clone() - .into_owned(); - - // Bob follows MtA - let b: Scalar = Scalar::::random(); - // E(a) * b - let b_times_enc_a = Paillier::mul( - alice_public_key, - RawCiphertext::from(encrypted_a.clone()), - RawPlaintext::from(&b.to_bigint()), - ); - let beta_prim = BigInt::sample_below(&alice_public_key.n); - let r = Randomness::sample(alice_public_key); - let enc_beta_prim = Paillier::encrypt_with_chosen_randomness( - alice_public_key, - RawPlaintext::from(&beta_prim), - &r, - ); - - let mta_out = Paillier::add(alice_public_key, b_times_enc_a, enc_beta_prim); - - let (bob_proof, _) = BobProof::::generate( - &encrypted_a, - &mta_out.0.clone().into_owned(), - &b, - &beta_prim, - alice_public_key, - &dlog_statement, - &r, - false, - ); - assert!(bob_proof.verify( - &encrypted_a, - &mta_out.0.clone().into_owned(), - alice_public_key, - &dlog_statement, - None - )); - - // Bob follows MtAwc - let ec_gen: Point = Point::::generator().to_point(); - let X = ec_gen * b.clone(); - let bob_proof = BobProofExt::::generate( - &encrypted_a, - &mta_out.0.clone().into_owned(), - &b, - &beta_prim, - alice_public_key, - &dlog_statement, - &r, - ); - assert!(bob_proof.verify( - &encrypted_a, - &mta_out.0.clone().into_owned(), - alice_public_key, - &dlog_statement, - &X - )); - }); - }); - } + use super::*; + use curv::elliptic::curves::{ + secp256_k1::{Secp256k1Point, Secp256k1Scalar}, + ECScalar, + }; + use paillier::{ + traits::{Encrypt, EncryptWithChosenRandomness, KeyGeneration}, + Add, DecryptionKey, Mul, Paillier, RawCiphertext, RawPlaintext, + }; + use sha2::Sha256; + + type GE = Secp256k1Point; + type FE = Secp256k1Scalar; + + pub(crate) fn generate_init( + ) -> (DLogStatement, EncryptionKey, DecryptionKey) { + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE) + .keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&ek_tilde.n); + let (xhi, _) = loop { + let xhi_ = BigInt::sample_below(&phi); + match BigInt::mod_inv(&xhi_, &phi) { + Some(inv) => break (xhi_, inv), + None => continue, + } + }; + let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); + + let (ek, dk) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE) + .keys(); + let dlog_statement = DLogStatement { g: h1, ni: h2, N: ek_tilde.n }; + (dlog_statement, ek, dk) + } + + #[test] + fn alice_zkp() { + let (dlog_statement, ek, _) = generate_init(); + + // Alice's secret value + let a = FE::random().to_bigint(); + let r = BigInt::from_paillier_key(&ek); + let cipher = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(a.clone()), + &Randomness::from(&r), + ) + .0 + .clone() + .into_owned(); + + let alice_proof = AliceProof::::generate( + &a, + &cipher, + &ek, + &dlog_statement, + &r, + ); + + assert!(alice_proof.verify(&cipher, &ek, &dlog_statement)); + } + + #[test] + fn bob_zkp() { + let (dlog_statement, ek, _) = generate_init(); + + (0..5).for_each(|_| { + let alice_public_key = &ek; + + // run MtA protocol with different inputs + (0..5).for_each(|_| { + // Simulate Alice + let a = FE::random().to_bigint(); + let encrypted_a = + Paillier::encrypt(alice_public_key, RawPlaintext::from(a)) + .0 + .clone() + .into_owned(); + + // Bob follows MtA + let b: Scalar = Scalar::::random(); + // E(a) * b + let b_times_enc_a = Paillier::mul( + alice_public_key, + RawCiphertext::from(encrypted_a.clone()), + RawPlaintext::from(&b.to_bigint()), + ); + let beta_prim = BigInt::sample_below(&alice_public_key.n); + let r = Randomness::sample(alice_public_key); + let enc_beta_prim = Paillier::encrypt_with_chosen_randomness( + alice_public_key, + RawPlaintext::from(&beta_prim), + &r, + ); + + let mta_out = Paillier::add( + alice_public_key, + b_times_enc_a, + enc_beta_prim, + ); + + let (bob_proof, _) = BobProof::::generate( + &encrypted_a, + &mta_out.0.clone().into_owned(), + &b, + &beta_prim, + alice_public_key, + &dlog_statement, + &r, + false, + ); + assert!(bob_proof.verify( + &encrypted_a, + &mta_out.0.clone().into_owned(), + alice_public_key, + &dlog_statement, + None + )); + + // Bob follows MtAwc + let ec_gen: Point = + Point::::generator().to_point(); + let X = ec_gen * b.clone(); + let bob_proof = BobProofExt::::generate( + &encrypted_a, + &mta_out.0.clone().into_owned(), + &b, + &beta_prim, + alice_public_key, + &dlog_statement, + &r, + ); + assert!(bob_proof.verify( + &encrypted_a, + &mta_out.0.clone().into_owned(), + alice_public_key, + &dlog_statement, + &X + )); + }); + }); + } } diff --git a/fs-dkr/src/refresh_message.rs b/fs-dkr/src/refresh_message.rs index f120c2c0..15aced57 100644 --- a/fs-dkr/src/refresh_message.rs +++ b/fs-dkr/src/refresh_message.rs @@ -30,419 +30,498 @@ use crate::ring_pedersen_proof::{RingPedersenProof, RingPedersenStatement}; #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(bound = "E: Curve, H: Digest + Clone")] pub struct RefreshMessage { - pub(crate) old_party_index: u16, - pub(crate) party_index: u16, - pdl_proof_vec: Vec>, - range_proofs: Vec>, - coefficients_committed_vec: VerifiableSS, - pub(crate) points_committed_vec: Vec>, - points_encrypted_vec: Vec, - dk_correctness_proof: NiCorrectKeyProof, - pub(crate) dlog_statement: DLogStatement, - pub(crate) ek: EncryptionKey, - pub(crate) remove_party_indices: Vec, - pub(crate) public_key: Point, - pub(crate) ring_pedersen_statement: RingPedersenStatement, - pub(crate) ring_pedersen_proof: RingPedersenProof, - #[serde(skip)] - pub hash_choice: HashChoice, + pub(crate) old_party_index: u16, + pub(crate) party_index: u16, + pdl_proof_vec: Vec>, + range_proofs: Vec>, + coefficients_committed_vec: VerifiableSS, + pub(crate) points_committed_vec: Vec>, + points_encrypted_vec: Vec, + dk_correctness_proof: NiCorrectKeyProof, + pub(crate) dlog_statement: DLogStatement, + pub(crate) ek: EncryptionKey, + pub(crate) remove_party_indices: Vec, + pub(crate) public_key: Point, + pub(crate) ring_pedersen_statement: RingPedersenStatement, + pub(crate) ring_pedersen_proof: RingPedersenProof, + #[serde(skip)] + pub hash_choice: HashChoice, } impl RefreshMessage { - pub fn distribute( - old_party_index: u16, - local_key: &mut LocalKey, - new_t: u16, - new_n: u16, - ) -> FsDkrResult<(RefreshMessage, DecryptionKey)> { - assert!(new_t <= new_n / 2); - let secret = local_key.keys_linear.x_i.clone(); - // secret share old key - if new_n <= new_t { - return Err(FsDkrError::NewPartyUnassignedIndexError) - } - let (vss_scheme, secret_shares) = - VerifiableSS::::share(new_t, new_n, &secret); - - local_key.vss_scheme = vss_scheme.clone(); - - // commit to points on the polynomial - let points_committed_vec: Vec<_> = (0..secret_shares.len()) - .map(|i| Point::::generator() * &secret_shares[i].clone()) - .collect(); - - // encrypt points on the polynomial using Paillier keys - let (points_encrypted_vec, randomness_vec): (Vec<_>, Vec<_>) = (0..secret_shares.len()) - .map(|i| { - let randomness = BigInt::sample_below(&local_key.paillier_key_vec[i].n); - let ciphertext = Paillier::encrypt_with_chosen_randomness( - &local_key.paillier_key_vec[i], - RawPlaintext::from(secret_shares[i].to_bigint()), - &Randomness::from(randomness.clone()), - ) - .0 - .into_owned(); - (ciphertext, randomness) - }) - .unzip(); - - // generate PDL proofs for each {point_committed, point_encrypted} pair - let pdl_proof_vec: Vec<_> = (0..secret_shares.len()) - .map(|i| { - let witness = - PDLwSlackWitness { x: secret_shares[i].clone(), r: randomness_vec[i].clone() }; - let statement = PDLwSlackStatement { - ciphertext: points_encrypted_vec[i].clone(), - ek: local_key.paillier_key_vec[i].clone(), - Q: points_committed_vec[i].clone(), - G: Point::::generator().to_point(), - h1: local_key.h1_h2_n_tilde_vec[i].g.clone(), - h2: local_key.h1_h2_n_tilde_vec[i].ni.clone(), - N_tilde: local_key.h1_h2_n_tilde_vec[i].N.clone(), - }; - PDLwSlackProof::prove(&witness, &statement) - }) - .collect(); - - let range_proofs = (0..secret_shares.len()) - .map(|i| { - AliceProof::generate( - &secret_shares[i].to_bigint(), - &points_encrypted_vec[i], - &local_key.paillier_key_vec[i], - &local_key.h1_h2_n_tilde_vec[i], - &randomness_vec[i], - ) - }) - .collect(); - - let (ek, dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - let dk_correctness_proof = NiCorrectKeyProof::proof(&dk, None); - - let (ring_pedersen_statement, ring_pedersen_witness) = RingPedersenStatement::generate(); - - let ring_pedersen_proof = - RingPedersenProof::prove(&ring_pedersen_witness, &ring_pedersen_statement); - Ok(( - RefreshMessage { - old_party_index, - party_index: local_key.i, - pdl_proof_vec, - range_proofs, - coefficients_committed_vec: vss_scheme, - points_committed_vec, - points_encrypted_vec, - dk_correctness_proof, - dlog_statement: local_key.h1_h2_n_tilde_vec[(local_key.i - 1) as usize].clone(), - ek, - remove_party_indices: Vec::new(), - public_key: local_key.y_sum_s.clone(), - ring_pedersen_statement, - ring_pedersen_proof, - hash_choice: HashChoice::new(), - }, - dk, - )) - } - - pub fn validate_collect( - refresh_messages: &[Self], - current_t: u16, - new_n: u16, - ) -> FsDkrResult<()> { - // check we got at least current threshold t + 1 refresh messages - // (i.e a quorum of existing parties has sent refresh messages). - if refresh_messages.len() <= current_t.into() { - return Err(FsDkrError::PartiesThresholdViolation { - threshold: current_t, - refreshed_keys: refresh_messages.len(), - }) - } - - // check all vectors are of same length - let reference_len = refresh_messages[0].pdl_proof_vec.len(); - - for (k, refresh_message) in refresh_messages.iter().enumerate() { - let pdl_proof_len = refresh_message.pdl_proof_vec.len(); - let points_commited_len = refresh_message.points_committed_vec.len(); - let points_encrypted_len = refresh_message.points_encrypted_vec.len(); - - if !(pdl_proof_len == reference_len && - points_commited_len == reference_len && - points_encrypted_len == reference_len) - { - return Err(FsDkrError::SizeMismatchError { - refresh_message_index: k, - pdl_proof_len, - points_commited_len, - points_encrypted_len, - }) - } - } - - for refresh_message in refresh_messages.iter() { - for i in 0..new_n as usize { - //TODO: we should handle the case of t( - refresh_messages: &'a [Self], - party_index: u16, - parameters: &'a ShamirSecretSharing, - ek: &'a EncryptionKey, - current_t: u16, - ) -> (RawCiphertext<'a>, Vec>) { - // TODO: check we have large enough qualified set , at least t+1 - //decrypt the new share - // we first homomorphically add all ciphertext encrypted using our encryption key - let ciphertext_vec: Vec<_> = (0..refresh_messages.len()) - .map(|k| refresh_messages[k].points_encrypted_vec[(party_index - 1) as usize].clone()) - .collect(); - - let indices: Vec = (0..(current_t + 1) as usize) - .map(|i| refresh_messages[i].old_party_index - 1) - .collect(); - - // optimization - one decryption - let li_vec: Vec<_> = (0..current_t as usize + 1) - .map(|i| { - VerifiableSS::::map_share_to_new_params( - parameters.clone().borrow(), - indices[i], - &indices, - ) - }) - .collect(); - - let ciphertext_vec_at_indices_mapped: Vec<_> = (0..(current_t + 1) as usize) - .map(|i| { - Paillier::mul( - ek, - RawCiphertext::from(ciphertext_vec[i].clone()), - RawPlaintext::from(li_vec[i].to_bigint()), - ) - }) - .collect(); - - let ciphertext_sum = ciphertext_vec_at_indices_mapped - .iter() - .fold(Paillier::encrypt(ek, RawPlaintext::from(BigInt::zero())), |acc, x| { - Paillier::add(ek, acc, x.clone()) - }); - - (ciphertext_sum, li_vec) - } - - pub fn replace( - new_parties: &[JoinMessage], - key: &mut LocalKey, - old_to_new_map: &HashMap, - new_t: u16, - new_n: u16, - ) -> FsDkrResult<(Self, DecryptionKey)> { - let current_len = key.paillier_key_vec.len() as u16; - let mut paillier_key_h1_h2_n_tilde_hash_map: HashMap = - HashMap::new(); - for old_party_index in old_to_new_map.keys() { - let paillier_key = - key.paillier_key_vec.get((old_party_index - 1) as usize).unwrap().clone(); - let h1_h2_n_tilde = - key.h1_h2_n_tilde_vec.get((old_party_index - 1) as usize).unwrap().clone(); - paillier_key_h1_h2_n_tilde_hash_map.insert( - *old_to_new_map.get(old_party_index).unwrap(), - (paillier_key, h1_h2_n_tilde), - ); - } - - for new_party_index in paillier_key_h1_h2_n_tilde_hash_map.keys() { - if *new_party_index <= current_len { - key.paillier_key_vec[(new_party_index - 1) as usize] = - paillier_key_h1_h2_n_tilde_hash_map.get(new_party_index).unwrap().clone().0; - key.h1_h2_n_tilde_vec[(new_party_index - 1) as usize] = - paillier_key_h1_h2_n_tilde_hash_map.get(new_party_index).unwrap().clone().1; - } else { - key.paillier_key_vec.insert( - (new_party_index - 1) as usize, - paillier_key_h1_h2_n_tilde_hash_map.get(new_party_index).unwrap().clone().0, - ); - key.h1_h2_n_tilde_vec.insert( - (new_party_index - 1) as usize, - paillier_key_h1_h2_n_tilde_hash_map.get(new_party_index).unwrap().clone().1, - ); - } - } - - for join_message in new_parties.iter() { - let party_index = join_message.get_party_index()?; - if party_index <= current_len { - key.paillier_key_vec[(party_index - 1) as usize] = join_message.ek.clone(); - key.h1_h2_n_tilde_vec[(party_index - 1) as usize] = - join_message.dlog_statement.clone(); - } else { - key.paillier_key_vec.insert((party_index - 1) as usize, join_message.ek.clone()); - key.h1_h2_n_tilde_vec - .insert((party_index - 1) as usize, join_message.dlog_statement.clone()); - } - } - let old_party_index = key.i; - key.i = *old_to_new_map.get(&key.i).unwrap(); - key.t = new_t; - key.n = new_n; - - RefreshMessage::distribute(old_party_index, key, new_t, new_n) - } - - pub fn collect( - refresh_messages: &[Self], - local_key: &mut LocalKey, - new_dk: DecryptionKey, - join_messages: &[JoinMessage], - current_t: u16, - ) -> FsDkrResult<()> { - let new_n = refresh_messages.len() + join_messages.len(); - RefreshMessage::validate_collect(refresh_messages, current_t, new_n as u16)?; - - for refresh_message in refresh_messages.iter() { - for i in 0..(new_n as usize) { - let statement = PDLwSlackStatement { - ciphertext: refresh_message.points_encrypted_vec[i].clone(), - ek: local_key.paillier_key_vec[i].clone(), - Q: refresh_message.points_committed_vec[i].clone(), - G: Point::::generator().to_point(), - h1: local_key.h1_h2_n_tilde_vec[i].g.clone(), - h2: local_key.h1_h2_n_tilde_vec[i].ni.clone(), - N_tilde: local_key.h1_h2_n_tilde_vec[i].N.clone(), - }; - refresh_message.pdl_proof_vec[i].verify(&statement)?; - if !refresh_message.range_proofs[i].verify( - &statement.ciphertext, - &statement.ek, - &local_key.h1_h2_n_tilde_vec[i], - ) { - return Err(FsDkrError::RangeProof { party_index: i }) - } - } - } - - // Verify ring-pedersen parameters - for refresh_message in refresh_messages.iter() { - RingPedersenProof::verify( - &refresh_message.ring_pedersen_proof, - &refresh_message.ring_pedersen_statement, - )?; - } - - for join_message in join_messages.iter() { - RingPedersenProof::verify( - &join_message.ring_pedersen_proof, - &join_message.ring_pedersen_statement, - )?; - } - - let old_ek = local_key.paillier_key_vec[(local_key.i - 1) as usize].clone(); - let (cipher_text_sum, li_vec) = RefreshMessage::get_ciphertext_sum( - refresh_messages, - local_key.i, - &local_key.vss_scheme.parameters, - &old_ek, - current_t, - ); - - for refresh_message in refresh_messages.iter() { - if refresh_message - .dk_correctness_proof - .verify(&refresh_message.ek, SALT_STRING) - .is_err() - { - return Err(FsDkrError::PaillierVerificationError { - party_index: refresh_message.party_index, - }) - } - let n_length = refresh_message.ek.n.bit_length(); - if n_length > crate::PAILLIER_KEY_SIZE || n_length < crate::PAILLIER_KEY_SIZE - 1 { - return Err(FsDkrError::ModuliTooSmall { - party_index: refresh_message.party_index, - moduli_size: n_length, - }) - } - - // if the proof checks, we add the new paillier public key to the key - local_key.paillier_key_vec[(refresh_message.party_index - 1) as usize] = - refresh_message.ek.clone(); - } - - for join_message in join_messages { - let party_index = join_message.get_party_index()?; - - if join_message.dk_correctness_proof.verify(&join_message.ek, SALT_STRING).is_err() { - return Err(FsDkrError::PaillierVerificationError { party_index }) - } - - // creating an inverse dlog statement - let dlog_statement_base_h2 = DLogStatement { - N: join_message.dlog_statement.N.clone(), - g: join_message.dlog_statement.ni.clone(), - ni: join_message.dlog_statement.g.clone(), - }; - if join_message - .composite_dlog_proof_base_h1 - .verify(&join_message.dlog_statement) - .is_err() || join_message - .composite_dlog_proof_base_h2 - .verify(&dlog_statement_base_h2) - .is_err() - { - return Err(FsDkrError::DLogProofValidation { party_index }) - } - - let n_length = join_message.ek.n.bit_length(); - if n_length > crate::PAILLIER_KEY_SIZE || n_length < crate::PAILLIER_KEY_SIZE - 1 { - return Err(FsDkrError::ModuliTooSmall { - party_index: join_message.get_party_index()?, - moduli_size: n_length, - }) - } - - // if the proof checks, we add the new paillier public key to the key - local_key.paillier_key_vec[(party_index - 1) as usize] = join_message.ek.clone(); - } - - let new_share = Paillier::decrypt(&local_key.paillier_dk, cipher_text_sum).0.into_owned(); - - let new_share_fe: Scalar = Scalar::::from(&new_share); - - // zeroize the old dk key - local_key.paillier_dk.q.zeroize(); - local_key.paillier_dk.p.zeroize(); - local_key.paillier_dk = new_dk; - - // update old key and output new key - local_key.keys_linear.x_i = new_share_fe.clone(); - local_key.keys_linear.y = Point::::generator() * new_share_fe; - - // update local key list of local public keys (X_i = g^x_i is updated by adding all - // committed points to that party) - for i in 0..refresh_messages.len() + join_messages.len() { - local_key - .pk_vec - .insert(i, refresh_messages[0].points_committed_vec[i].clone() * li_vec[0].clone()); - for j in 1..current_t as usize + 1 { - local_key.pk_vec[i] = local_key.pk_vec[i].clone() + - refresh_messages[j].points_committed_vec[i].clone() * li_vec[j].clone(); - } - } - - Ok(()) - } + pub fn distribute( + old_party_index: u16, + local_key: &mut LocalKey, + new_t: u16, + new_n: u16, + ) -> FsDkrResult<(RefreshMessage, DecryptionKey)> { + assert!(new_t <= new_n / 2); + let secret = local_key.keys_linear.x_i.clone(); + // secret share old key + if new_n <= new_t { + return Err(FsDkrError::NewPartyUnassignedIndexError) + } + let (vss_scheme, secret_shares) = + VerifiableSS::::share(new_t, new_n, &secret); + + local_key.vss_scheme = vss_scheme.clone(); + + // commit to points on the polynomial + let points_committed_vec: Vec<_> = (0..secret_shares.len()) + .map(|i| Point::::generator() * &secret_shares[i].clone()) + .collect(); + + // encrypt points on the polynomial using Paillier keys + let (points_encrypted_vec, randomness_vec): (Vec<_>, Vec<_>) = (0.. + secret_shares.len()) + .map(|i| { + let randomness = + BigInt::sample_below(&local_key.paillier_key_vec[i].n); + let ciphertext = Paillier::encrypt_with_chosen_randomness( + &local_key.paillier_key_vec[i], + RawPlaintext::from(secret_shares[i].to_bigint()), + &Randomness::from(randomness.clone()), + ) + .0 + .into_owned(); + (ciphertext, randomness) + }) + .unzip(); + + // generate PDL proofs for each {point_committed, point_encrypted} pair + let pdl_proof_vec: Vec<_> = (0..secret_shares.len()) + .map(|i| { + let witness = PDLwSlackWitness { + x: secret_shares[i].clone(), + r: randomness_vec[i].clone(), + }; + let statement = PDLwSlackStatement { + ciphertext: points_encrypted_vec[i].clone(), + ek: local_key.paillier_key_vec[i].clone(), + Q: points_committed_vec[i].clone(), + G: Point::::generator().to_point(), + h1: local_key.h1_h2_n_tilde_vec[i].g.clone(), + h2: local_key.h1_h2_n_tilde_vec[i].ni.clone(), + N_tilde: local_key.h1_h2_n_tilde_vec[i].N.clone(), + }; + PDLwSlackProof::prove(&witness, &statement) + }) + .collect(); + + let range_proofs = (0..secret_shares.len()) + .map(|i| { + AliceProof::generate( + &secret_shares[i].to_bigint(), + &points_encrypted_vec[i], + &local_key.paillier_key_vec[i], + &local_key.h1_h2_n_tilde_vec[i], + &randomness_vec[i], + ) + }) + .collect(); + + let (ek, dk) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE) + .keys(); + let dk_correctness_proof = NiCorrectKeyProof::proof(&dk, None); + + let (ring_pedersen_statement, ring_pedersen_witness) = + RingPedersenStatement::generate(); + + let ring_pedersen_proof = RingPedersenProof::prove( + &ring_pedersen_witness, + &ring_pedersen_statement, + ); + Ok(( + RefreshMessage { + old_party_index, + party_index: local_key.i, + pdl_proof_vec, + range_proofs, + coefficients_committed_vec: vss_scheme, + points_committed_vec, + points_encrypted_vec, + dk_correctness_proof, + dlog_statement: local_key.h1_h2_n_tilde_vec + [(local_key.i - 1) as usize] + .clone(), + ek, + remove_party_indices: Vec::new(), + public_key: local_key.y_sum_s.clone(), + ring_pedersen_statement, + ring_pedersen_proof, + hash_choice: HashChoice::new(), + }, + dk, + )) + } + + pub fn validate_collect( + refresh_messages: &[Self], + current_t: u16, + new_n: u16, + ) -> FsDkrResult<()> { + // check we got at least current threshold t + 1 refresh messages + // (i.e a quorum of existing parties has sent refresh messages). + if refresh_messages.len() <= current_t.into() { + return Err(FsDkrError::PartiesThresholdViolation { + threshold: current_t, + refreshed_keys: refresh_messages.len(), + }) + } + + // check all vectors are of same length + let reference_len = refresh_messages[0].pdl_proof_vec.len(); + + for (k, refresh_message) in refresh_messages.iter().enumerate() { + let pdl_proof_len = refresh_message.pdl_proof_vec.len(); + let points_commited_len = + refresh_message.points_committed_vec.len(); + let points_encrypted_len = + refresh_message.points_encrypted_vec.len(); + + if !(pdl_proof_len == reference_len && + points_commited_len == reference_len && + points_encrypted_len == reference_len) + { + return Err(FsDkrError::SizeMismatchError { + refresh_message_index: k, + pdl_proof_len, + points_commited_len, + points_encrypted_len, + }) + } + } + + for refresh_message in refresh_messages.iter() { + for i in 0..new_n as usize { + //TODO: we should handle the case of t( + refresh_messages: &'a [Self], + party_index: u16, + parameters: &'a ShamirSecretSharing, + ek: &'a EncryptionKey, + current_t: u16, + ) -> (RawCiphertext<'a>, Vec>) { + // TODO: check we have large enough qualified set , at least t+1 + //decrypt the new share + // we first homomorphically add all ciphertext encrypted using our + // encryption key + let ciphertext_vec: Vec<_> = (0..refresh_messages.len()) + .map(|k| { + refresh_messages[k].points_encrypted_vec + [(party_index - 1) as usize] + .clone() + }) + .collect(); + + let indices: Vec = (0..(current_t + 1) as usize) + .map(|i| refresh_messages[i].old_party_index - 1) + .collect(); + + // optimization - one decryption + let li_vec: Vec<_> = (0..current_t as usize + 1) + .map(|i| { + VerifiableSS::::map_share_to_new_params( + parameters.clone().borrow(), + indices[i], + &indices, + ) + }) + .collect(); + + let ciphertext_vec_at_indices_mapped: Vec<_> = (0..(current_t + 1) + as usize) + .map(|i| { + Paillier::mul( + ek, + RawCiphertext::from(ciphertext_vec[i].clone()), + RawPlaintext::from(li_vec[i].to_bigint()), + ) + }) + .collect(); + + let ciphertext_sum = ciphertext_vec_at_indices_mapped.iter().fold( + Paillier::encrypt(ek, RawPlaintext::from(BigInt::zero())), + |acc, x| Paillier::add(ek, acc, x.clone()), + ); + + (ciphertext_sum, li_vec) + } + + pub fn replace( + new_parties: &[JoinMessage], + key: &mut LocalKey, + old_to_new_map: &HashMap, + new_t: u16, + new_n: u16, + ) -> FsDkrResult<(Self, DecryptionKey)> { + let current_len = key.paillier_key_vec.len() as u16; + let mut paillier_key_h1_h2_n_tilde_hash_map: HashMap< + u16, + (EncryptionKey, DLogStatement), + > = HashMap::new(); + for old_party_index in old_to_new_map.keys() { + let paillier_key = key + .paillier_key_vec + .get((old_party_index - 1) as usize) + .unwrap() + .clone(); + let h1_h2_n_tilde = key + .h1_h2_n_tilde_vec + .get((old_party_index - 1) as usize) + .unwrap() + .clone(); + paillier_key_h1_h2_n_tilde_hash_map.insert( + *old_to_new_map.get(old_party_index).unwrap(), + (paillier_key, h1_h2_n_tilde), + ); + } + + for new_party_index in paillier_key_h1_h2_n_tilde_hash_map.keys() { + if *new_party_index <= current_len { + key.paillier_key_vec[(new_party_index - 1) as usize] = + paillier_key_h1_h2_n_tilde_hash_map + .get(new_party_index) + .unwrap() + .clone() + .0; + key.h1_h2_n_tilde_vec[(new_party_index - 1) as usize] = + paillier_key_h1_h2_n_tilde_hash_map + .get(new_party_index) + .unwrap() + .clone() + .1; + } else { + key.paillier_key_vec.insert( + (new_party_index - 1) as usize, + paillier_key_h1_h2_n_tilde_hash_map + .get(new_party_index) + .unwrap() + .clone() + .0, + ); + key.h1_h2_n_tilde_vec.insert( + (new_party_index - 1) as usize, + paillier_key_h1_h2_n_tilde_hash_map + .get(new_party_index) + .unwrap() + .clone() + .1, + ); + } + } + + for join_message in new_parties.iter() { + let party_index = join_message.get_party_index()?; + if party_index <= current_len { + key.paillier_key_vec[(party_index - 1) as usize] = + join_message.ek.clone(); + key.h1_h2_n_tilde_vec[(party_index - 1) as usize] = + join_message.dlog_statement.clone(); + } else { + key.paillier_key_vec.insert( + (party_index - 1) as usize, + join_message.ek.clone(), + ); + key.h1_h2_n_tilde_vec.insert( + (party_index - 1) as usize, + join_message.dlog_statement.clone(), + ); + } + } + let old_party_index = key.i; + key.i = *old_to_new_map.get(&key.i).unwrap(); + key.t = new_t; + key.n = new_n; + + RefreshMessage::distribute(old_party_index, key, new_t, new_n) + } + + pub fn collect( + refresh_messages: &[Self], + local_key: &mut LocalKey, + new_dk: DecryptionKey, + join_messages: &[JoinMessage], + current_t: u16, + ) -> FsDkrResult<()> { + let new_n = refresh_messages.len() + join_messages.len(); + RefreshMessage::validate_collect( + refresh_messages, + current_t, + new_n as u16, + )?; + + for refresh_message in refresh_messages.iter() { + for i in 0..new_n { + let statement = PDLwSlackStatement { + ciphertext: refresh_message.points_encrypted_vec[i].clone(), + ek: local_key.paillier_key_vec[i].clone(), + Q: refresh_message.points_committed_vec[i].clone(), + G: Point::::generator().to_point(), + h1: local_key.h1_h2_n_tilde_vec[i].g.clone(), + h2: local_key.h1_h2_n_tilde_vec[i].ni.clone(), + N_tilde: local_key.h1_h2_n_tilde_vec[i].N.clone(), + }; + refresh_message.pdl_proof_vec[i].verify(&statement)?; + if !refresh_message.range_proofs[i].verify( + &statement.ciphertext, + &statement.ek, + &local_key.h1_h2_n_tilde_vec[i], + ) { + return Err(FsDkrError::RangeProof { party_index: i }) + } + } + } + + // Verify ring-pedersen parameters + for refresh_message in refresh_messages.iter() { + RingPedersenProof::verify( + &refresh_message.ring_pedersen_proof, + &refresh_message.ring_pedersen_statement, + )?; + } + + for join_message in join_messages.iter() { + RingPedersenProof::verify( + &join_message.ring_pedersen_proof, + &join_message.ring_pedersen_statement, + )?; + } + + let old_ek = + local_key.paillier_key_vec[(local_key.i - 1) as usize].clone(); + let (cipher_text_sum, li_vec) = RefreshMessage::get_ciphertext_sum( + refresh_messages, + local_key.i, + &local_key.vss_scheme.parameters, + &old_ek, + current_t, + ); + + for refresh_message in refresh_messages.iter() { + if refresh_message + .dk_correctness_proof + .verify(&refresh_message.ek, SALT_STRING) + .is_err() + { + return Err(FsDkrError::PaillierVerificationError { + party_index: refresh_message.party_index, + }) + } + let n_length = refresh_message.ek.n.bit_length(); + if !(crate::PAILLIER_KEY_SIZE - 1..=crate::PAILLIER_KEY_SIZE) + .contains(&n_length) + { + return Err(FsDkrError::ModuliTooSmall { + party_index: refresh_message.party_index, + moduli_size: n_length, + }) + } + + // if the proof checks, we add the new paillier public key to the + // key + local_key.paillier_key_vec + [(refresh_message.party_index - 1) as usize] = + refresh_message.ek.clone(); + } + + for join_message in join_messages { + let party_index = join_message.get_party_index()?; + + if join_message + .dk_correctness_proof + .verify(&join_message.ek, SALT_STRING) + .is_err() + { + return Err(FsDkrError::PaillierVerificationError { + party_index, + }) + } + + // creating an inverse dlog statement + let dlog_statement_base_h2 = DLogStatement { + N: join_message.dlog_statement.N.clone(), + g: join_message.dlog_statement.ni.clone(), + ni: join_message.dlog_statement.g.clone(), + }; + if join_message + .composite_dlog_proof_base_h1 + .verify(&join_message.dlog_statement) + .is_err() || + join_message + .composite_dlog_proof_base_h2 + .verify(&dlog_statement_base_h2) + .is_err() + { + return Err(FsDkrError::DLogProofValidation { party_index }) + } + + let n_length = join_message.ek.n.bit_length(); + if !(crate::PAILLIER_KEY_SIZE - 1..=crate::PAILLIER_KEY_SIZE) + .contains(&n_length) + { + //if n_length > crate::PAILLIER_KEY_SIZE || n_length < + // crate::PAILLIER_KEY_SIZE - 1 { + return Err(FsDkrError::ModuliTooSmall { + party_index: join_message.get_party_index()?, + moduli_size: n_length, + }) + } + + // if the proof checks, we add the new paillier public key to the + // key + local_key.paillier_key_vec[(party_index - 1) as usize] = + join_message.ek.clone(); + } + + let new_share = + Paillier::decrypt(&local_key.paillier_dk, cipher_text_sum) + .0 + .into_owned(); + + let new_share_fe: Scalar = Scalar::::from(&new_share); + + // zeroize the old dk key + local_key.paillier_dk.q.zeroize(); + local_key.paillier_dk.p.zeroize(); + local_key.paillier_dk = new_dk; + + // update old key and output new key + local_key.keys_linear.x_i = new_share_fe.clone(); + local_key.keys_linear.y = Point::::generator() * new_share_fe; + + // update local key list of local public keys (X_i = g^x_i is updated by + // adding all committed points to that party) + for i in 0..refresh_messages.len() + join_messages.len() { + local_key.pk_vec.insert( + i, + refresh_messages[0].points_committed_vec[i].clone() * + li_vec[0].clone(), + ); + for j in 1..current_t as usize + 1 { + local_key.pk_vec[i] = local_key.pk_vec[i].clone() + + refresh_messages[j].points_committed_vec[i].clone() * + li_vec[j].clone(); + } + } + + Ok(()) + } } diff --git a/fs-dkr/src/ring_pedersen_proof.rs b/fs-dkr/src/ring_pedersen_proof.rs index cd21ac7b..013f118d 100644 --- a/fs-dkr/src/ring_pedersen_proof.rs +++ b/fs-dkr/src/ring_pedersen_proof.rs @@ -1,22 +1,22 @@ #![allow(non_snake_case)] /* - Ring Pedersen Proof - Copyright 2022 by Webb Technologies. - - ring_pedersen_proof.rs is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + Ring Pedersen Proof + Copyright 2022 by Webb Technologies. + + ring_pedersen_proof.rs is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. + @license GPL-3.0+ */ use bitvec::prelude::*; use curv::{ - arithmetic::traits::*, - cryptographic_primitives::hashing::{Digest, DigestExt}, - elliptic::curves::Curve, - BigInt, + arithmetic::traits::*, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::Curve, + BigInt, }; use paillier::{EncryptionKey, KeyGeneration, Paillier}; use serde::{Deserialize, Serialize}; @@ -27,129 +27,152 @@ use crate::error::{FsDkrError, FsDkrResult}; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(bound = "E: Curve, H: Digest + Clone")] pub struct RingPedersenStatement { - pub S: BigInt, - pub T: BigInt, - pub N: BigInt, - phi: BigInt, - pub ek: EncryptionKey, - #[serde(skip)] - phantom: PhantomData<(E, H)>, + pub S: BigInt, + pub T: BigInt, + pub N: BigInt, + phi: BigInt, + pub ek: EncryptionKey, + #[serde(skip)] + phantom: PhantomData<(E, H)>, } pub struct RingPedersenWitness { - p: BigInt, - q: BigInt, - lambda: BigInt, - phantom: PhantomData<(E, H)>, + p: BigInt, + q: BigInt, + lambda: BigInt, + phantom: PhantomData<(E, H)>, } impl RingPedersenStatement { - pub fn generate() -> (Self, RingPedersenWitness) { - let (ek_tilde, dk_tilde) = - Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let r = BigInt::sample_below(&ek_tilde.n); - let lambda = BigInt::sample_below(&phi); - let t = BigInt::mod_pow(&r, &BigInt::from(2), &ek_tilde.n); - let s = BigInt::mod_pow(&t, &lambda, &ek_tilde.n); - - ( - Self { S: s, T: t, N: ek_tilde.clone().n, phi, ek: ek_tilde, phantom: PhantomData }, - RingPedersenWitness { p: dk_tilde.p, q: dk_tilde.q, lambda, phantom: PhantomData }, - ) - } + pub fn generate() -> (Self, RingPedersenWitness) { + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE) + .keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let r = BigInt::sample_below(&ek_tilde.n); + let lambda = BigInt::sample_below(&phi); + let t = BigInt::mod_pow(&r, &BigInt::from(2), &ek_tilde.n); + let s = BigInt::mod_pow(&t, &lambda, &ek_tilde.n); + + ( + Self { + S: s, + T: t, + N: ek_tilde.clone().n, + phi, + ek: ek_tilde, + phantom: PhantomData, + }, + RingPedersenWitness { + p: dk_tilde.p, + q: dk_tilde.q, + lambda, + phantom: PhantomData, + }, + ) + } } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(bound = "E: Curve, H: Digest + Clone")] pub struct RingPedersenProof { - A: Vec, - Z: Vec, - #[serde(skip)] - phantom: PhantomData<(E, H)>, + A: Vec, + Z: Vec, + #[serde(skip)] + phantom: PhantomData<(E, H)>, } // Link to the UC non-interactive threshold ECDSA paper impl RingPedersenProof { - pub fn prove( - witness: &RingPedersenWitness, - statement: &RingPedersenStatement, - ) -> RingPedersenProof { - // 1. Sample alphas from 1 -> m from \phi(N) - let mut a = [(); M].map(|_| BigInt::zero()); - let mut A = [(); M].map(|_| BigInt::zero()); - let mut hash = H::new(); - for i in 0..M { - // TODO: Consider ensuring we get a unit element of this subgroup - let a_i = BigInt::sample_below(&statement.phi); - a[i] = a_i.clone(); - let A_i = BigInt::mod_pow(&statement.T, &a_i, &statement.N); - A[i] = A_i.clone(); - hash = H::chain_bigint(hash, &A_i); - } - - let e: BigInt = hash.result_bigint(); - let bitwise_e: BitVec = BitVec::from_vec(e.to_bytes()); - - let mut Z = [(); M].map(|_| BigInt::zero()); - for i in 0..M { - let e_i = if bitwise_e[i] { BigInt::one() } else { BigInt::zero() }; - let z_i = BigInt::mod_add(&a[i], &(e_i * &witness.lambda), &statement.phi); - Z[i] = z_i; - } - - Self { A: A.to_vec(), Z: Z.to_vec(), phantom: PhantomData } - } - - pub fn verify( - proof: &RingPedersenProof, - statement: &RingPedersenStatement, - ) -> FsDkrResult<()> { - let mut hash = H::new(); - for i in 0..M { - hash = H::chain_bigint(hash, &proof.A[i]); - } - - let e: BigInt = hash.result_bigint(); - let bitwise_e: BitVec = BitVec::from_vec(e.to_bytes()); - - for i in 0..M { - let mut e_i = 0; - if bitwise_e[i] { - e_i = 1; - } - - if BigInt::mod_pow(&statement.T, &proof.Z[i], &statement.N) == - BigInt::mod_mul( - &proof.A[i], - &BigInt::mod_pow(&statement.S, &BigInt::from(e_i), &statement.N), - &statement.N, - ) { - continue - } else { - return Err(FsDkrError::RingPedersenProofError) - } - } - Ok(()) - } + pub fn prove( + witness: &RingPedersenWitness, + statement: &RingPedersenStatement, + ) -> RingPedersenProof { + // 1. Sample alphas from 1 -> m from \phi(N) + let mut a = [(); M].map(|_| BigInt::zero()); + let mut A = [(); M].map(|_| BigInt::zero()); + let mut hash = H::new(); + for i in 0..M { + // TODO: Consider ensuring we get a unit element of this subgroup + let a_i = BigInt::sample_below(&statement.phi); + a[i] = a_i.clone(); + let A_i = BigInt::mod_pow(&statement.T, &a_i, &statement.N); + A[i] = A_i.clone(); + hash = H::chain_bigint(hash, &A_i); + } + + let e: BigInt = hash.result_bigint(); + let bitwise_e: BitVec = BitVec::from_vec(e.to_bytes()); + + let mut Z = [(); M].map(|_| BigInt::zero()); + for i in 0..M { + let e_i = if bitwise_e[i] { BigInt::one() } else { BigInt::zero() }; + let z_i = BigInt::mod_add( + &a[i], + &(e_i * &witness.lambda), + &statement.phi, + ); + Z[i] = z_i; + } + + Self { A: A.to_vec(), Z: Z.to_vec(), phantom: PhantomData } + } + + pub fn verify( + proof: &RingPedersenProof, + statement: &RingPedersenStatement, + ) -> FsDkrResult<()> { + let mut hash = H::new(); + for i in 0..M { + hash = H::chain_bigint(hash, &proof.A[i]); + } + + let e: BigInt = hash.result_bigint(); + let bitwise_e: BitVec = BitVec::from_vec(e.to_bytes()); + + for i in 0..M { + let mut e_i = 0; + if bitwise_e[i] { + e_i = 1; + } + + if BigInt::mod_pow(&statement.T, &proof.Z[i], &statement.N) == + BigInt::mod_mul( + &proof.A[i], + &BigInt::mod_pow( + &statement.S, + &BigInt::from(e_i), + &statement.N, + ), + &statement.N, + ) + { + continue + } else { + return Err(FsDkrError::RingPedersenProofError) + } + } + Ok(()) + } } #[cfg(test)] mod tests { - use super::*; - use curv::elliptic::curves::secp256_k1::Secp256k1; - use sha2::Sha256; - - #[test] - fn test_ring_pedersen() { - let (statement, witness) = RingPedersenStatement::::generate(); - let proof = RingPedersenProof::::prove( + use super::*; + use curv::elliptic::curves::secp256_k1::Secp256k1; + use sha2::Sha256; + + #[test] + fn test_ring_pedersen() { + let (statement, witness) = + RingPedersenStatement::::generate(); + let proof = RingPedersenProof::::prove( &witness, &statement, ); - assert!(RingPedersenProof::::verify( + assert!(RingPedersenProof::::verify( &proof, &statement ) .is_ok()); - } + } } diff --git a/fs-dkr/src/test.rs b/fs-dkr/src/test.rs index add0a629..ab0e5568 100644 --- a/fs-dkr/src/test.rs +++ b/fs-dkr/src/test.rs @@ -1,396 +1,474 @@ #[cfg(test)] mod tests { - use crate::refresh_message::RefreshMessage; - use curv::{ - arithmetic::Converter, - cryptographic_primitives::secret_sharing::feldman_vss::{ - ShamirSecretSharing, VerifiableSS, - }, - elliptic::curves::{secp256_k1::Secp256k1Point, Secp256k1}, - BigInt, - }; - use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::{ - party_i::{verify, Keys}, - state_machine::{ - keygen::{Keygen, LocalKey}, - sign::{CompletedOfflineStage, OfflineStage, SignManual}, - }, - }; - use sha2::Sha256; - - use crate::{add_party_message::JoinMessage, error::FsDkrResult}; - use curv::{ - cryptographic_primitives::{hashing::Digest, proofs::sigma_dlog::DLogProof}, - elliptic::curves::Scalar, - }; - use paillier::DecryptionKey; - use round_based::dev::Simulation; - use std::collections::HashMap; - - type GE = Secp256k1Point; - - #[test] - fn test1() { - //simulate keygen - let t = 3; - let n = 6; - let mut keys = simulate_keygen(t, n); - - let old_keys = keys.clone(); - simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); - - // check that sum of old keys is equal to sum of new keys - let old_linear_secret_key: Vec<_> = - (0..old_keys.len()).map(|i| old_keys[i].keys_linear.x_i.clone()).collect(); - - let new_linear_secret_key: Vec<_> = - (0..keys.len()).map(|i| keys[i].keys_linear.x_i.clone()).collect(); - let indices: Vec<_> = (0..(t + 1) as u16).collect(); - let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, - commitments: Vec::new(), - proof: DLogProof::::prove(&Scalar::random()), - }; - assert_eq!( - vss.reconstruct(&indices[..], &old_linear_secret_key[0..(t + 1) as usize]), - vss.reconstruct(&indices[..], &new_linear_secret_key[0..(t + 1) as usize]) - ); - assert_ne!(old_linear_secret_key, new_linear_secret_key); - } - - #[test] - fn test_sign_rotate_sign() { - let mut keys = simulate_keygen(2, 5); - let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); - simulate_signing(offline_sign, b"ZenGo"); - simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); - let offline_sign = simulate_offline_stage(keys.clone(), &[2, 3, 4]); - simulate_signing(offline_sign, b"ZenGo"); - simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); - let offline_sign = simulate_offline_stage(keys, &[1, 3, 5]); - simulate_signing(offline_sign, b"ZenGo"); - } - - #[test] - fn test_remove_sign_rotate_sign() { - let mut keys = simulate_keygen(2, 5); - let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); - simulate_signing(offline_sign, b"ZenGo"); - simulate_dkr_removal::<{ crate::M_SECURITY }>(&mut keys, [1].to_vec(), None); - let offline_sign = simulate_offline_stage(keys.clone(), &[2, 3, 4]); - simulate_signing(offline_sign, b"ZenGo"); - simulate_dkr_removal::<{ crate::M_SECURITY }>(&mut keys, [1, 2].to_vec(), None); - let offline_sign = simulate_offline_stage(keys, &[3, 4, 5]); - simulate_signing(offline_sign, b"ZenGo"); - } - - #[test] - fn test_add_party_with_permute() { - fn simulate_replace( - keys: &mut Vec>, - party_indices: &[u16], - old_to_new_map: &HashMap, - t: u16, - n: u16, - ) -> FsDkrResult<()> { - fn generate_join_messages_and_keys( - number_of_new_parties: usize, - ) -> (Vec>, Vec) { - // the new party generates it's join message to start joining the computation - (0..number_of_new_parties).map(|_| JoinMessage::distribute()).unzip() - } - - fn generate_refresh_parties_replace( - keys: &mut [LocalKey], - old_to_new_map: &HashMap, - join_messages: &[JoinMessage], - ) -> (Vec>, Vec) { - let new_n = (&keys.len() + join_messages.len()) as u16; - keys.iter_mut() - .map(|key| { - RefreshMessage::replace(join_messages, key, old_to_new_map, key.t, new_n) - .unwrap() - }) - .unzip() - } - - // each party that wants to join generates a join message and a pair of paillier keys. - let (mut join_messages, new_keys) = - generate_join_messages_and_keys::<{ crate::M_SECURITY }>(party_indices.len()); - - // each new party has to be informed through offchannel communication what party index - // it has been assigned (the information is public). - for (join_message, party_index) in join_messages.iter_mut().zip(party_indices) { - join_message.party_index = Some(*party_index); - } - - // each existing party has to generate it's refresh message aware of the new parties - let (refresh_messages, dk_keys) = - generate_refresh_parties_replace(keys, &old_to_new_map, join_messages.as_slice()); - let mut new_keys_vec: Vec<(u16, LocalKey)> = - Vec::with_capacity(keys.len() + join_messages.len()); - // all existing parties rotate aware of the join_messages - for i in 0..keys.len() as usize { - RefreshMessage::collect( - refresh_messages.as_slice(), - &mut keys[i], - dk_keys[i].clone(), - join_messages.as_slice(), - t, - ) - .expect(""); - new_keys_vec.push((keys[i].i - 1, keys[i].clone())); - } - - // all new parties generate a local key - for (join_message, dk) in join_messages.iter().zip(new_keys) { - let party_index = join_message.party_index.unwrap(); - let local_key = join_message.collect( - refresh_messages.as_slice(), - dk, - join_messages.as_slice(), - t, - n, - t, - )?; - - new_keys_vec.push((party_index - 1, local_key)); - } - - new_keys_vec.sort_by(|a, b| a.0.cmp(&b.0)); - let keys_replacements = - new_keys_vec.iter().map(|a| a.1.clone()).collect::>>(); - *keys = keys_replacements; - Ok(()) - } - - let t = 2; - let n = 7; - - let all_keys = simulate_keygen(t, n); - // Remove the 2nd and 7th party - let mut keys = all_keys.clone(); - keys.remove(6); - keys.remove(1); - - let mut old_to_new_map: HashMap = HashMap::new(); - old_to_new_map.insert(1, 4); - old_to_new_map.insert(3, 1); - old_to_new_map.insert(4, 3); - old_to_new_map.insert(5, 6); - old_to_new_map.insert(6, 5); - - // Simulate the replace - simulate_replace::<{ crate::M_SECURITY }>(&mut keys, &[2, 7], &old_to_new_map, t, n) - .unwrap(); - // check that sum of old keys is equal to sum of new keys - let old_linear_secret_key: Vec<_> = - (0..all_keys.len()).map(|i| all_keys[i].keys_linear.x_i.clone()).collect(); - - let new_linear_secret_key: Vec<_> = - (0..keys.len()).map(|i| keys[i].keys_linear.x_i.clone()).collect(); - let indices: Vec<_> = (0..(t + 1) as u16).collect(); - let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, - commitments: Vec::new(), - proof: DLogProof::::prove(&Scalar::random()), - }; - assert_eq!( - vss.reconstruct(&indices[..], &old_linear_secret_key[0..(t + 1) as usize]), - vss.reconstruct(&indices[..], &new_linear_secret_key[0..(t + 1) as usize]) - ); - assert_ne!(old_linear_secret_key, new_linear_secret_key); - - let offline_sign = simulate_offline_stage(keys, &[1, 2, 7]); - simulate_signing(offline_sign, b"ZenGo"); - } - - #[test] - fn test_change_threshold_sign_rotate_sign() { - let mut keys = simulate_keygen(2, 5); - let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); - simulate_signing(offline_sign, b"ZenGo"); - // Change threshold to 1 (i.e quorum size = 2). - simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, Some(1)); - let offline_sign = simulate_offline_stage(keys.clone(), &[3, 4]); - simulate_signing(offline_sign, b"ZenGo"); - // Change threshold to back to 2 (i.e quorum size = 3). - simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, Some(2)); - let offline_sign = simulate_offline_stage(keys, &[1, 3, 5]); - simulate_signing(offline_sign, b"ZenGo"); - } - - fn simulate_keygen(t: u16, n: u16) -> Vec> { - //simulate keygen - let mut simulation = Simulation::new(); - simulation.enable_benchmarks(false); - - for i in 1..=n { - simulation.add_party(Keygen::new(i, t, n).unwrap()); - } - - simulation.run().unwrap() - } - - fn simulate_dkr_removal( - keys: &mut Vec>, - remove_party_indices: Vec, - new_t_option: Option, - ) { - let mut broadcast_messages: HashMap>> = - HashMap::new(); - let mut new_dks: HashMap = HashMap::new(); - let mut refresh_messages: Vec> = Vec::new(); - let mut party_key: HashMap> = HashMap::new(); - // TODO: Verify this is correct - let new_n = keys.len() as u16; - let current_t = keys[0].t; - let new_t = new_t_option.unwrap_or(current_t); - for key in keys.iter_mut() { - let (refresh_message, new_dk) = - RefreshMessage::distribute(key.i, key, new_t, new_n).unwrap(); - refresh_messages.push(refresh_message.clone()); - new_dks.insert(refresh_message.party_index.into(), new_dk); - party_key.insert(refresh_message.party_index.into(), key.clone()); - } - - for refresh_message in refresh_messages.iter() { - broadcast_messages.insert(refresh_message.party_index.into(), Vec::new()); - } - - for refresh_message in refresh_messages.iter_mut() { - if !remove_party_indices.contains(&refresh_message.party_index.into()) { - refresh_message.remove_party_indices = remove_party_indices.clone(); - } else { - let mut new_remove_party_indices = remove_party_indices.clone(); - new_remove_party_indices - .retain(|value| *value != refresh_message.party_index.into()); - refresh_message.remove_party_indices = new_remove_party_indices; - } - - for (party_index, refresh_bucket) in broadcast_messages.iter_mut() { - if refresh_message.remove_party_indices.contains(&(*party_index as u16)) { - continue - } - refresh_bucket.push(refresh_message.clone()); - } - } - - for remove_party_index in remove_party_indices.iter() { - assert_eq!(broadcast_messages[&(*remove_party_index as usize)].len(), 1); - } - - // keys will be updated to refreshed values - for (party, key) in party_key.iter_mut() { - if remove_party_indices.contains(&(*party as u16)) { - continue - } - - RefreshMessage::collect( - broadcast_messages[party].clone().as_slice(), - key, - new_dks[party].clone(), - &[], - current_t, - ) - .expect(""); - } - - for remove_party_index in remove_party_indices { - let result = RefreshMessage::collect( - &broadcast_messages[&(remove_party_index as usize)], - &mut keys[remove_party_index as usize], - new_dks[&(remove_party_index as usize)].clone(), - &[], - current_t, - ); - assert!(result.is_err()); - } - } - - fn simulate_dkr( - keys: &mut Vec>, - new_t_option: Option, - ) -> (Vec>, Vec) { - let mut broadcast_vec: Vec> = Vec::new(); - let mut new_dks: Vec = Vec::new(); - let keys_len = keys.len(); - let current_t = keys[0].t; - let new_t = new_t_option.unwrap_or(current_t); - for key in keys.iter_mut() { - let (refresh_message, new_dk) = - RefreshMessage::distribute(key.i, key, new_t, keys_len as u16).unwrap(); - broadcast_vec.push(refresh_message); - new_dks.push(new_dk); - } - - // keys will be updated to refreshed values - for i in 0..keys.len() as usize { - RefreshMessage::collect( - &broadcast_vec, - &mut keys[i], - new_dks[i].clone(), - &[], - current_t, - ) - .expect(""); - } - - (broadcast_vec, new_dks) - } - - fn simulate_offline_stage( - local_keys: Vec>, - s_l: &[u16], - ) -> Vec { - let mut simulation = Simulation::new(); - simulation.enable_benchmarks(false); - - for (i, &keygen_i) in (1..).zip(s_l) { - simulation.add_party( - OfflineStage::new(i, s_l.to_vec(), local_keys[usize::from(keygen_i - 1)].clone()) - .unwrap(), - ); - } - - simulation.run().unwrap() - } - - fn simulate_signing(offline: Vec, message: &[u8]) { - let message = create_hash(&[&BigInt::from_bytes(message)]); - let pk = &offline[0].public_key(); - - let parties = offline - .iter() - .map(|o| SignManual::new(message.clone(), o.clone())) - .collect::, _>>() - .unwrap(); - let (parties, local_sigs): (Vec<_>, Vec<_>) = parties.into_iter().unzip(); - // parties.remove(0).complete(&local_sigs[1..]).unwrap(); - let local_sigs_except = |i: usize| { - let mut v = vec![]; - v.extend_from_slice(&local_sigs[..i]); - if i + 1 < local_sigs.len() { - v.extend_from_slice(&local_sigs[i + 1..]); - } - v - }; - - assert!(parties - .into_iter() - .enumerate() - .map(|(i, p)| p.complete(&local_sigs_except(i)).unwrap()) - .all(|signature| verify(&signature, &pk, &message).is_ok())); - } - - fn create_hash(big_ints: &[&BigInt]) -> BigInt { - let hasher = Sha256::new(); - - for value in big_ints { - hasher.clone().chain(&BigInt::to_bytes(value)); - } - - let result_hex = hasher.finalize(); - BigInt::from_bytes(&result_hex[..]) - } + use crate::refresh_message::RefreshMessage; + use curv::{ + arithmetic::Converter, + cryptographic_primitives::secret_sharing::feldman_vss::{ + ShamirSecretSharing, VerifiableSS, + }, + elliptic::curves::{secp256_k1::Secp256k1Point, Secp256k1}, + BigInt, + }; + use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::{ + party_i::{verify, Keys}, + state_machine::{ + keygen::{Keygen, LocalKey}, + sign::{CompletedOfflineStage, OfflineStage, SignManual}, + }, + }; + use sha2::Sha256; + + use crate::{add_party_message::JoinMessage, error::FsDkrResult}; + use curv::{ + cryptographic_primitives::{ + hashing::Digest, proofs::sigma_dlog::DLogProof, + }, + elliptic::curves::Scalar, + }; + use paillier::DecryptionKey; + use round_based::dev::Simulation; + use std::collections::HashMap; + + type GE = Secp256k1Point; + + #[test] + fn test1() { + //simulate keygen + let t = 3; + let n = 6; + let mut keys = simulate_keygen(t, n); + + let old_keys = keys.clone(); + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); + + // check that sum of old keys is equal to sum of new keys + let old_linear_secret_key: Vec<_> = (0..old_keys.len()) + .map(|i| old_keys[i].keys_linear.x_i.clone()) + .collect(); + + let new_linear_secret_key: Vec<_> = + (0..keys.len()).map(|i| keys[i].keys_linear.x_i.clone()).collect(); + let indices: Vec<_> = (0..(t + 1) as u16).collect(); + let vss = VerifiableSS:: { + parameters: ShamirSecretSharing { threshold: t, share_count: n }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random(), + ), + }; + assert_eq!( + vss.reconstruct( + &indices[..], + &old_linear_secret_key[0..(t + 1) as usize] + ), + vss.reconstruct( + &indices[..], + &new_linear_secret_key[0..(t + 1) as usize] + ) + ); + assert_ne!(old_linear_secret_key, new_linear_secret_key); + } + + #[test] + fn test_sign_rotate_sign() { + let mut keys = simulate_keygen(2, 5); + let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); + let offline_sign = simulate_offline_stage(keys.clone(), &[2, 3, 4]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, None); + let offline_sign = simulate_offline_stage(keys, &[1, 3, 5]); + simulate_signing(offline_sign, b"ZenGo"); + } + + #[test] + fn test_remove_sign_rotate_sign() { + let mut keys = simulate_keygen(2, 5); + let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr_removal::<{ crate::M_SECURITY }>( + &mut keys, + [1].to_vec(), + None, + ); + let offline_sign = simulate_offline_stage(keys.clone(), &[2, 3, 4]); + simulate_signing(offline_sign, b"ZenGo"); + simulate_dkr_removal::<{ crate::M_SECURITY }>( + &mut keys, + [1, 2].to_vec(), + None, + ); + let offline_sign = simulate_offline_stage(keys, &[3, 4, 5]); + simulate_signing(offline_sign, b"ZenGo"); + } + + #[test] + fn test_add_party_with_permute() { + fn simulate_replace( + keys: &mut Vec>, + party_indices: &[u16], + old_to_new_map: &HashMap, + t: u16, + n: u16, + ) -> FsDkrResult<()> { + fn generate_join_messages_and_keys( + number_of_new_parties: usize, + ) -> (Vec>, Vec) + { + // the new party generates it's join message to start joining + // the computation + (0..number_of_new_parties) + .map(|_| JoinMessage::distribute()) + .unzip() + } + + fn generate_refresh_parties_replace( + keys: &mut [LocalKey], + old_to_new_map: &HashMap, + join_messages: &[JoinMessage], + ) -> (Vec>, Vec) + { + let new_n = (&keys.len() + join_messages.len()) as u16; + keys.iter_mut() + .map(|key| { + RefreshMessage::replace( + join_messages, + key, + old_to_new_map, + key.t, + new_n, + ) + .unwrap() + }) + .unzip() + } + + // each party that wants to join generates a join message and a pair + // of paillier keys. + let (mut join_messages, new_keys) = + generate_join_messages_and_keys::<{ crate::M_SECURITY }>( + party_indices.len(), + ); + + // each new party has to be informed through offchannel + // communication what party index it has been assigned + // (the information is public). + for (join_message, party_index) in + join_messages.iter_mut().zip(party_indices) + { + join_message.party_index = Some(*party_index); + } + + // each existing party has to generate it's refresh message aware of + // the new parties + let (refresh_messages, dk_keys) = generate_refresh_parties_replace( + keys, + &old_to_new_map, + join_messages.as_slice(), + ); + let mut new_keys_vec: Vec<(u16, LocalKey)> = + Vec::with_capacity(keys.len() + join_messages.len()); + // all existing parties rotate aware of the join_messages + for i in 0..keys.len() as usize { + RefreshMessage::collect( + refresh_messages.as_slice(), + &mut keys[i], + dk_keys[i].clone(), + join_messages.as_slice(), + t, + ) + .expect(""); + new_keys_vec.push((keys[i].i - 1, keys[i].clone())); + } + + // all new parties generate a local key + for (join_message, dk) in join_messages.iter().zip(new_keys) { + let party_index = join_message.party_index.unwrap(); + let local_key = join_message.collect( + refresh_messages.as_slice(), + dk, + join_messages.as_slice(), + t, + n, + t, + )?; + + new_keys_vec.push((party_index - 1, local_key)); + } + + new_keys_vec.sort_by(|a, b| a.0.cmp(&b.0)); + let keys_replacements = new_keys_vec + .iter() + .map(|a| a.1.clone()) + .collect::>>(); + *keys = keys_replacements; + Ok(()) + } + + let t = 2; + let n = 7; + + let all_keys = simulate_keygen(t, n); + // Remove the 2nd and 7th party + let mut keys = all_keys.clone(); + keys.remove(6); + keys.remove(1); + + let mut old_to_new_map: HashMap = HashMap::new(); + old_to_new_map.insert(1, 4); + old_to_new_map.insert(3, 1); + old_to_new_map.insert(4, 3); + old_to_new_map.insert(5, 6); + old_to_new_map.insert(6, 5); + + // Simulate the replace + simulate_replace::<{ crate::M_SECURITY }>( + &mut keys, + &[2, 7], + &old_to_new_map, + t, + n, + ) + .unwrap(); + // check that sum of old keys is equal to sum of new keys + let old_linear_secret_key: Vec<_> = (0..all_keys.len()) + .map(|i| all_keys[i].keys_linear.x_i.clone()) + .collect(); + + let new_linear_secret_key: Vec<_> = + (0..keys.len()).map(|i| keys[i].keys_linear.x_i.clone()).collect(); + let indices: Vec<_> = (0..(t + 1) as u16).collect(); + let vss = VerifiableSS:: { + parameters: ShamirSecretSharing { threshold: t, share_count: n }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random(), + ), + }; + assert_eq!( + vss.reconstruct( + &indices[..], + &old_linear_secret_key[0..(t + 1) as usize] + ), + vss.reconstruct( + &indices[..], + &new_linear_secret_key[0..(t + 1) as usize] + ) + ); + assert_ne!(old_linear_secret_key, new_linear_secret_key); + + let offline_sign = simulate_offline_stage(keys, &[1, 2, 7]); + simulate_signing(offline_sign, b"ZenGo"); + } + + #[test] + fn test_change_threshold_sign_rotate_sign() { + let mut keys = simulate_keygen(2, 5); + let offline_sign = simulate_offline_stage(keys.clone(), &[1, 2, 3]); + simulate_signing(offline_sign, b"ZenGo"); + // Change threshold to 1 (i.e quorum size = 2). + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, Some(1)); + let offline_sign = simulate_offline_stage(keys.clone(), &[3, 4]); + simulate_signing(offline_sign, b"ZenGo"); + // Change threshold to back to 2 (i.e quorum size = 3). + simulate_dkr::<{ crate::M_SECURITY }>(&mut keys, Some(2)); + let offline_sign = simulate_offline_stage(keys, &[1, 3, 5]); + simulate_signing(offline_sign, b"ZenGo"); + } + + fn simulate_keygen(t: u16, n: u16) -> Vec> { + //simulate keygen + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + for i in 1..=n { + simulation.add_party(Keygen::new(i, t, n).unwrap()); + } + + simulation.run().unwrap() + } + + fn simulate_dkr_removal( + keys: &mut Vec>, + remove_party_indices: Vec, + new_t_option: Option, + ) { + let mut broadcast_messages: HashMap< + usize, + Vec>, + > = HashMap::new(); + let mut new_dks: HashMap = HashMap::new(); + let mut refresh_messages: Vec> = + Vec::new(); + let mut party_key: HashMap> = HashMap::new(); + // TODO: Verify this is correct + let new_n = keys.len() as u16; + let current_t = keys[0].t; + let new_t = new_t_option.unwrap_or(current_t); + for key in keys.iter_mut() { + let (refresh_message, new_dk) = + RefreshMessage::distribute(key.i, key, new_t, new_n).unwrap(); + refresh_messages.push(refresh_message.clone()); + new_dks.insert(refresh_message.party_index.into(), new_dk); + party_key.insert(refresh_message.party_index.into(), key.clone()); + } + + for refresh_message in refresh_messages.iter() { + broadcast_messages + .insert(refresh_message.party_index.into(), Vec::new()); + } + + for refresh_message in refresh_messages.iter_mut() { + if !remove_party_indices + .contains(&refresh_message.party_index.into()) + { + refresh_message.remove_party_indices = + remove_party_indices.clone(); + } else { + let mut new_remove_party_indices = remove_party_indices.clone(); + new_remove_party_indices.retain(|value| { + *value != refresh_message.party_index.into() + }); + refresh_message.remove_party_indices = new_remove_party_indices; + } + + for (party_index, refresh_bucket) in broadcast_messages.iter_mut() { + if refresh_message + .remove_party_indices + .contains(&(*party_index as u16)) + { + continue + } + refresh_bucket.push(refresh_message.clone()); + } + } + + for remove_party_index in remove_party_indices.iter() { + assert_eq!( + broadcast_messages[&(*remove_party_index as usize)].len(), + 1 + ); + } + + // keys will be updated to refreshed values + for (party, key) in party_key.iter_mut() { + if remove_party_indices.contains(&(*party as u16)) { + continue + } + + RefreshMessage::collect( + broadcast_messages[party].clone().as_slice(), + key, + new_dks[party].clone(), + &[], + current_t, + ) + .expect(""); + } + + for remove_party_index in remove_party_indices { + let result = RefreshMessage::collect( + &broadcast_messages[&(remove_party_index as usize)], + &mut keys[remove_party_index as usize], + new_dks[&(remove_party_index as usize)].clone(), + &[], + current_t, + ); + assert!(result.is_err()); + } + } + + fn simulate_dkr( + keys: &mut Vec>, + new_t_option: Option, + ) -> (Vec>, Vec) { + let mut broadcast_vec: Vec> = + Vec::new(); + let mut new_dks: Vec = Vec::new(); + let keys_len = keys.len(); + let current_t = keys[0].t; + let new_t = new_t_option.unwrap_or(current_t); + for key in keys.iter_mut() { + let (refresh_message, new_dk) = + RefreshMessage::distribute(key.i, key, new_t, keys_len as u16) + .unwrap(); + broadcast_vec.push(refresh_message); + new_dks.push(new_dk); + } + + // keys will be updated to refreshed values + for i in 0..keys.len() as usize { + RefreshMessage::collect( + &broadcast_vec, + &mut keys[i], + new_dks[i].clone(), + &[], + current_t, + ) + .expect(""); + } + + (broadcast_vec, new_dks) + } + + fn simulate_offline_stage( + local_keys: Vec>, + s_l: &[u16], + ) -> Vec { + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + for (i, &keygen_i) in (1..).zip(s_l) { + simulation.add_party( + OfflineStage::new( + i, + s_l.to_vec(), + local_keys[usize::from(keygen_i - 1)].clone(), + ) + .unwrap(), + ); + } + + simulation.run().unwrap() + } + + fn simulate_signing(offline: Vec, message: &[u8]) { + let message = create_hash(&[&BigInt::from_bytes(message)]); + let pk = &offline[0].public_key(); + + let parties = offline + .iter() + .map(|o| SignManual::new(message.clone(), o.clone())) + .collect::, _>>() + .unwrap(); + let (parties, local_sigs): (Vec<_>, Vec<_>) = + parties.into_iter().unzip(); + // parties.remove(0).complete(&local_sigs[1..]).unwrap(); + let local_sigs_except = |i: usize| { + let mut v = vec![]; + v.extend_from_slice(&local_sigs[..i]); + if i + 1 < local_sigs.len() { + v.extend_from_slice(&local_sigs[i + 1..]); + } + v + }; + + assert!(parties + .into_iter() + .enumerate() + .map(|(i, p)| p.complete(&local_sigs_except(i)).unwrap()) + .all(|signature| verify(&signature, &pk, &message).is_ok())); + } + + fn create_hash(big_ints: &[&BigInt]) -> BigInt { + let hasher = Sha256::new(); + + for value in big_ints { + hasher.clone().chain(&BigInt::to_bytes(value)); + } + + let result_hex = hasher.finalize(); + BigInt::from_bytes(&result_hex[..]) + } } diff --git a/fs-dkr/src/zk_pdl_with_slack.rs b/fs-dkr/src/zk_pdl_with_slack.rs index 63b7f158..a19c6d45 100644 --- a/fs-dkr/src/zk_pdl_with_slack.rs +++ b/fs-dkr/src/zk_pdl_with_slack.rs @@ -5,312 +5,336 @@ //! //! Statement: (c, pk, Q, G) //! witness (x, r) such that Q = xG, c = Enc(pk, x, r) -//! note that because of the range proof, the proof has a slack in the range: x in [-q^3, q^3] +//! note that because of the range proof, the proof has a slack in the range: x +//! in [-q^3, q^3] use std::marker::PhantomData; use crate::error::{FsDkrError, FsDkrResult}; use curv::{ - arithmetic::traits::*, - cryptographic_primitives::hashing::{Digest, DigestExt}, - elliptic::curves::{Curve, Point, Scalar, Secp256k1}, - BigInt, + arithmetic::traits::*, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{Curve, Point, Scalar, Secp256k1}, + BigInt, }; use paillier::EncryptionKey; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PDLwSlackStatement { - pub ciphertext: BigInt, - pub ek: EncryptionKey, - pub Q: Point, - pub G: Point, - pub h1: BigInt, - pub h2: BigInt, - pub N_tilde: BigInt, + pub ciphertext: BigInt, + pub ek: EncryptionKey, + pub Q: Point, + pub G: Point, + pub h1: BigInt, + pub h2: BigInt, + pub N_tilde: BigInt, } #[derive(Clone)] pub struct PDLwSlackWitness { - pub x: Scalar, - pub r: BigInt, + pub x: Scalar, + pub r: BigInt, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(bound = "E: Curve, H: Digest + Clone")] pub struct PDLwSlackProof { - z: BigInt, - u1: Point, - u2: BigInt, - u3: BigInt, - s1: BigInt, - s2: BigInt, - s3: BigInt, - _phantom: PhantomData, + z: BigInt, + u1: Point, + u2: BigInt, + u3: BigInt, + s1: BigInt, + s2: BigInt, + s3: BigInt, + _phantom: PhantomData, } impl PDLwSlackProof { - pub fn prove(witness: &PDLwSlackWitness, statement: &PDLwSlackStatement) -> Self { - let q3 = Scalar::::group_order().pow(3); - let q_N_tilde = Scalar::::group_order() * &statement.N_tilde; - let q3_N_tilde = &q3 * &statement.N_tilde; - - let alpha = BigInt::sample_below(&q3); - let one = BigInt::one(); - let beta = BigInt::sample_range(&one, &(&statement.ek.n - &one)); - let rho = BigInt::sample_below(&q_N_tilde); - let gamma = BigInt::sample_below(&q3_N_tilde); - - let z = commitment_unknown_order( - &statement.h1, - &statement.h2, - &statement.N_tilde, - &witness.x.to_bigint(), - &rho, - ); - let u1 = statement.G.clone() * Scalar::::from(&alpha); - let u2 = commitment_unknown_order( - &(&statement.ek.n + BigInt::one()), - &beta, - &statement.ek.nn, - &alpha, - &statement.ek.n, - ); - let u3 = commitment_unknown_order( - &statement.h1, - &statement.h2, - &statement.N_tilde, - &alpha, - &gamma, - ); - - let e = H::new() - .chain_bigint(&BigInt::from_bytes(&statement.G.to_bytes(true))) - .chain_bigint(&BigInt::from_bytes(&statement.Q.to_bytes(true))) - .chain_bigint(&statement.ciphertext) - .chain_bigint(&z) - .chain_bigint(&BigInt::from_bytes(&u1.to_bytes(true))) - .chain_bigint(&u2) - .chain_bigint(&u3) - .result_bigint(); - - let s1 = &e * witness.x.to_bigint() + alpha; - let s2 = commitment_unknown_order(&witness.r, &beta, &statement.ek.n, &e, &BigInt::one()); - let s3 = &e * rho + gamma; - - PDLwSlackProof { z, u1, u2, u3, s1, s2, s3, _phantom: PhantomData } - } - - pub fn verify(&self, statement: &PDLwSlackStatement) -> FsDkrResult<()> { - let e = H::new() - .chain_bigint(&BigInt::from_bytes(&statement.G.to_bytes(true))) - .chain_bigint(&BigInt::from_bytes(&statement.Q.to_bytes(true))) - .chain_bigint(&statement.ciphertext) - .chain_bigint(&self.z) - .chain_bigint(&BigInt::from_bytes(&self.u1.to_bytes(true))) - .chain_bigint(&self.u2) - .chain_bigint(&self.u3) - .result_bigint(); - - let g_s1 = statement.G.clone() * Scalar::::from(&self.s1); - let e_fe_neg = Scalar::::from(&(Scalar::::group_order() - &e)); - let y_minus_e = statement.Q.clone() * e_fe_neg; - let u1_test = g_s1 + y_minus_e; - - let u2_test_tmp = commitment_unknown_order( - &(&statement.ek.n + BigInt::one()), - &self.s2, - &statement.ek.nn, - &self.s1, - &statement.ek.n, - ); - let u2_test = commitment_unknown_order( - &u2_test_tmp, - &statement.ciphertext, - &statement.ek.nn, - &BigInt::one(), - &(-&e), - ); - - let u3_test_tmp = commitment_unknown_order( - &statement.h1, - &statement.h2, - &statement.N_tilde, - &self.s1, - &self.s3, - ); - let u3_test = commitment_unknown_order( - &u3_test_tmp, - &self.z, - &statement.N_tilde, - &BigInt::one(), - &(-&e), - ); - if self.u1 == u1_test && self.u2 == u2_test && self.u3 == u3_test { - Ok(()) - } else { - Err(FsDkrError::PDLwSlackProof { - is_u1_eq: self.u1 == u1_test, - is_u2_eq: self.u2 == u2_test, - is_u3_eq: self.u3 == u3_test, - }) - } - } + pub fn prove( + witness: &PDLwSlackWitness, + statement: &PDLwSlackStatement, + ) -> Self { + let q3 = Scalar::::group_order().pow(3); + let q_N_tilde = Scalar::::group_order() * &statement.N_tilde; + let q3_N_tilde = &q3 * &statement.N_tilde; + + let alpha = BigInt::sample_below(&q3); + let one = BigInt::one(); + let beta = BigInt::sample_range(&one, &(&statement.ek.n - &one)); + let rho = BigInt::sample_below(&q_N_tilde); + let gamma = BigInt::sample_below(&q3_N_tilde); + + let z = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &witness.x.to_bigint(), + &rho, + ); + let u1 = statement.G.clone() * Scalar::::from(&alpha); + let u2 = commitment_unknown_order( + &(&statement.ek.n + BigInt::one()), + &beta, + &statement.ek.nn, + &alpha, + &statement.ek.n, + ); + let u3 = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &alpha, + &gamma, + ); + + let e = H::new() + .chain_bigint(&BigInt::from_bytes(&statement.G.to_bytes(true))) + .chain_bigint(&BigInt::from_bytes(&statement.Q.to_bytes(true))) + .chain_bigint(&statement.ciphertext) + .chain_bigint(&z) + .chain_bigint(&BigInt::from_bytes(&u1.to_bytes(true))) + .chain_bigint(&u2) + .chain_bigint(&u3) + .result_bigint(); + + let s1 = &e * witness.x.to_bigint() + alpha; + let s2 = commitment_unknown_order( + &witness.r, + &beta, + &statement.ek.n, + &e, + &BigInt::one(), + ); + let s3 = &e * rho + gamma; + + PDLwSlackProof { z, u1, u2, u3, s1, s2, s3, _phantom: PhantomData } + } + + pub fn verify(&self, statement: &PDLwSlackStatement) -> FsDkrResult<()> { + let e = H::new() + .chain_bigint(&BigInt::from_bytes(&statement.G.to_bytes(true))) + .chain_bigint(&BigInt::from_bytes(&statement.Q.to_bytes(true))) + .chain_bigint(&statement.ciphertext) + .chain_bigint(&self.z) + .chain_bigint(&BigInt::from_bytes(&self.u1.to_bytes(true))) + .chain_bigint(&self.u2) + .chain_bigint(&self.u3) + .result_bigint(); + + let g_s1 = statement.G.clone() * Scalar::::from(&self.s1); + let e_fe_neg = Scalar::::from(&(Scalar::::group_order() - &e)); + let y_minus_e = statement.Q.clone() * e_fe_neg; + let u1_test = g_s1 + y_minus_e; + + let u2_test_tmp = commitment_unknown_order( + &(&statement.ek.n + BigInt::one()), + &self.s2, + &statement.ek.nn, + &self.s1, + &statement.ek.n, + ); + let u2_test = commitment_unknown_order( + &u2_test_tmp, + &statement.ciphertext, + &statement.ek.nn, + &BigInt::one(), + &(-&e), + ); + + let u3_test_tmp = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &self.s1, + &self.s3, + ); + let u3_test = commitment_unknown_order( + &u3_test_tmp, + &self.z, + &statement.N_tilde, + &BigInt::one(), + &(-&e), + ); + if self.u1 == u1_test && self.u2 == u2_test && self.u3 == u3_test { + Ok(()) + } else { + Err(FsDkrError::PDLwSlackProof { + is_u1_eq: self.u1 == u1_test, + is_u2_eq: self.u2 == u2_test, + is_u3_eq: self.u3 == u3_test, + }) + } + } } pub fn commitment_unknown_order( - h1: &BigInt, - h2: &BigInt, - N_tilde: &BigInt, - x: &BigInt, - r: &BigInt, + h1: &BigInt, + h2: &BigInt, + N_tilde: &BigInt, + x: &BigInt, + r: &BigInt, ) -> BigInt { - let h1_x = BigInt::mod_pow(h1, x, N_tilde); - let h2_r = { - if r < &BigInt::zero() { - let h2_inv = BigInt::mod_inv(h2, N_tilde).unwrap(); - BigInt::mod_pow(&h2_inv, &(-r), N_tilde) - } else { - BigInt::mod_pow(h2, r, N_tilde) - } - }; - BigInt::mod_mul(&h1_x, &h2_r, N_tilde) + let h1_x = BigInt::mod_pow(h1, x, N_tilde); + let h2_r = { + if r < &BigInt::zero() { + let h2_inv = BigInt::mod_inv(h2, N_tilde).unwrap(); + BigInt::mod_pow(&h2_inv, &(-r), N_tilde) + } else { + BigInt::mod_pow(h2, r, N_tilde) + } + }; + BigInt::mod_mul(&h1_x, &h2_r, N_tilde) } #[cfg(test)] mod test { - use super::*; - use curv::{ - elliptic::curves::secp256_k1::{Secp256k1Point, Secp256k1Scalar}, - BigInt, - }; - use paillier::{ - core::Randomness, - traits::{EncryptWithChosenRandomness, KeyGeneration}, - Paillier, RawPlaintext, - }; - use sha2::Sha256; - use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; - - type GE = Secp256k1Point; - type FE = Secp256k1Scalar; - - #[test] - fn test_zk_pdl_with_slack() { - // N_tilde, h1, h2 generation - let (ek_tilde, dk_tilde) = - Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - // note: safe primes should be used: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&phi); - let S = BigInt::from(2).pow(256 as u32); - let xhi = BigInt::sample_below(&S); - let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); - let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; - - let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); - - // generate the scalar secret and Paillier encrypt it - let (ek, _dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - // note: safe primes should be used here as well: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let randomness = Randomness::sample(&ek); - let x: Scalar = Scalar::::random(); - - let Q = Point::::generator().to_point() * &x; - - let c = Paillier::encrypt_with_chosen_randomness( - &ek, - RawPlaintext::from(x.to_bigint().clone()), - &randomness, - ) - .0 - .into_owned(); - - // Generate PDL with slack statement, witness and proof - let pdl_w_slack_statement = PDLwSlackStatement { - ciphertext: c, - ek, - Q, - G: Point::::generator().to_point(), - h1, - h2, - N_tilde: ek_tilde.n, - }; - - let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; - - let proof = PDLwSlackProof::::prove( - &pdl_w_slack_witness, - &pdl_w_slack_statement, - ); - // verify h1,h2, N_tilde - let setup_result = composite_dlog_proof.verify(&statement); - assert!(setup_result.is_ok()); - let result = proof.verify(&pdl_w_slack_statement); - assert!(result.is_ok()); - } - - #[test] - #[should_panic] - fn test_zk_pdl_with_slack_soundness() { - // N_tilde, h1, h2 generation - let (ek_tilde, dk_tilde) = - Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - // note: safe primes should be used: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&phi); - let S = BigInt::from(2).pow(256 as u32); - let xhi = BigInt::sample_below(&S); - let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); - let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; - - let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); - - // generate the scalar secret and Paillier encrypt it - let (ek, _dk) = Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE).keys(); - // note: safe primes should be used here as well: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let randomness = Randomness::sample(&ek); - let x: Scalar = Scalar::::random(); - - let Q = Point::::generator().to_point() * &x; - - // here we encrypt x + 1 instead of x: - let c = Paillier::encrypt_with_chosen_randomness( - &ek, - RawPlaintext::from(x.to_bigint().clone() + BigInt::one()), - &randomness, - ) - .0 - .into_owned(); - - // Generate PDL with slack statement, witness and proof - let pdl_w_slack_statement = PDLwSlackStatement { - ciphertext: c, - ek, - Q, - G: Point::::generator().to_point(), - h1, - h2, - N_tilde: ek_tilde.n, - }; - - let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; - - let proof = PDLwSlackProof::::prove( - &pdl_w_slack_witness, - &pdl_w_slack_statement, - ); - // verify h1,h2, N_tilde - let setup_result = composite_dlog_proof.verify(&statement); - assert!(setup_result.is_ok()); - let result = proof.verify(&pdl_w_slack_statement); - assert!(result.is_ok()); - } + use super::*; + use curv::{ + elliptic::curves::secp256_k1::{Secp256k1Point, Secp256k1Scalar}, + BigInt, + }; + use paillier::{ + core::Randomness, + traits::{EncryptWithChosenRandomness, KeyGeneration}, + Paillier, RawPlaintext, + }; + use sha2::Sha256; + use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; + + type GE = Secp256k1Point; + type FE = Secp256k1Scalar; + + #[test] + fn test_zk_pdl_with_slack() { + // N_tilde, h1, h2 generation + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE) + .keys(); + // note: safe primes should be used: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&phi); + let S = BigInt::from(2).pow(256 as u32); + let xhi = BigInt::sample_below(&S); + let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); + let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); + let statement = DLogStatement { + N: ek_tilde.n.clone(), + g: h1.clone(), + ni: h2.clone(), + }; + + let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); + + // generate the scalar secret and Paillier encrypt it + let (ek, _dk) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE) + .keys(); + // note: safe primes should be used here as well: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let randomness = Randomness::sample(&ek); + let x: Scalar = Scalar::::random(); + + let Q = Point::::generator().to_point() * &x; + + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint().clone()), + &randomness, + ) + .0 + .into_owned(); + + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: c, + ek, + Q, + G: Point::::generator().to_point(), + h1, + h2, + N_tilde: ek_tilde.n, + }; + + let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; + + let proof = PDLwSlackProof::::prove( + &pdl_w_slack_witness, + &pdl_w_slack_statement, + ); + // verify h1,h2, N_tilde + let setup_result = composite_dlog_proof.verify(&statement); + assert!(setup_result.is_ok()); + let result = proof.verify(&pdl_w_slack_statement); + assert!(result.is_ok()); + } + + #[test] + #[should_panic] + fn test_zk_pdl_with_slack_soundness() { + // N_tilde, h1, h2 generation + let (ek_tilde, dk_tilde) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE) + .keys(); + // note: safe primes should be used: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&phi); + let S = BigInt::from(2).pow(256 as u32); + let xhi = BigInt::sample_below(&S); + let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); + let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); + let statement = DLogStatement { + N: ek_tilde.n.clone(), + g: h1.clone(), + ni: h2.clone(), + }; + + let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); + + // generate the scalar secret and Paillier encrypt it + let (ek, _dk) = + Paillier::keypair_with_modulus_size(crate::PAILLIER_KEY_SIZE) + .keys(); + // note: safe primes should be used here as well: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let randomness = Randomness::sample(&ek); + let x: Scalar = Scalar::::random(); + + let Q = Point::::generator().to_point() * &x; + + // here we encrypt x + 1 instead of x: + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint().clone() + BigInt::one()), + &randomness, + ) + .0 + .into_owned(); + + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: c, + ek, + Q, + G: Point::::generator().to_point(), + h1, + h2, + N_tilde: ek_tilde.n, + }; + + let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; + + let proof = PDLwSlackProof::::prove( + &pdl_w_slack_witness, + &pdl_w_slack_statement, + ); + // verify h1,h2, N_tilde + let setup_result = composite_dlog_proof.verify(&statement); + assert!(setup_result.is_ok()); + let result = proof.verify(&pdl_w_slack_statement); + assert!(result.is_ok()); + } } diff --git a/rustfmt.toml b/rustfmt.toml index f4d7ad63..5a7d47a5 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,6 +1,6 @@ # Basic -hard_tabs = true -max_width = 100 +hard_tabs = false +max_width = 80 use_small_heuristics = "Max" # Imports imports_granularity = "Crate" diff --git a/src/lib.rs b/src/lib.rs index 04466f89..9e8d27e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,20 +1,20 @@ /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies. + Copyright 2022 by Webb Technologies. - This file is part of cggmp library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of cggmp library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - This file is derived/inspired from Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) + This file is derived/inspired from Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) - cggmp-threshold-ecdsa is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + cggmp-threshold-ecdsa is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ #![allow(non_snake_case)] @@ -31,29 +31,29 @@ pub mod utilities; #[derive(Copy, PartialEq, Eq, Clone, Debug)] pub enum Error { - InvalidKey, - InvalidSS, - InvalidCom, - InvalidSig, - Phase5BadSum, - Phase6Error, - AffineWithGroupComRangeProofError, + InvalidKey, + InvalidSS, + InvalidCom, + InvalidSig, + Phase5BadSum, + Phase6Error, + AffineWithGroupComRangeProofError, } #[derive(Clone, Debug)] pub struct ErrorType { - pub error_type: String, - pub bad_actors: Vec, - pub data: Vec, + pub error_type: String, + pub bad_actors: Vec, + pub data: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ProofVerificationErrorData { - proof_symbol: String, - verifying_party: u16, + proof_symbol: String, + verifying_party: u16, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NoOfflineStageErrorData { - l: usize, + l: usize, } diff --git a/src/party_i.rs b/src/party_i.rs index 42634ad0..634789ca 100644 --- a/src/party_i.rs +++ b/src/party_i.rs @@ -1,48 +1,55 @@ /* - Multi-party ECDSA + Multi-party ECDSA - Copyright 2018 by Kzen Networks + Copyright 2018 by Kzen Networks - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ use std::fmt::Debug; use centipede::juggling::{ - proof_system::{Helgamalsegmented, Witness}, - segmentation::Msegmentation, + proof_system::{Helgamalsegmented, Witness}, + segmentation::Msegmentation, }; use curv::{ - arithmetic::traits::*, - cryptographic_primitives::{ - commitments::{hash_commitment::HashCommitment, traits::Commitment}, - proofs::{sigma_correct_homomorphic_elgamal_enc::*, sigma_dlog::DLogProof}, - secret_sharing::feldman_vss::VerifiableSS, - }, - elliptic::curves::{secp256_k1::Secp256k1, Curve, Point, Scalar}, - BigInt, + arithmetic::traits::*, + cryptographic_primitives::{ + commitments::{hash_commitment::HashCommitment, traits::Commitment}, + proofs::{ + sigma_correct_homomorphic_elgamal_enc::*, sigma_dlog::DLogProof, + }, + secret_sharing::feldman_vss::VerifiableSS, + }, + elliptic::curves::{secp256_k1::Secp256k1, Curve, Point, Scalar}, + BigInt, }; use sha2::Sha256; use crate::Error::{self, InvalidSig, Phase5BadSum, Phase6Error}; use paillier::{ - Decrypt, DecryptionKey, EncryptionKey, KeyGeneration, Paillier, RawCiphertext, RawPlaintext, + Decrypt, DecryptionKey, EncryptionKey, KeyGeneration, Paillier, + RawCiphertext, RawPlaintext, }; use serde::{Deserialize, Serialize}; -use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement, NiCorrectKeyProof}; +use zk_paillier::zkproofs::{ + CompositeDLogProof, DLogStatement, NiCorrectKeyProof, +}; use crate::{ - utilities::zk_pdl_with_slack::{PDLwSlackProof, PDLwSlackStatement, PDLwSlackWitness}, - ErrorType, + utilities::zk_pdl_with_slack::{ + PDLwSlackProof, PDLwSlackStatement, PDLwSlackWitness, + }, + ErrorType, }; use curv::cryptographic_primitives::proofs::sigma_valid_pedersen::PedersenProof; @@ -54,191 +61,235 @@ const PAILLIER_MAX_BIT_LENGTH: usize = 2048; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Parameters { - pub threshold: u16, //t - pub share_count: u16, //n + pub threshold: u16, //t + pub share_count: u16, //n } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Keys { - pub u_i: Scalar, - pub y_i: Point, - // Paillier keys - pub dk: DecryptionKey, - pub ek: EncryptionKey, - pub party_index: usize, - pub N_tilde: BigInt, - pub h1: BigInt, - pub h2: BigInt, - pub xhi: BigInt, - pub xhi_inv: BigInt, + pub u_i: Scalar, + pub y_i: Point, + // Paillier keys + pub dk: DecryptionKey, + pub ek: EncryptionKey, + pub party_index: usize, + pub N_tilde: BigInt, + pub h1: BigInt, + pub h2: BigInt, + pub xhi: BigInt, + pub xhi_inv: BigInt, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PartyPrivate { - u_i: Scalar, - x_i: Scalar, - dk: DecryptionKey, + u_i: Scalar, + x_i: Scalar, + dk: DecryptionKey, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct KeyGenBroadcastMessage1 { - pub e: EncryptionKey, - pub dlog_statement: DLogStatement, - pub com: BigInt, - pub correct_key_proof: NiCorrectKeyProof, - pub composite_dlog_proof_base_h1: CompositeDLogProof, - pub composite_dlog_proof_base_h2: CompositeDLogProof, + pub e: EncryptionKey, + pub dlog_statement: DLogStatement, + pub com: BigInt, + pub correct_key_proof: NiCorrectKeyProof, + pub composite_dlog_proof_base_h1: CompositeDLogProof, + pub composite_dlog_proof_base_h2: CompositeDLogProof, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct KeyGenDecommitMessage1 { - pub blind_factor: BigInt, - pub y_i: Point, + pub blind_factor: BigInt, + pub y_i: Point, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SharedKeys { - pub y: Point, - pub x_i: Scalar, + pub y: Point, + pub x_i: Scalar, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SignKeys { - pub w_i: Scalar, - pub g_w_i: Point, - pub k_i: Scalar, - pub gamma_i: Scalar, - pub g_gamma_i: Point, + pub w_i: Scalar, + pub g_w_i: Point, + pub k_i: Scalar, + pub gamma_i: Scalar, + pub g_gamma_i: Point, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SignBroadcastPhase1 { - pub com: BigInt, + pub com: BigInt, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SignDecommitPhase1 { - pub blind_factor: BigInt, - pub g_gamma_i: Point, + pub blind_factor: BigInt, + pub g_gamma_i: Point, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LocalSignature { - pub r: Scalar, - pub R: Point, - pub s_i: Scalar, - pub m: BigInt, - pub y: Point, + pub r: Scalar, + pub R: Point, + pub s_i: Scalar, + pub m: BigInt, + pub y: Point, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SignatureRecid { - pub r: Scalar, - pub s: Scalar, - pub recid: u8, + pub r: Scalar, + pub s: Scalar, + pub recid: u8, } pub fn generate_h1_h2_N_tilde() -> (BigInt, BigInt, BigInt, BigInt, BigInt) { - // note, should be safe primes: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys();; - let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&ek_tilde.n); - let (mut xhi, mut xhi_inv) = loop { - let xhi_ = BigInt::sample_below(&phi); - match BigInt::mod_inv(&xhi_, &phi) { - Some(inv) => break (xhi_, inv), - None => continue, - } - }; - let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); - xhi = BigInt::sub(&phi, &xhi); - xhi_inv = BigInt::sub(&phi, &xhi_inv); - - (ek_tilde.n, h1, h2, xhi, xhi_inv) + // note, should be safe primes: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys();; + let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&ek_tilde.n); + let (mut xhi, mut xhi_inv) = loop { + let xhi_ = BigInt::sample_below(&phi); + match BigInt::mod_inv(&xhi_, &phi) { + Some(inv) => break (xhi_, inv), + None => continue, + } + }; + let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); + xhi = BigInt::sub(&phi, &xhi); + xhi_inv = BigInt::sub(&phi, &xhi_inv); + + (ek_tilde.n, h1, h2, xhi, xhi_inv) } impl Keys { - pub fn create(index: usize) -> Self { - let u = Scalar::::random(); - let y = Point::generator() * &u; - let (ek, dk) = Paillier::keypair().keys(); - let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); - - Self { u_i: u, y_i: y, dk, ek, party_index: index, N_tilde, h1, h2, xhi, xhi_inv } - } - - // we recommend using safe primes if the code is used in production - pub fn create_safe_prime(index: usize) -> Self { - let u = Scalar::::random(); - let y = Point::generator() * &u; - - let (ek, dk) = Paillier::keypair_safe_primes().keys(); - let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); - - Self { u_i: u, y_i: y, dk, ek, party_index: index, N_tilde, h1, h2, xhi, xhi_inv } - } - pub fn create_from(u: Scalar, index: usize) -> Self { - let y = Point::generator() * &u; - let (ek, dk) = Paillier::keypair().keys(); - let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); - - Self { u_i: u, y_i: y, dk, ek, party_index: index, N_tilde, h1, h2, xhi, xhi_inv } - } - - pub fn phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2( - &self, - ) -> (KeyGenBroadcastMessage1, KeyGenDecommitMessage1) { - let blind_factor = BigInt::sample(SECURITY); - let correct_key_proof = NiCorrectKeyProof::proof(&self.dk, None); - - let dlog_statement_base_h1 = - DLogStatement { N: self.N_tilde.clone(), g: self.h1.clone(), ni: self.h2.clone() }; - let dlog_statement_base_h2 = - DLogStatement { N: self.N_tilde.clone(), g: self.h2.clone(), ni: self.h1.clone() }; - - let composite_dlog_proof_base_h1 = - CompositeDLogProof::prove(&dlog_statement_base_h1, &self.xhi); - let composite_dlog_proof_base_h2 = - CompositeDLogProof::prove(&dlog_statement_base_h2, &self.xhi_inv); - - let com = HashCommitment::::create_commitment_with_user_defined_randomness( + pub fn create(index: usize) -> Self { + let u = Scalar::::random(); + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair().keys(); + let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); + + Self { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + N_tilde, + h1, + h2, + xhi, + xhi_inv, + } + } + + // we recommend using safe primes if the code is used in production + pub fn create_safe_prime(index: usize) -> Self { + let u = Scalar::::random(); + let y = Point::generator() * &u; + + let (ek, dk) = Paillier::keypair_safe_primes().keys(); + let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); + + Self { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + N_tilde, + h1, + h2, + xhi, + xhi_inv, + } + } + pub fn create_from(u: Scalar, index: usize) -> Self { + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair().keys(); + let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); + + Self { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + N_tilde, + h1, + h2, + xhi, + xhi_inv, + } + } + + pub fn phase1_broadcast_phase3_proof_of_correct_key_proof_of_correct_h1h2( + &self, + ) -> (KeyGenBroadcastMessage1, KeyGenDecommitMessage1) { + let blind_factor = BigInt::sample(SECURITY); + let correct_key_proof = NiCorrectKeyProof::proof(&self.dk, None); + + let dlog_statement_base_h1 = DLogStatement { + N: self.N_tilde.clone(), + g: self.h1.clone(), + ni: self.h2.clone(), + }; + let dlog_statement_base_h2 = DLogStatement { + N: self.N_tilde.clone(), + g: self.h2.clone(), + ni: self.h1.clone(), + }; + + let composite_dlog_proof_base_h1 = + CompositeDLogProof::prove(&dlog_statement_base_h1, &self.xhi); + let composite_dlog_proof_base_h2 = + CompositeDLogProof::prove(&dlog_statement_base_h2, &self.xhi_inv); + + let com = HashCommitment::::create_commitment_with_user_defined_randomness( &BigInt::from_bytes(self.y_i.to_bytes(true).as_ref()), &blind_factor, ); - let bcm1 = KeyGenBroadcastMessage1 { - e: self.ek.clone(), - dlog_statement: dlog_statement_base_h1, - com, - correct_key_proof, - composite_dlog_proof_base_h1, - composite_dlog_proof_base_h2, - }; - let decom1 = KeyGenDecommitMessage1 { blind_factor, y_i: self.y_i.clone() }; - (bcm1, decom1) - } - - #[allow(clippy::type_complexity)] - pub fn phase1_verify_com_phase3_verify_correct_key_verify_dlog_phase2_distribute( - &self, - params: &Parameters, - decom_vec: &[KeyGenDecommitMessage1], - bc1_vec: &[KeyGenBroadcastMessage1], - ) -> Result<(VerifiableSS, Vec>, usize), ErrorType> { - let mut bad_actors_vec = Vec::new(); - // test length: - assert_eq!(decom_vec.len(), usize::from(params.share_count)); - assert_eq!(bc1_vec.len(), usize::from(params.share_count)); - // test paillier correct key, h1,h2 correct generation and test decommitments - let correct_key_correct_decom_all = (0..bc1_vec.len()) - .map(|i| { - let dlog_statement_base_h2 = DLogStatement { - N: bc1_vec[i].dlog_statement.N.clone(), - g: bc1_vec[i].dlog_statement.ni.clone(), - ni: bc1_vec[i].dlog_statement.g.clone(), - }; - let test_res = + let bcm1 = KeyGenBroadcastMessage1 { + e: self.ek.clone(), + dlog_statement: dlog_statement_base_h1, + com, + correct_key_proof, + composite_dlog_proof_base_h1, + composite_dlog_proof_base_h2, + }; + let decom1 = + KeyGenDecommitMessage1 { blind_factor, y_i: self.y_i.clone() }; + (bcm1, decom1) + } + + #[allow(clippy::type_complexity)] + pub fn phase1_verify_com_phase3_verify_correct_key_verify_dlog_phase2_distribute( + &self, + params: &Parameters, + decom_vec: &[KeyGenDecommitMessage1], + bc1_vec: &[KeyGenBroadcastMessage1], + ) -> Result< + (VerifiableSS, Vec>, usize), + ErrorType, + > { + let mut bad_actors_vec = Vec::new(); + // test length: + assert_eq!(decom_vec.len(), usize::from(params.share_count)); + assert_eq!(bc1_vec.len(), usize::from(params.share_count)); + // test paillier correct key, h1,h2 correct generation and test + // decommitments + let correct_key_correct_decom_all = (0..bc1_vec.len()) + .map(|i| { + let dlog_statement_base_h2 = DLogStatement { + N: bc1_vec[i].dlog_statement.N.clone(), + g: bc1_vec[i].dlog_statement.ni.clone(), + ni: bc1_vec[i].dlog_statement.g.clone(), + }; + let test_res = HashCommitment::::create_commitment_with_user_defined_randomness( &BigInt::from_bytes(&decom_vec[i].y_i.to_bytes(true)), &decom_vec[i].blind_factor, @@ -256,217 +307,267 @@ impl Keys { .composite_dlog_proof_base_h2 .verify(&dlog_statement_base_h2) .is_ok(); - if !test_res { - bad_actors_vec.push(i); - false - } else { - true - } - }) - .all(|x| x); - - let err_type = ErrorType { - error_type: "invalid key".to_string(), - bad_actors: bad_actors_vec, - data: Vec::new(), - }; - - let (vss_scheme, secret_shares) = - VerifiableSS::share(params.threshold, params.share_count, &self.u_i); - if correct_key_correct_decom_all { - Ok((vss_scheme, secret_shares.to_vec(), self.party_index)) - } else { - Err(err_type) - } - } - - pub fn phase2_verify_vss_construct_keypair_phase3_pok_dlog( - &self, - params: &Parameters, - y_vec: &[Point], - secret_shares_vec: &[Scalar], - vss_scheme_vec: &[VerifiableSS], - index: usize, - ) -> Result<(SharedKeys, DLogProof), ErrorType> { - let mut bad_actors_vec = Vec::new(); - assert_eq!(y_vec.len(), usize::from(params.share_count)); - assert_eq!(secret_shares_vec.len(), usize::from(params.share_count)); - assert_eq!(vss_scheme_vec.len(), usize::from(params.share_count)); - - let correct_ss_verify = (0..y_vec.len()) - .map(|i| { - let res = vss_scheme_vec[i] - .validate_share(&secret_shares_vec[i], index.try_into().unwrap()) - .is_ok() && vss_scheme_vec[i].commitments[0] == y_vec[i]; - if !res { - bad_actors_vec.push(i); - false - } else { - true - } - }) - .all(|x| x); - - let err_type = ErrorType { - error_type: "invalid vss".to_string(), - bad_actors: bad_actors_vec, - data: Vec::new(), - }; - - if correct_ss_verify { - let (head, tail) = y_vec.split_at(1); - let y = tail.iter().fold(head[0].clone(), |acc, x| acc + x); - - let x_i = secret_shares_vec.iter().fold(Scalar::::zero(), |acc, x| acc + x); - let dlog_proof = DLogProof::prove(&x_i); - Ok((SharedKeys { y, x_i }, dlog_proof)) - } else { - Err(err_type) - } - } - - pub fn get_commitments_to_xi( - vss_scheme_vec: &[VerifiableSS], - ) -> Vec> { - let len = vss_scheme_vec.len(); - let (head, tail) = vss_scheme_vec.split_at(1); - let mut global_coefficients = head[0].commitments.clone(); - for vss in tail { - for (i, coefficient_commitment) in vss.commitments.iter().enumerate() { - global_coefficients[i] = &global_coefficients[i] + &*coefficient_commitment; - } - } - - let global_vss = VerifiableSS { - parameters: vss_scheme_vec[0].parameters.clone(), - commitments: global_coefficients, - proof: vss_scheme_vec[0].proof.clone(), - }; - (1..=len) - .map(|i| global_vss.get_point_commitment(i.try_into().unwrap())) - .collect::>>() - } - - pub fn update_commitments_to_xi( - comm: &Point, - vss_scheme: &VerifiableSS, - index: usize, - s: &[usize], - ) -> Point { - let s: Vec = s.iter().map(|&i| i.try_into().unwrap()).collect(); - let li = VerifiableSS::::map_share_to_new_params( - &vss_scheme.parameters, - index.try_into().unwrap(), - s.as_slice(), - ); - comm * &li - } - - pub fn verify_dlog_proofs_check_against_vss( - params: &Parameters, - dlog_proofs_vec: &[DLogProof], - y_vec: &[Point], - vss_vec: &[VerifiableSS], - ) -> Result<(), ErrorType> { - let mut bad_actors_vec = Vec::new(); - assert_eq!(y_vec.len(), usize::from(params.share_count)); - assert_eq!(dlog_proofs_vec.len(), usize::from(params.share_count)); - let xi_commitments = Keys::get_commitments_to_xi(vss_vec); - let xi_dlog_verify = (0..y_vec.len()) - .map(|i| { - let ver_res = DLogProof::verify(&dlog_proofs_vec[i]).is_ok(); - let verify_against_vss = xi_commitments[i] == dlog_proofs_vec[i].pk; - if !ver_res || !verify_against_vss { - bad_actors_vec.push(i); - false - } else { - true - } - }) - .all(|x| x); - - let err_type = ErrorType { - error_type: "bad dlog proof".to_string(), - bad_actors: bad_actors_vec, - data: Vec::new(), - }; - - if xi_dlog_verify { - Ok(()) - } else { - Err(err_type) - } - } + if !test_res { + bad_actors_vec.push(i); + false + } else { + true + } + }) + .all(|x| x); + + let err_type = ErrorType { + error_type: "invalid key".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + + let (vss_scheme, secret_shares) = VerifiableSS::share( + params.threshold, + params.share_count, + &self.u_i, + ); + if correct_key_correct_decom_all { + Ok((vss_scheme, secret_shares.to_vec(), self.party_index)) + } else { + Err(err_type) + } + } + + pub fn phase2_verify_vss_construct_keypair_phase3_pok_dlog( + &self, + params: &Parameters, + y_vec: &[Point], + secret_shares_vec: &[Scalar], + vss_scheme_vec: &[VerifiableSS], + index: usize, + ) -> Result<(SharedKeys, DLogProof), ErrorType> { + let mut bad_actors_vec = Vec::new(); + assert_eq!(y_vec.len(), usize::from(params.share_count)); + assert_eq!(secret_shares_vec.len(), usize::from(params.share_count)); + assert_eq!(vss_scheme_vec.len(), usize::from(params.share_count)); + + let correct_ss_verify = (0..y_vec.len()) + .map(|i| { + let res = vss_scheme_vec[i] + .validate_share( + &secret_shares_vec[i], + index.try_into().unwrap(), + ) + .is_ok() && + vss_scheme_vec[i].commitments[0] == y_vec[i]; + if !res { + bad_actors_vec.push(i); + false + } else { + true + } + }) + .all(|x| x); + + let err_type = ErrorType { + error_type: "invalid vss".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + + if correct_ss_verify { + let (head, tail) = y_vec.split_at(1); + let y = tail.iter().fold(head[0].clone(), |acc, x| acc + x); + + let x_i = secret_shares_vec + .iter() + .fold(Scalar::::zero(), |acc, x| acc + x); + let dlog_proof = DLogProof::prove(&x_i); + Ok((SharedKeys { y, x_i }, dlog_proof)) + } else { + Err(err_type) + } + } + + pub fn get_commitments_to_xi( + vss_scheme_vec: &[VerifiableSS], + ) -> Vec> { + let len = vss_scheme_vec.len(); + let (head, tail) = vss_scheme_vec.split_at(1); + let mut global_coefficients = head[0].commitments.clone(); + for vss in tail { + for (i, coefficient_commitment) in + vss.commitments.iter().enumerate() + { + global_coefficients[i] = + &global_coefficients[i] + &*coefficient_commitment; + } + } + + let global_vss = VerifiableSS { + parameters: vss_scheme_vec[0].parameters.clone(), + commitments: global_coefficients, + proof: vss_scheme_vec[0].proof.clone(), + }; + (1..=len) + .map(|i| global_vss.get_point_commitment(i.try_into().unwrap())) + .collect::>>() + } + + pub fn update_commitments_to_xi( + comm: &Point, + vss_scheme: &VerifiableSS, + index: usize, + s: &[usize], + ) -> Point { + let s: Vec = s.iter().map(|&i| i.try_into().unwrap()).collect(); + let li = + VerifiableSS::::map_share_to_new_params( + &vss_scheme.parameters, + index.try_into().unwrap(), + s.as_slice(), + ); + comm * &li + } + + pub fn verify_dlog_proofs_check_against_vss( + params: &Parameters, + dlog_proofs_vec: &[DLogProof], + y_vec: &[Point], + vss_vec: &[VerifiableSS], + ) -> Result<(), ErrorType> { + let mut bad_actors_vec = Vec::new(); + assert_eq!(y_vec.len(), usize::from(params.share_count)); + assert_eq!(dlog_proofs_vec.len(), usize::from(params.share_count)); + let xi_commitments = Keys::get_commitments_to_xi(vss_vec); + let xi_dlog_verify = (0..y_vec.len()) + .map(|i| { + let ver_res = DLogProof::verify(&dlog_proofs_vec[i]).is_ok(); + let verify_against_vss = + xi_commitments[i] == dlog_proofs_vec[i].pk; + if !ver_res || !verify_against_vss { + bad_actors_vec.push(i); + false + } else { + true + } + }) + .all(|x| x); + + let err_type = ErrorType { + error_type: "bad dlog proof".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + + if xi_dlog_verify { + Ok(()) + } else { + Err(err_type) + } + } } impl PartyPrivate { - pub fn set_private(key: Keys, shared_key: SharedKeys) -> Self { - Self { u_i: key.u_i, x_i: shared_key.x_i, dk: key.dk } - } - - pub fn y_i(&self) -> Point { - let g = Point::generator(); - g * &self.u_i - } - - pub fn decrypt(&self, ciphertext: BigInt) -> RawPlaintext { - Paillier::decrypt(&self.dk, &RawCiphertext::from(ciphertext)) - } - - pub fn refresh_private_key(&self, factor: &Scalar, index: usize) -> Keys { - let u: Scalar = &self.u_i + factor; - let y = Point::generator() * &u; - let (ek, dk) = Paillier::keypair().keys(); - - let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); - - Keys { u_i: u, y_i: y, dk, ek, party_index: index, N_tilde, h1, h2, xhi, xhi_inv } - } - - // we recommend using safe primes if the code is used in production - pub fn refresh_private_key_safe_prime(&self, factor: &Scalar, index: usize) -> Keys { - let u: Scalar = &self.u_i + factor; - let y = Point::generator() * &u; - let (ek, dk) = Paillier::keypair_safe_primes().keys(); - - let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); - - Keys { u_i: u, y_i: y, dk, ek, party_index: index, N_tilde, h1, h2, xhi, xhi_inv } - } - - // used for verifiable recovery - pub fn to_encrypted_segment( - &self, - segment_size: usize, - num_of_segments: usize, - pub_ke_y: &Point, - g: &Point, - ) -> (Witness, Helgamalsegmented) { - Msegmentation::to_encrypted_segments(&self.u_i, &segment_size, num_of_segments, pub_ke_y, g) - } - - pub fn update_private_key( - &self, - factor_u_i: &Scalar, - factor_x_i: &Scalar, - ) -> Self { - PartyPrivate { - u_i: &self.u_i + factor_u_i, - x_i: &self.x_i + factor_x_i, - dk: self.dk.clone(), - } - } + pub fn set_private(key: Keys, shared_key: SharedKeys) -> Self { + Self { u_i: key.u_i, x_i: shared_key.x_i, dk: key.dk } + } + + pub fn y_i(&self) -> Point { + let g = Point::generator(); + g * &self.u_i + } + + pub fn decrypt(&self, ciphertext: BigInt) -> RawPlaintext { + Paillier::decrypt(&self.dk, &RawCiphertext::from(ciphertext)) + } + + pub fn refresh_private_key( + &self, + factor: &Scalar, + index: usize, + ) -> Keys { + let u: Scalar = &self.u_i + factor; + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair().keys(); + + let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); + + Keys { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + N_tilde, + h1, + h2, + xhi, + xhi_inv, + } + } + + // we recommend using safe primes if the code is used in production + pub fn refresh_private_key_safe_prime( + &self, + factor: &Scalar, + index: usize, + ) -> Keys { + let u: Scalar = &self.u_i + factor; + let y = Point::generator() * &u; + let (ek, dk) = Paillier::keypair_safe_primes().keys(); + + let (N_tilde, h1, h2, xhi, xhi_inv) = generate_h1_h2_N_tilde(); + + Keys { + u_i: u, + y_i: y, + dk, + ek, + party_index: index, + N_tilde, + h1, + h2, + xhi, + xhi_inv, + } + } + + // used for verifiable recovery + pub fn to_encrypted_segment( + &self, + segment_size: usize, + num_of_segments: usize, + pub_ke_y: &Point, + g: &Point, + ) -> (Witness, Helgamalsegmented) { + Msegmentation::to_encrypted_segments( + &self.u_i, + &segment_size, + num_of_segments, + pub_ke_y, + g, + ) + } + + pub fn update_private_key( + &self, + factor_u_i: &Scalar, + factor_x_i: &Scalar, + ) -> Self { + PartyPrivate { + u_i: &self.u_i + factor_u_i, + x_i: &self.x_i + factor_x_i, + dk: self.dk.clone(), + } + } } impl SignKeys { - pub fn g_w_vec( - pk_vec: &[Point], - s: &[usize], - vss_scheme: &VerifiableSS, - ) -> Vec> { - let s: Vec = s.iter().map(|&i| i.try_into().unwrap()).collect(); - // TODO: check bounds - (0..s.len()) + pub fn g_w_vec( + pk_vec: &[Point], + s: &[usize], + vss_scheme: &VerifiableSS, + ) -> Vec> { + let s: Vec = s.iter().map(|&i| i.try_into().unwrap()).collect(); + // TODO: check bounds + (0..s.len()) .map(|i| { let li = VerifiableSS::::map_share_to_new_params( &vss_scheme.parameters, @@ -476,96 +577,109 @@ impl SignKeys { &pk_vec[s[i] as usize] * &li }) .collect::>>() - } - - pub fn create( - private_x_i: &Scalar, - vss_scheme: &VerifiableSS, - index: usize, - s: &[usize], - ) -> Self { - let s: Vec = s.iter().map(|&i| i.try_into().unwrap()).collect(); - let li = VerifiableSS::::map_share_to_new_params( - &vss_scheme.parameters, - index.try_into().unwrap(), - s.as_slice(), - ); - let w_i = li * private_x_i; - let g = Point::generator(); - let g_w_i = g * &w_i; - let gamma_i = Scalar::::random(); - let g_gamma_i = g * &gamma_i; - let k_i = Scalar::::random(); - Self { w_i, g_w_i, k_i, gamma_i, g_gamma_i } - } - - pub fn phase1_broadcast(&self) -> (SignBroadcastPhase1, SignDecommitPhase1) { - let blind_factor = BigInt::sample(SECURITY); - let g = Point::generator(); - let g_gamma_i = g * &self.gamma_i; - let com = HashCommitment::::create_commitment_with_user_defined_randomness( + } + + pub fn create( + private_x_i: &Scalar, + vss_scheme: &VerifiableSS, + index: usize, + s: &[usize], + ) -> Self { + let s: Vec = s.iter().map(|&i| i.try_into().unwrap()).collect(); + let li = + VerifiableSS::::map_share_to_new_params( + &vss_scheme.parameters, + index.try_into().unwrap(), + s.as_slice(), + ); + let w_i = li * private_x_i; + let g = Point::generator(); + let g_w_i = g * &w_i; + let gamma_i = Scalar::::random(); + let g_gamma_i = g * &gamma_i; + let k_i = Scalar::::random(); + Self { w_i, g_w_i, k_i, gamma_i, g_gamma_i } + } + + pub fn phase1_broadcast( + &self, + ) -> (SignBroadcastPhase1, SignDecommitPhase1) { + let blind_factor = BigInt::sample(SECURITY); + let g = Point::generator(); + let g_gamma_i = g * &self.gamma_i; + let com = HashCommitment::::create_commitment_with_user_defined_randomness( &BigInt::from_bytes(g_gamma_i.to_bytes(true).as_ref()), &blind_factor, ); - ( - SignBroadcastPhase1 { com }, - SignDecommitPhase1 { blind_factor, g_gamma_i: self.g_gamma_i.clone() }, - ) - } - - pub fn phase2_delta_i( - &self, - alpha_vec: &[Scalar], - beta_vec: &[Scalar], - ) -> Scalar { - let vec_len = alpha_vec.len(); - assert_eq!(alpha_vec.len(), beta_vec.len()); - // assert_eq!(alpha_vec.len(), self.s.len() - 1); - let ki_gamma_i = &self.k_i * &self.gamma_i; - - (0..vec_len) - .map(|i| &alpha_vec[i] + &beta_vec[i]) - .fold(ki_gamma_i, |acc, x| acc + x) - } - - pub fn phase2_sigma_i( - &self, - miu_vec: &[Scalar], - ni_vec: &[Scalar], - ) -> Scalar { - let vec_len = miu_vec.len(); - assert_eq!(miu_vec.len(), ni_vec.len()); - //assert_eq!(miu_vec.len(), self.s.len() - 1); - let ki_w_i = &self.k_i * &self.w_i; - (0..vec_len).map(|i| &miu_vec[i] + &ni_vec[i]).fold(ki_w_i, |acc, x| acc + x) - } - - pub fn phase3_compute_t_i( - sigma_i: &Scalar, - ) -> (Point, Scalar, PedersenProof) { - let g_sigma_i = Point::generator() * sigma_i; - let l = Scalar::::random(); - let h_l = Point::::base_point2() * &l; - let T = g_sigma_i + h_l; - let T_zk_proof = PedersenProof::::prove(sigma_i, &l); - - (T, l, T_zk_proof) - } - pub fn phase3_reconstruct_delta(delta_vec: &[Scalar]) -> Scalar { - let sum = delta_vec.iter().fold(Scalar::::zero(), |acc, x| acc + x); - sum.invert().unwrap() - } - - pub fn phase4( - delta_inv: &Scalar, - b_proof_vec: &[&DLogProof], - phase1_decommit_vec: Vec, - bc1_vec: &[SignBroadcastPhase1], - index: usize, - ) -> Result, ErrorType> { - let mut bad_actors_vec = Vec::new(); - let test_b_vec_and_com = (0..b_proof_vec.len()) + ( + SignBroadcastPhase1 { com }, + SignDecommitPhase1 { + blind_factor, + g_gamma_i: self.g_gamma_i.clone(), + }, + ) + } + + pub fn phase2_delta_i( + &self, + alpha_vec: &[Scalar], + beta_vec: &[Scalar], + ) -> Scalar { + let vec_len = alpha_vec.len(); + assert_eq!(alpha_vec.len(), beta_vec.len()); + // assert_eq!(alpha_vec.len(), self.s.len() - 1); + let ki_gamma_i = &self.k_i * &self.gamma_i; + + (0..vec_len) + .map(|i| &alpha_vec[i] + &beta_vec[i]) + .fold(ki_gamma_i, |acc, x| acc + x) + } + + pub fn phase2_sigma_i( + &self, + miu_vec: &[Scalar], + ni_vec: &[Scalar], + ) -> Scalar { + let vec_len = miu_vec.len(); + assert_eq!(miu_vec.len(), ni_vec.len()); + //assert_eq!(miu_vec.len(), self.s.len() - 1); + let ki_w_i = &self.k_i * &self.w_i; + (0..vec_len) + .map(|i| &miu_vec[i] + &ni_vec[i]) + .fold(ki_w_i, |acc, x| acc + x) + } + + pub fn phase3_compute_t_i( + sigma_i: &Scalar, + ) -> (Point, Scalar, PedersenProof) + { + let g_sigma_i = Point::generator() * sigma_i; + let l = Scalar::::random(); + let h_l = Point::::base_point2() * &l; + let T = g_sigma_i + h_l; + let T_zk_proof = PedersenProof::::prove(sigma_i, &l); + + (T, l, T_zk_proof) + } + pub fn phase3_reconstruct_delta( + delta_vec: &[Scalar], + ) -> Scalar { + let sum = delta_vec + .iter() + .fold(Scalar::::zero(), |acc, x| acc + x); + sum.invert().unwrap() + } + + pub fn phase4( + delta_inv: &Scalar, + b_proof_vec: &[&DLogProof], + phase1_decommit_vec: Vec, + bc1_vec: &[SignBroadcastPhase1], + index: usize, + ) -> Result, ErrorType> { + let mut bad_actors_vec = Vec::new(); + let test_b_vec_and_com = (0..b_proof_vec.len()) .map(|j| { let ind = if j < index { j } else { j + 1 }; let res = b_proof_vec[j].pk == phase1_decommit_vec[ind].g_gamma_i && @@ -584,245 +698,274 @@ impl SignKeys { }) .all(|x| x); - let mut g_gamma_i_iter = phase1_decommit_vec.iter(); - let head = g_gamma_i_iter.next().unwrap(); - let tail = g_gamma_i_iter; - - let err_type = ErrorType { - error_type: "bad gamma_i decommit".to_string(), - bad_actors: bad_actors_vec, - data: Vec::new(), - }; - - if test_b_vec_and_com { - Ok({ - let gamma_sum = tail.fold(head.g_gamma_i.clone(), |acc, x| acc + &x.g_gamma_i); - // R - gamma_sum * delta_inv - }) - } else { - Err(err_type) - } - } + let mut g_gamma_i_iter = phase1_decommit_vec.iter(); + let head = g_gamma_i_iter.next().unwrap(); + let tail = g_gamma_i_iter; + + let err_type = ErrorType { + error_type: "bad gamma_i decommit".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + + if test_b_vec_and_com { + Ok({ + let gamma_sum = tail + .fold(head.g_gamma_i.clone(), |acc, x| acc + &x.g_gamma_i); + // R + gamma_sum * delta_inv + }) + } else { + Err(err_type) + } + } } impl LocalSignature { - pub fn phase5_proof_pdl( - R_dash: &Point, - R: &Point, - k_ciphertext: &BigInt, - ek: &EncryptionKey, - k_i: &Scalar, - k_enc_randomness: &BigInt, - dlog_statement: &DLogStatement, - ) -> PDLwSlackProof { - // Generate PDL with slack statement, witness and proof - let pdl_w_slack_statement = PDLwSlackStatement { - ciphertext: k_ciphertext.clone(), - ek: ek.clone(), - Q: R_dash.clone(), - G: R.clone(), - h1: dlog_statement.g.clone(), - h2: dlog_statement.ni.clone(), - N_tilde: dlog_statement.N.clone(), - }; - - let pdl_w_slack_witness = PDLwSlackWitness { x: k_i.clone(), r: k_enc_randomness.clone() }; - - PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement) - } - - #[allow(clippy::too_many_arguments)] - pub fn phase5_verify_pdl( - pdl_w_slack_proof_vec: &[PDLwSlackProof], - R_dash: &Point, - R: &Point, - k_ciphertext: &BigInt, - ek: &EncryptionKey, - dlog_statement: &[DLogStatement], - s: &[usize], - i: usize, - ) -> Result<(), ErrorType> { - let mut bad_actors_vec = Vec::new(); - - let num_of_other_participants = s.len() - 1; - if pdl_w_slack_proof_vec.len() != num_of_other_participants { - bad_actors_vec.push(i); - } else { - let proofs_verification = (0..pdl_w_slack_proof_vec.len()) - .map(|j| { - let ind = if j < i { j } else { j + 1 }; - let pdl_w_slack_statement = PDLwSlackStatement { - ciphertext: k_ciphertext.clone(), - ek: ek.clone(), - Q: R_dash.clone(), - G: R.clone(), - h1: dlog_statement[s[ind]].g.clone(), - h2: dlog_statement[s[ind]].ni.clone(), - N_tilde: dlog_statement[s[ind]].N.clone(), - }; - let ver_res = pdl_w_slack_proof_vec[j].verify(&pdl_w_slack_statement); - if ver_res.is_err() { - bad_actors_vec.push(i); - false - } else { - true - } - }) - .all(|x| x); - if proofs_verification { - return Ok(()) - } - } - - let err_type = ErrorType { - error_type: "Bad PDLwSlack proof".to_string(), - bad_actors: bad_actors_vec, - data: Vec::new(), - }; - Err(err_type) - } - - pub fn phase5_check_R_dash_sum(R_dash_vec: &[Point]) -> Result<(), Error> { - let sum = R_dash_vec.iter().fold(Point::generator().to_point(), |acc, x| acc + x); - match sum - &Point::generator().to_point() == Point::generator().to_point() { - true => Ok(()), - false => Err(Phase5BadSum), - } - } - - pub fn phase6_compute_S_i_and_proof_of_consistency( - R: &Point, - T: &Point, - sigma: &Scalar, - l: &Scalar, - ) -> (Point, HomoELGamalProof) { - let S = R * sigma; - let delta = HomoElGamalStatement { - G: R.clone(), - H: Point::::base_point2().clone(), - Y: Point::generator().to_point(), - D: T.clone(), - E: S.clone(), - }; - let witness = HomoElGamalWitness { x: l.clone(), r: sigma.clone() }; - let proof = HomoELGamalProof::prove(&witness, &delta); - - (S, proof) - } - - pub fn phase6_verify_proof( - S_vec: &[Point], - proof_vec: &[HomoELGamalProof], - R_vec: &[Point], - T_vec: &[Point], - ) -> Result<(), ErrorType> { - let mut bad_actors_vec = Vec::new(); - let mut verify_proofs = true; - for i in 0..proof_vec.len() { - let delta = HomoElGamalStatement { - G: R_vec[i].clone(), - H: Point::::base_point2().clone(), - Y: Point::generator().to_point(), - D: T_vec[i].clone(), - E: S_vec[i].clone(), - }; - if proof_vec[i].verify(&delta).is_err() { - verify_proofs = false; - bad_actors_vec.push(i); - }; - } - - match verify_proofs { - true => Ok(()), - false => { - let err_type = ErrorType { - error_type: "phase6".to_string(), - bad_actors: bad_actors_vec, - data: Vec::new(), - }; - Err(err_type) - }, - } - } - - pub fn phase6_check_S_i_sum( - pubkey_y: &Point, - S_vec: &[Point], - ) -> Result<(), Error> { - let sum_plus_g = S_vec.iter().fold(Point::generator().to_point(), |acc, x| acc + x); - let sum = sum_plus_g - &Point::generator().to_point(); - - match &sum == pubkey_y { - true => Ok(()), - false => Err(Phase6Error), - } - } - - pub fn phase7_local_sig( - k_i: &Scalar, - message: &BigInt, - R: &Point, - sigma_i: &Scalar, - pubkey: &Point, - ) -> Self { - let m_fe = Scalar::::from(message); - let r = Scalar::::from( - &R.x_coord().unwrap().mod_floor(Scalar::::group_order()), - ); - let s_i = m_fe * k_i + &r * sigma_i; - Self { r, R: R.clone(), s_i, m: message.clone(), y: pubkey.clone() } - } - - pub fn output_signature(&self, s_vec: &[Scalar]) -> Result { - let mut s = s_vec.iter().fold(self.s_i.clone(), |acc, x| acc + x); - let s_bn = s.to_bigint(); - - let r = Scalar::::from( - &self.R.x_coord().unwrap().mod_floor(Scalar::::group_order()), - ); - let ry: BigInt = self.R.y_coord().unwrap().mod_floor(Scalar::::group_order()); - - /* - Calculate recovery id - it is not possible to compute the public key out of the signature - itself. Recovery id is used to enable extracting the public key uniquely. - 1. id = R.y & 1 - 2. if (s > curve.q / 2) id = id ^ 1 - */ - let is_ry_odd = ry.test_bit(0); - let mut recid = if is_ry_odd { 1 } else { 0 }; - let s_tag_bn = Scalar::::group_order() - &s_bn; - if s_bn > s_tag_bn { - s = Scalar::::from(&s_tag_bn); - recid ^= 1; - } - let sig = SignatureRecid { r, s, recid }; - let ver = verify(&sig, &self.y, &self.m).is_ok(); - if ver { - Ok(sig) - } else { - Err(InvalidSig) - } - } + pub fn phase5_proof_pdl( + R_dash: &Point, + R: &Point, + k_ciphertext: &BigInt, + ek: &EncryptionKey, + k_i: &Scalar, + k_enc_randomness: &BigInt, + dlog_statement: &DLogStatement, + ) -> PDLwSlackProof { + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: k_ciphertext.clone(), + ek: ek.clone(), + Q: R_dash.clone(), + G: R.clone(), + h1: dlog_statement.g.clone(), + h2: dlog_statement.ni.clone(), + N_tilde: dlog_statement.N.clone(), + }; + + let pdl_w_slack_witness = + PDLwSlackWitness { x: k_i.clone(), r: k_enc_randomness.clone() }; + + PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement) + } + + #[allow(clippy::too_many_arguments)] + pub fn phase5_verify_pdl( + pdl_w_slack_proof_vec: &[PDLwSlackProof], + R_dash: &Point, + R: &Point, + k_ciphertext: &BigInt, + ek: &EncryptionKey, + dlog_statement: &[DLogStatement], + s: &[usize], + i: usize, + ) -> Result<(), ErrorType> { + let mut bad_actors_vec = Vec::new(); + + let num_of_other_participants = s.len() - 1; + if pdl_w_slack_proof_vec.len() != num_of_other_participants { + bad_actors_vec.push(i); + } else { + let proofs_verification = (0..pdl_w_slack_proof_vec.len()) + .map(|j| { + let ind = if j < i { j } else { j + 1 }; + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: k_ciphertext.clone(), + ek: ek.clone(), + Q: R_dash.clone(), + G: R.clone(), + h1: dlog_statement[s[ind]].g.clone(), + h2: dlog_statement[s[ind]].ni.clone(), + N_tilde: dlog_statement[s[ind]].N.clone(), + }; + let ver_res = + pdl_w_slack_proof_vec[j].verify(&pdl_w_slack_statement); + if ver_res.is_err() { + bad_actors_vec.push(i); + false + } else { + true + } + }) + .all(|x| x); + if proofs_verification { + return Ok(()) + } + } + + let err_type = ErrorType { + error_type: "Bad PDLwSlack proof".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + Err(err_type) + } + + pub fn phase5_check_R_dash_sum( + R_dash_vec: &[Point], + ) -> Result<(), Error> { + let sum = R_dash_vec + .iter() + .fold(Point::generator().to_point(), |acc, x| acc + x); + match sum - &Point::generator().to_point() == + Point::generator().to_point() + { + true => Ok(()), + false => Err(Phase5BadSum), + } + } + + pub fn phase6_compute_S_i_and_proof_of_consistency( + R: &Point, + T: &Point, + sigma: &Scalar, + l: &Scalar, + ) -> (Point, HomoELGamalProof) { + let S = R * sigma; + let delta = HomoElGamalStatement { + G: R.clone(), + H: Point::::base_point2().clone(), + Y: Point::generator().to_point(), + D: T.clone(), + E: S.clone(), + }; + let witness = HomoElGamalWitness { x: l.clone(), r: sigma.clone() }; + let proof = HomoELGamalProof::prove(&witness, &delta); + + (S, proof) + } + + pub fn phase6_verify_proof( + S_vec: &[Point], + proof_vec: &[HomoELGamalProof], + R_vec: &[Point], + T_vec: &[Point], + ) -> Result<(), ErrorType> { + let mut bad_actors_vec = Vec::new(); + let mut verify_proofs = true; + for i in 0..proof_vec.len() { + let delta = HomoElGamalStatement { + G: R_vec[i].clone(), + H: Point::::base_point2().clone(), + Y: Point::generator().to_point(), + D: T_vec[i].clone(), + E: S_vec[i].clone(), + }; + if proof_vec[i].verify(&delta).is_err() { + verify_proofs = false; + bad_actors_vec.push(i); + }; + } + + match verify_proofs { + true => Ok(()), + false => { + let err_type = ErrorType { + error_type: "phase6".to_string(), + bad_actors: bad_actors_vec, + data: Vec::new(), + }; + Err(err_type) + }, + } + } + + pub fn phase6_check_S_i_sum( + pubkey_y: &Point, + S_vec: &[Point], + ) -> Result<(), Error> { + let sum_plus_g = + S_vec.iter().fold(Point::generator().to_point(), |acc, x| acc + x); + let sum = sum_plus_g - &Point::generator().to_point(); + + match &sum == pubkey_y { + true => Ok(()), + false => Err(Phase6Error), + } + } + + pub fn phase7_local_sig( + k_i: &Scalar, + message: &BigInt, + R: &Point, + sigma_i: &Scalar, + pubkey: &Point, + ) -> Self { + let m_fe = Scalar::::from(message); + let r = Scalar::::from( + &R.x_coord().unwrap().mod_floor(Scalar::::group_order()), + ); + let s_i = m_fe * k_i + &r * sigma_i; + Self { r, R: R.clone(), s_i, m: message.clone(), y: pubkey.clone() } + } + + pub fn output_signature( + &self, + s_vec: &[Scalar], + ) -> Result { + let mut s = s_vec.iter().fold(self.s_i.clone(), |acc, x| acc + x); + let s_bn = s.to_bigint(); + + let r = Scalar::::from( + &self + .R + .x_coord() + .unwrap() + .mod_floor(Scalar::::group_order()), + ); + let ry: BigInt = self + .R + .y_coord() + .unwrap() + .mod_floor(Scalar::::group_order()); + + /* + Calculate recovery id - it is not possible to compute the public key out of the signature + itself. Recovery id is used to enable extracting the public key uniquely. + 1. id = R.y & 1 + 2. if (s > curve.q / 2) id = id ^ 1 + */ + let is_ry_odd = ry.test_bit(0); + let mut recid = if is_ry_odd { 1 } else { 0 }; + let s_tag_bn = Scalar::::group_order() - &s_bn; + if s_bn > s_tag_bn { + s = Scalar::::from(&s_tag_bn); + recid ^= 1; + } + let sig = SignatureRecid { r, s, recid }; + let ver = verify(&sig, &self.y, &self.m).is_ok(); + if ver { + Ok(sig) + } else { + Err(InvalidSig) + } + } } -pub fn verify(sig: &SignatureRecid, y: &Point, message: &BigInt) -> Result<(), Error> { - let b = sig.s.invert().unwrap(); - let a = Scalar::::from(message); - let u1 = a * &b; - let u2 = &sig.r * &b; - - let g = Point::generator(); - let gu1 = g * u1; - let yu2 = y * &u2; - // can be faster using shamir trick - - if sig.r == - Scalar::::from( - &(gu1 + yu2).x_coord().unwrap().mod_floor(Scalar::::group_order()), - ) { - Ok(()) - } else { - Err(InvalidSig) - } +pub fn verify( + sig: &SignatureRecid, + y: &Point, + message: &BigInt, +) -> Result<(), Error> { + let b = sig.s.invert().unwrap(); + let a = Scalar::::from(message); + let u1 = a * &b; + let u2 = &sig.r * &b; + + let g = Point::generator(); + let gu1 = g * u1; + let yu2 = y * &u2; + // can be faster using shamir trick + + if sig.r == + Scalar::::from( + &(gu1 + yu2) + .x_coord() + .unwrap() + .mod_floor(Scalar::::group_order()), + ) + { + Ok(()) + } else { + Err(InvalidSig) + } } diff --git a/src/presign/mod.rs b/src/presign/mod.rs index 57aa3105..096b9092 100644 --- a/src/presign/mod.rs +++ b/src/presign/mod.rs @@ -1,17 +1,17 @@ /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies. + Copyright 2022 by Webb Technologies. - This file is part of cggmp library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of cggmp library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - cggmp-threshold-ecdsa is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + cggmp-threshold-ecdsa is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ use std::collections::HashMap; @@ -26,15 +26,17 @@ use paillier::{DecryptionKey, EncryptionKey}; use sha2::Sha256; use crate::utilities::{ - aff_g::{ - PaillierAffineOpWithGroupComInRangeProof, PaillierAffineOpWithGroupComInRangeStatement, - }, - dec_q::{PaillierDecryptionModQProof, PaillierDecryptionModQStatement}, - enc::{PaillierEncryptionInRangeProof, PaillierEncryptionInRangeStatement}, - log_star::{ - KnowledgeOfExponentPaillierEncryptionProof, KnowledgeOfExponentPaillierEncryptionStatement, - }, - mul::{PaillierMulProof, PaillierMulStatement}, + aff_g::{ + PaillierAffineOpWithGroupComInRangeProof, + PaillierAffineOpWithGroupComInRangeStatement, + }, + dec_q::{PaillierDecryptionModQProof, PaillierDecryptionModQStatement}, + enc::{PaillierEncryptionInRangeProof, PaillierEncryptionInRangeStatement}, + log_star::{ + KnowledgeOfExponentPaillierEncryptionProof, + KnowledgeOfExponentPaillierEncryptionStatement, + }, + mul::{PaillierMulProof, PaillierMulStatement}, }; use serde::{Deserialize, Serialize}; @@ -44,181 +46,193 @@ pub mod rounds; pub mod state_machine; pub fn DEFAULT_ENCRYPTION_KEY() -> EncryptionKey { - EncryptionKey { n: BigInt::zero(), nn: BigInt::zero() } + EncryptionKey { n: BigInt::zero(), nn: BigInt::zero() } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SSID { - // Group generator and order - pub g: Point, - pub q: BigInt, - // Parties - pub P: Vec, - pub rid: [u8; 32], - pub X: LocalKey, - pub Y: Option>, - // Pedersen parameters - pub N: BigInt, - pub S: BigInt, - pub T: BigInt, + // Group generator and order + pub g: Point, + pub q: BigInt, + // Parties + pub P: Vec, + pub rid: [u8; 32], + pub X: LocalKey, + pub Y: Option>, + // Pedersen parameters + pub N: BigInt, + pub S: BigInt, + pub T: BigInt, } impl Zeroize for SSID { - fn zeroize(&mut self) { - self.q.zeroize(); - self.P.zeroize(); - self.rid.zeroize(); - // X zeroize - self.X.paillier_dk.p.zeroize(); - self.X.paillier_dk.q.zeroize(); - // TODO: Ensure this clears memory or zeroize directly - // FIXME: This is a hack in the meantime until we are sure the memory is cleared. - self.X.pk_vec = vec![]; - - for encryption_key in self.X.paillier_key_vec.iter_mut() { - (*encryption_key).n.zeroize(); - (*encryption_key).nn.zeroize(); - } - // TODO: Zeroize directly if this is insufficient - self.X.y_sum_s = Point::zero(); - for dlog_statement in self.X.h1_h2_n_tilde_vec.iter_mut() { - (*dlog_statement).N.zeroize(); - (*dlog_statement).g.zeroize(); - (*dlog_statement).ni.zeroize(); - } - self.X.vss_scheme.parameters.threshold.zeroize(); - self.X.vss_scheme.parameters.share_count.zeroize(); - // TODO: Zeroize directly if this is insufficient - self.X.vss_scheme.commitments = vec![]; - self.X.i.zeroize(); - self.X.t.zeroize(); - self.X.n.zeroize(); - // Y zeroize - // TODO: Zeroize directly if this is insufficient - self.Y = None; - self.N.zeroize(); - self.S.zeroize(); - self.T.zeroize(); - } + fn zeroize(&mut self) { + self.q.zeroize(); + self.P.zeroize(); + self.rid.zeroize(); + // X zeroize + self.X.paillier_dk.p.zeroize(); + self.X.paillier_dk.q.zeroize(); + // TODO: Ensure this clears memory or zeroize directly + // FIXME: This is a hack in the meantime until we are sure the memory is + // cleared. + self.X.pk_vec = vec![]; + + for encryption_key in self.X.paillier_key_vec.iter_mut() { + (*encryption_key).n.zeroize(); + (*encryption_key).nn.zeroize(); + } + // TODO: Zeroize directly if this is insufficient + self.X.y_sum_s = Point::zero(); + for dlog_statement in self.X.h1_h2_n_tilde_vec.iter_mut() { + (*dlog_statement).N.zeroize(); + (*dlog_statement).g.zeroize(); + (*dlog_statement).ni.zeroize(); + } + self.X.vss_scheme.parameters.threshold.zeroize(); + self.X.vss_scheme.parameters.share_count.zeroize(); + // TODO: Zeroize directly if this is insufficient + self.X.vss_scheme.commitments = vec![]; + self.X.i.zeroize(); + self.X.t.zeroize(); + self.X.n.zeroize(); + // Y zeroize + // TODO: Zeroize directly if this is insufficient + self.Y = None; + self.N.zeroize(); + self.S.zeroize(); + self.T.zeroize(); + } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PreSigningSecrets { - pub x_i: BigInt, - pub y_i: Option, - pub ek: EncryptionKey, - pub dk: DecryptionKey, + pub x_i: BigInt, + pub y_i: Option, + pub ek: EncryptionKey, + pub dk: DecryptionKey, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PreSigningP2PMessage1 { - pub ssid: SSID, - pub i: u16, - pub K_i: BigInt, - pub G_i: BigInt, - pub ek: EncryptionKey, - pub psi_0_j_i: PaillierEncryptionInRangeProof, - pub enc_j_statement: PaillierEncryptionInRangeStatement, + pub ssid: SSID, + pub i: u16, + pub K_i: BigInt, + pub G_i: BigInt, + pub ek: EncryptionKey, + pub psi_0_j_i: PaillierEncryptionInRangeProof, + pub enc_j_statement: PaillierEncryptionInRangeStatement, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PreSigningP2PMessage2 { - pub ssid: SSID, - pub i: u16, - pub Gamma_i: Point, - pub D_j_i: BigInt, - pub F_j_i: BigInt, - pub D_hat_j_i: BigInt, - pub F_hat_j_i: BigInt, - pub psi_j_i: PaillierAffineOpWithGroupComInRangeProof, - pub statement_psi_j_i: PaillierAffineOpWithGroupComInRangeStatement, - pub psi_hat_j_i: PaillierAffineOpWithGroupComInRangeProof, - pub statement_psi_hat_j_i: PaillierAffineOpWithGroupComInRangeStatement, - pub psi_prime_j_i: KnowledgeOfExponentPaillierEncryptionProof, - pub statement_psi_prime_j_i: KnowledgeOfExponentPaillierEncryptionStatement, + pub ssid: SSID, + pub i: u16, + pub Gamma_i: Point, + pub D_j_i: BigInt, + pub F_j_i: BigInt, + pub D_hat_j_i: BigInt, + pub F_hat_j_i: BigInt, + pub psi_j_i: PaillierAffineOpWithGroupComInRangeProof, + pub statement_psi_j_i: + PaillierAffineOpWithGroupComInRangeStatement, + pub psi_hat_j_i: PaillierAffineOpWithGroupComInRangeProof, + pub statement_psi_hat_j_i: + PaillierAffineOpWithGroupComInRangeStatement, + pub psi_prime_j_i: KnowledgeOfExponentPaillierEncryptionProof, + pub statement_psi_prime_j_i: + KnowledgeOfExponentPaillierEncryptionStatement, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PreSigningP2PMessage3 { - pub ssid: SSID, - pub i: u16, - pub delta_i: BigInt, - pub Delta_i: Point, - pub psi_prime_prime_j_i: KnowledgeOfExponentPaillierEncryptionProof, - pub statement_psi_prime_prime_j_i: KnowledgeOfExponentPaillierEncryptionStatement, + pub ssid: SSID, + pub i: u16, + pub delta_i: BigInt, + pub Delta_i: Point, + pub psi_prime_prime_j_i: + KnowledgeOfExponentPaillierEncryptionProof, + pub statement_psi_prime_prime_j_i: + KnowledgeOfExponentPaillierEncryptionStatement, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PresigningOutput { - pub ssid: SSID, - pub R: Point, - pub i: u16, - pub k_i: BigInt, - pub chi_i: BigInt, + pub ssid: SSID, + pub R: Point, + pub i: u16, + pub k_i: BigInt, + pub chi_i: BigInt, } impl Zeroize for PresigningOutput { - fn zeroize(&mut self) { - self.ssid.zeroize(); - // TODO: zeroize R - self.R = Point::zero(); - self.i.zeroize(); - self.k_i.zeroize(); - self.chi_i.zeroize(); - } + fn zeroize(&mut self) { + self.ssid.zeroize(); + // TODO: zeroize R + self.R = Point::zero(); + self.i.zeroize(); + self.k_i.zeroize(); + self.chi_i.zeroize(); + } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PresigningTranscript { - pub ssid: SSID, - pub secrets: PreSigningSecrets, - pub eks: HashMap, - pub gamma_i: BigInt, - pub Gamma_i: Point, - pub Gammas: HashMap>, - pub Gamma: Point, - pub k_i: BigInt, - pub nu_i: BigInt, - pub rho_i: BigInt, - pub G_i: BigInt, - pub K_i: BigInt, - pub G: HashMap, - pub K: HashMap, - pub beta_i: HashMap, - pub beta_hat_i: HashMap, - pub r_i: HashMap, - pub r_hat_i: HashMap, - pub s_i: HashMap, - pub s_hat_i: HashMap, - pub delta_i: BigInt, - pub chi_i: BigInt, - pub Delta_i: Point, - pub deltas: HashMap, - pub Deltas: HashMap>, - pub delta: BigInt, - pub D_j: HashMap, - pub D_hat_j: HashMap, - pub F_j: HashMap, - pub F_hat_j: HashMap, - pub D_i: HashMap, - pub D_hat_i: HashMap, - pub F_i: HashMap, - pub F_hat_i: HashMap, - pub alpha_i: HashMap, - pub alpha_hat_i: HashMap, - pub S: HashMap, - pub T: HashMap, - pub N_hats: HashMap, + pub ssid: SSID, + pub secrets: PreSigningSecrets, + pub eks: HashMap, + pub gamma_i: BigInt, + pub Gamma_i: Point, + pub Gammas: HashMap>, + pub Gamma: Point, + pub k_i: BigInt, + pub nu_i: BigInt, + pub rho_i: BigInt, + pub G_i: BigInt, + pub K_i: BigInt, + pub G: HashMap, + pub K: HashMap, + pub beta_i: HashMap, + pub beta_hat_i: HashMap, + pub r_i: HashMap, + pub r_hat_i: HashMap, + pub s_i: HashMap, + pub s_hat_i: HashMap, + pub delta_i: BigInt, + pub chi_i: BigInt, + pub Delta_i: Point, + pub deltas: HashMap, + pub Deltas: HashMap>, + pub delta: BigInt, + pub D_j: HashMap, + pub D_hat_j: HashMap, + pub F_j: HashMap, + pub F_hat_j: HashMap, + pub D_i: HashMap, + pub D_hat_i: HashMap, + pub F_i: HashMap, + pub F_hat_i: HashMap, + pub alpha_i: HashMap, + pub alpha_hat_i: HashMap, + pub S: HashMap, + pub T: HashMap, + pub N_hats: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IdentifiableAbortBroadcastMessage { - pub i: u16, - pub statements_D_j_i: - HashMap<(u16, u16), PaillierAffineOpWithGroupComInRangeStatement>, - pub proofs_D_j_i: HashMap<(u16, u16), PaillierAffineOpWithGroupComInRangeProof>, - pub statement_H_i: PaillierMulStatement, - pub proof_H_i: PaillierMulProof, - pub statement_delta_i: HashMap>, - pub proof_delta_i: HashMap>, + pub i: u16, + pub statements_D_j_i: HashMap< + (u16, u16), + PaillierAffineOpWithGroupComInRangeStatement, + >, + pub proofs_D_j_i: HashMap< + (u16, u16), + PaillierAffineOpWithGroupComInRangeProof, + >, + pub statement_H_i: PaillierMulStatement, + pub proof_H_i: PaillierMulProof, + pub statement_delta_i: + HashMap>, + pub proof_delta_i: HashMap>, } diff --git a/src/presign/rounds.rs b/src/presign/rounds.rs index d95c1289..5855b18b 100644 --- a/src/presign/rounds.rs +++ b/src/presign/rounds.rs @@ -1,595 +1,685 @@ /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies. + Copyright 2022 by Webb Technologies. - This file is part of cggmp library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of cggmp library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - cggmp-threshold-ecdsa is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + cggmp-threshold-ecdsa is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ use std::{collections::HashMap, marker::PhantomData}; use crate::{ - utilities::{ - aff_g::{ - PaillierAffineOpWithGroupComInRangeProof, PaillierAffineOpWithGroupComInRangeStatement, - PaillierAffineOpWithGroupComInRangeWitness, - }, - dec_q::{ - PaillierDecryptionModQProof, PaillierDecryptionModQStatement, - PaillierDecryptionModQWitness, - }, - enc::{ - PaillierEncryptionInRangeProof, PaillierEncryptionInRangeStatement, - PaillierEncryptionInRangeWitness, - }, - log_star::{ - KnowledgeOfExponentPaillierEncryptionProof, - KnowledgeOfExponentPaillierEncryptionStatement, - KnowledgeOfExponentPaillierEncryptionWitness, - }, - mul::{PaillierMulProof, PaillierMulStatement, PaillierMulWitness}, - sample_relatively_prime_integer, L_PRIME, - }, - ErrorType, ProofVerificationErrorData, + utilities::{ + aff_g::{ + PaillierAffineOpWithGroupComInRangeProof, + PaillierAffineOpWithGroupComInRangeStatement, + PaillierAffineOpWithGroupComInRangeWitness, + }, + dec_q::{ + PaillierDecryptionModQProof, PaillierDecryptionModQStatement, + PaillierDecryptionModQWitness, + }, + enc::{ + PaillierEncryptionInRangeProof, PaillierEncryptionInRangeStatement, + PaillierEncryptionInRangeWitness, + }, + log_star::{ + KnowledgeOfExponentPaillierEncryptionProof, + KnowledgeOfExponentPaillierEncryptionStatement, + KnowledgeOfExponentPaillierEncryptionWitness, + }, + mul::{PaillierMulProof, PaillierMulStatement, PaillierMulWitness}, + sample_relatively_prime_integer, L_PRIME, + }, + ErrorType, ProofVerificationErrorData, }; use super::{ - IdentifiableAbortBroadcastMessage, PreSigningP2PMessage1, PreSigningP2PMessage2, - PreSigningP2PMessage3, PreSigningSecrets, PresigningOutput, PresigningTranscript, - DEFAULT_ENCRYPTION_KEY, SSID, + IdentifiableAbortBroadcastMessage, PreSigningP2PMessage1, + PreSigningP2PMessage2, PreSigningP2PMessage3, PreSigningSecrets, + PresigningOutput, PresigningTranscript, DEFAULT_ENCRYPTION_KEY, SSID, }; use curv::{ - arithmetic::{traits::*, Modulo, Samplable}, - cryptographic_primitives::secret_sharing::feldman_vss::VerifiableSS, - elliptic::curves::{Point, Scalar, Secp256k1}, - BigInt, + arithmetic::{traits::*, Modulo, Samplable}, + cryptographic_primitives::secret_sharing::feldman_vss::VerifiableSS, + elliptic::curves::{Point, Scalar, Secp256k1}, + BigInt, }; use paillier::{ - Add, Decrypt, EncryptWithChosenRandomness, EncryptionKey, Mul, Paillier, Randomness, - RawCiphertext, RawPlaintext, + Add, Decrypt, EncryptWithChosenRandomness, EncryptionKey, Mul, Paillier, + Randomness, RawCiphertext, RawPlaintext, }; use round_based::{ - containers::{push::Push, BroadcastMsgs, BroadcastMsgsStore, P2PMsgs, P2PMsgsStore}, - Msg, + containers::{ + push::Push, BroadcastMsgs, BroadcastMsgsStore, P2PMsgs, P2PMsgsStore, + }, + Msg, }; use sha2::Sha256; use thiserror::Error; -use super::state_machine::{Round0Messages, Round1Messages, Round2Messages, Round3Messages}; +use super::state_machine::{ + Round0Messages, Round1Messages, Round2Messages, Round3Messages, +}; pub struct Round0 { - pub ssid: SSID, - pub secrets: PreSigningSecrets, - pub S: HashMap, - pub T: HashMap, - pub N_hats: HashMap, - pub l: usize, // This is the number of presignings to run in parallel + pub ssid: SSID, + pub secrets: PreSigningSecrets, + pub S: HashMap, + pub T: HashMap, + pub N_hats: HashMap, + pub l: usize, // This is the number of presignings to run in parallel } impl Round0 { - pub fn proceed(self, mut output: O) -> Result - where - O: Push>>>, - { - // k_i <- F_q - let k_i = BigInt::sample_below(&self.ssid.q); - // gamma_i <- F_q - let gamma_i = BigInt::sample_below(&self.ssid.q); - // rho_i <- Z*_{N_i} - let rho_i = sample_relatively_prime_integer(&self.secrets.ek.n); - // nu_i <- Z*_{N_i} - let nu_i = sample_relatively_prime_integer(&self.secrets.ek.n); - // G_i = enc_i(gamma_i; nu_i) - let G_i: BigInt = Paillier::encrypt_with_chosen_randomness( - &self.secrets.ek, - RawPlaintext::from(gamma_i.clone()), - &Randomness::from(nu_i.clone()), - ) - .into(); - // K_i = enc_i(k_i; rho_i) - let K_i: BigInt = Paillier::encrypt_with_chosen_randomness( - &self.secrets.ek, - RawPlaintext::from(k_i.clone()), - &Randomness(rho_i.clone()), - ) - .into(); - let witness_psi_0_j_i = PaillierEncryptionInRangeWitness::new(k_i.clone(), rho_i.clone()); - - for j in self.ssid.P.iter() { - if *j != self.ssid.X.i { - let statement_psi_0_j_i = PaillierEncryptionInRangeStatement { - N0: self.secrets.ek.n.clone(), - NN0: self.secrets.ek.nn.clone(), - K: K_i.clone(), - s: self.S.get(j).unwrap_or(&BigInt::zero()).clone(), - t: self.T.get(j).unwrap_or(&BigInt::zero()).clone(), - N_hat: self.N_hats.get(j).unwrap_or(&BigInt::zero()).clone(), - phantom: PhantomData, - }; - let psi_0_j_i = PaillierEncryptionInRangeProof::::prove( - &witness_psi_0_j_i, - &statement_psi_0_j_i, - ); - - let body = PreSigningP2PMessage1 { - ssid: self.ssid.clone(), - i: self.ssid.X.i, - K_i: K_i.clone(), - G_i: G_i.clone(), - psi_0_j_i, - enc_j_statement: statement_psi_0_j_i, - ek: self.secrets.ek.clone(), - }; - output.push(Msg { - sender: self.ssid.X.i, - receiver: Some(*j), - body: Box::new(body), - }); - } - } - Ok(Round1 { - ssid: self.ssid, - secrets: self.secrets, - gamma_i, - k_i, - nu_i, - rho_i, - G_i, - K_i, - S: self.S, - T: self.T, - N_hats: self.N_hats, - }) - } - pub fn is_expensive(&self) -> bool { - false - } + pub fn proceed(self, mut output: O) -> Result + where + O: Push>>>, + { + // k_i <- F_q + let k_i = BigInt::sample_below(&self.ssid.q); + // gamma_i <- F_q + let gamma_i = BigInt::sample_below(&self.ssid.q); + // rho_i <- Z*_{N_i} + let rho_i = sample_relatively_prime_integer(&self.secrets.ek.n); + // nu_i <- Z*_{N_i} + let nu_i = sample_relatively_prime_integer(&self.secrets.ek.n); + // G_i = enc_i(gamma_i; nu_i) + let G_i: BigInt = Paillier::encrypt_with_chosen_randomness( + &self.secrets.ek, + RawPlaintext::from(gamma_i.clone()), + &Randomness::from(nu_i.clone()), + ) + .into(); + // K_i = enc_i(k_i; rho_i) + let K_i: BigInt = Paillier::encrypt_with_chosen_randomness( + &self.secrets.ek, + RawPlaintext::from(k_i.clone()), + &Randomness(rho_i.clone()), + ) + .into(); + let witness_psi_0_j_i = + PaillierEncryptionInRangeWitness::new(k_i.clone(), rho_i.clone()); + + for j in self.ssid.P.iter() { + if *j != self.ssid.X.i { + let statement_psi_0_j_i = PaillierEncryptionInRangeStatement { + N0: self.secrets.ek.n.clone(), + NN0: self.secrets.ek.nn.clone(), + K: K_i.clone(), + s: self.S.get(j).unwrap_or(&BigInt::zero()).clone(), + t: self.T.get(j).unwrap_or(&BigInt::zero()).clone(), + N_hat: self + .N_hats + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + phantom: PhantomData, + }; + let psi_0_j_i = + PaillierEncryptionInRangeProof::::prove( + &witness_psi_0_j_i, + &statement_psi_0_j_i, + ); + + let body = PreSigningP2PMessage1 { + ssid: self.ssid.clone(), + i: self.ssid.X.i, + K_i: K_i.clone(), + G_i: G_i.clone(), + psi_0_j_i, + enc_j_statement: statement_psi_0_j_i, + ek: self.secrets.ek.clone(), + }; + output.push(Msg { + sender: self.ssid.X.i, + receiver: Some(*j), + body: Box::new(body), + }); + } + } + Ok(Round1 { + ssid: self.ssid, + secrets: self.secrets, + gamma_i, + k_i, + nu_i, + rho_i, + G_i, + K_i, + S: self.S, + T: self.T, + N_hats: self.N_hats, + }) + } + pub fn is_expensive(&self) -> bool { + false + } } pub struct Round1 { - pub ssid: SSID, - pub secrets: PreSigningSecrets, - pub gamma_i: BigInt, - pub k_i: BigInt, - pub nu_i: BigInt, - pub rho_i: BigInt, - pub G_i: BigInt, - pub K_i: BigInt, - pub S: HashMap, - pub T: HashMap, - pub N_hats: HashMap, + pub ssid: SSID, + pub secrets: PreSigningSecrets, + pub gamma_i: BigInt, + pub k_i: BigInt, + pub nu_i: BigInt, + pub rho_i: BigInt, + pub G_i: BigInt, + pub K_i: BigInt, + pub S: HashMap, + pub T: HashMap, + pub N_hats: HashMap, } impl Round1 { - pub fn proceed( - self, - input: P2PMsgs>, - mut output: O, - ) -> Result - where - O: Push>>>, - { - // Since x_i is a (t,n) share of x, we need to transform it to a (t,t+1) share omega_i using - // the appropriate lagrangian coefficient lambda_{i,S} as defined by GG18 and GG20. - // We then use omega_i in place of x_i for the rest of the protocol. - // Ref: https://eprint.iacr.org/2021/060.pdf (Section 1.2.8) - // Ref: https://eprint.iacr.org/2019/114.pdf (Section 4.2) - // Ref: https://eprint.iacr.org/2020/540.pdf (Section 3.2) - let lambda_i_s = VerifiableSS::::map_share_to_new_params( - &self.ssid.X.vss_scheme.parameters, - self.ssid.X.i - 1, - &self.ssid.P.iter().map(|i| i - 1).collect::>(), - ); - let omega_i = BigInt::mod_mul(&lambda_i_s.to_bigint(), &self.secrets.x_i, &self.ssid.q); - - let mut K: HashMap = HashMap::new(); - let mut G: HashMap = HashMap::new(); - let mut eks: HashMap = HashMap::new(); - // Verify P2P Messages - for msg in input.into_vec() { - // j - let j = msg.i; - // Insert K_j - K.insert(j, msg.K_i); - // Insert G_j - G.insert(j, msg.G_i); - // Insert j's Paillier encryption key - eks.insert(j, msg.ek); - let psi_0_i_j = msg.psi_0_j_i; - let enc_i_statement = msg.enc_j_statement; - // Verify psi_0_i_j proof - if PaillierEncryptionInRangeProof::::verify( - &psi_0_i_j, - &enc_i_statement, - ) - .is_err() - { - let error_data = ProofVerificationErrorData { - proof_symbol: "psi_0_i_j".to_string(), - verifying_party: self.ssid.X.i, - }; - return Err(PresignError::ProofVerificationError(ErrorType { - error_type: "enc".to_string(), - bad_actors: vec![j.into()], - data: bincode::serialize(&error_data).unwrap(), - })) - } - } - - // Gamma_i = g^{gamma_i} - let Gamma_i = - Point::::generator().as_point() * Scalar::from_bigint(&self.gamma_i); - // {beta, beta_hat, r, r_hat, s, s_hat}_i will store mapping from j to {beta, beta_hat, r, - // r_hat, s, s_hat}_i_j. - let mut beta_i: HashMap = HashMap::new(); - let mut beta_hat_i: HashMap = HashMap::new(); - let mut r_i: HashMap = HashMap::new(); - let mut r_hat_i: HashMap = HashMap::new(); - let mut s_i: HashMap = HashMap::new(); - let mut s_hat_i: HashMap = HashMap::new(); - let mut D_j: HashMap = HashMap::new(); - let mut F_j: HashMap = HashMap::new(); - let mut D_hat_j: HashMap = HashMap::new(); - let mut F_hat_j: HashMap = HashMap::new(); - - for j in self.ssid.P.iter() { - if j != &self.ssid.X.i { - // r_i_j <- Z_{N_j} - let r_i_j = - BigInt::sample_below(&eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).n); - r_i.insert(*j, r_i_j.clone()); - // s_i_j <- Z_{N_j} - let s_i_j = - BigInt::sample_below(&eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).n); - s_i.insert(*j, s_i_j.clone()); - // r_hat_i_j <- Z_{N_j} - let r_hat_i_j = - BigInt::sample_below(&eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).n); - r_hat_i.insert(*j, r_hat_i_j.clone()); - // s_hat_i_j <- Z_{N_j} - let s_hat_i_j = - BigInt::sample_below(&eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).n); - s_hat_i.insert(*j, s_hat_i_j.clone()); - let upper = BigInt::pow(&BigInt::from(2), L_PRIME as u32); - let lower = BigInt::from(-1).mul(&upper); - // beta_i_j <- [-2^L_PRIME, 2^L_PRIME] - let beta_i_j = BigInt::sample_range(&lower, &upper); - beta_i.insert(*j, beta_i_j.clone()); - // beta_hat_i_j <- [-2^L_PRIME, 2^L_PRIME] - let beta_hat_i_j = BigInt::sample_range(&lower, &upper); - beta_hat_i.insert(*j, beta_hat_i_j.clone()); - - let encrypt_minus_beta_i_j = Paillier::encrypt_with_chosen_randomness( - eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), - RawPlaintext::from(BigInt::from(-1).mul(&beta_i_j.clone())), - &Randomness::from(s_i_j.clone()), - ); - // D_j_i = (gamma_i [.] K_j ) ⊕ enc_j(-beta_i_j; s_i_j) where [.] is Paillier - // multiplication - let D_j_i: BigInt = Paillier::add( - eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), - Paillier::mul( - eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), - RawCiphertext::from(K.get(j).unwrap_or(&BigInt::zero())), - RawPlaintext::from(self.gamma_i.clone()), - ), - encrypt_minus_beta_i_j, - ) - .into(); - - D_j.insert(*j, D_j_i.clone()); - - // F_j_i = enc_i(beta_i_j, r_i_j) - let F_j_i: BigInt = Paillier::encrypt_with_chosen_randomness( - &self.secrets.ek, - // To compute F_j_i, beta_i_j is NOT multiplied by -1 in the paper - // (see Figure 7, Round 2 in the paper), but that makes Π^aff-g ZK proof - // verification fail (see Figure 15 in the paper). This is because Π^aff-g - // states: - // - Y = (1 + N_1)^y ρ_y ^ N_1 - // - D = C^x (1 + N_0)^y ρ ^ N_0 - // And from Figure 7, Round 2 we can see that: - // - x = gamma_i - // - y = beta_i_j - // - ρ = s_i_j - // - ρ_y = r_i_j - // - Y = F_j_i i.e (1 + N_j)^{beta_i_j} r_i_j ^ {N_j} - // :- (1) enc_j(beta_i_j, r_i_j) - // - C = K_j - // - D = D_j_i i.e K ^ {gamma_i} (1 + N_i)^{beta_i_j} s_i_j ^ {N_i} - // :- (2) D ≡ gamma_i ⊙ K_j ⊕ enc_i(beta_i_j, s_i_j) - // - // From (1) and (2) we can see the mismatch between Π^aff-g (see Figure 15) and - // pre-signing definitions (see Figure 7, Round 2) i.e the Π^aff-g verification - // will fail unless we apply (or don't apply) negation uniformly to beta_i_j - // before encrypting it to generate F_j_i and D_j_i. - // - // We have 2 options to solve the sign mismatch: - // - (A). We can redefine y as negative beta_i_j, update F_j_i to encrypt a - // negative beta_i_j, and leave D_j_i unchanged. - // - (B). We can redefine D_j_i to encrypt a positive beta_i_j, leave y and - // F_j_i unchanged, and modify Figure 7, Round 3 to subtract beta_i_j instead - // of adding it when computing delta_i. - // - // We choose option (A) since it entails less modifications to the overall - // protocol (i.e all changes are performed in Round 2). - // - // NOTE: A similar transformation has to be applied to the "hat" variants (i.e - // beta_hat_i_j, F_hat_j_i, D_hat_j_i) as well. - // (see also https://en.wikipedia.org/wiki/Paillier_cryptosystem#Homomorphic_properties) - RawPlaintext::from(BigInt::from(-1).mul(&beta_i_j.clone())), - &Randomness::from(r_i_j.clone()), - ) - .into(); - - F_j.insert(*j, F_j_i.clone()); - - // Compute D_hat_j_i - let encrypt_minus_beta_hat_i_j = Paillier::encrypt_with_chosen_randomness( - eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), - RawPlaintext::from(BigInt::from(-1).mul(&beta_hat_i_j)), - &Randomness::from(s_hat_i_j.clone()), - ); - // D_hat_j_i = (x_i [.] K_j ) ⊕ enc_j(-beta_hat_i_j; s_hat_i_j) where [.] is - // Paillier multiplication - let D_hat_j_i: BigInt = Paillier::add( - eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), - Paillier::mul( - eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), - RawCiphertext::from(K.get(j).unwrap_or(&BigInt::zero())), - // We use omega_i in place of x_i, see doc on omega_i definition for - // explanation. - RawPlaintext::from(omega_i.clone()), - ), - encrypt_minus_beta_hat_i_j, - ) - .into(); - - D_hat_j.insert(*j, D_hat_j_i.clone()); - - // F_hat_j_i = enc_i(beta_hat_i_j, r_hat_i_j) - let F_hat_j_i: BigInt = Paillier::encrypt_with_chosen_randomness( - &self.secrets.ek, - // See reasoning documented under F_j_i for why we multiply beta_hat_i_j by -1 - // before encrypting it. - RawPlaintext::from(BigInt::from(-1).mul(&beta_hat_i_j.clone())), - &Randomness::from(r_hat_i_j.clone()), - ) - .into(); - - F_hat_j.insert(*j, F_hat_j_i.clone()); - - // psi_j_i - let witness_psi_j_i = PaillierAffineOpWithGroupComInRangeWitness::new( - self.gamma_i.clone(), - // See reasoning documented under F_j_i for why we multiply beta_i_j by -1. - BigInt::from(-1).mul(&beta_i_j.clone()), - s_i_j.clone(), - r_i_j.clone(), - ); - let statement_psi_j_i = PaillierAffineOpWithGroupComInRangeStatement { - S: self.S.get(j).unwrap_or(&BigInt::zero()).clone(), - T: self.T.get(j).unwrap_or(&BigInt::zero()).clone(), - N_hat: self.N_hats.get(j).unwrap_or(&BigInt::zero()).clone(), - N0: eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).n.clone(), - N1: self.secrets.ek.n.clone(), - NN0: eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).nn.clone(), - NN1: self.secrets.ek.nn.clone(), - C: K.get(j).unwrap_or(&BigInt::zero()).clone(), - D: D_j_i.clone(), - Y: F_j_i.clone(), - X: Gamma_i.clone(), - ek_prover: self.secrets.ek.clone(), - ek_verifier: eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).clone(), - phantom: PhantomData, - }; - let psi_j_i = PaillierAffineOpWithGroupComInRangeProof::::prove( - &witness_psi_j_i, - &statement_psi_j_i, - ); - - // psi_hat_j_i - let witness_psi_hat_j_i = PaillierAffineOpWithGroupComInRangeWitness::new( - // We use omega_i in place of x_i, see doc on omega_i definition for - // explanation. - omega_i.clone(), - // See reasoning documented under F_j_i for why we multiply beta_hat_i_j by -1. - BigInt::from(-1).mul(&beta_hat_i_j.clone()), - s_hat_i_j.clone(), - r_hat_i_j.clone(), - ); - let statement_psi_hat_j_i = PaillierAffineOpWithGroupComInRangeStatement { - S: self.S.get(j).unwrap_or(&BigInt::zero()).clone(), - T: self.T.get(j).unwrap_or(&BigInt::zero()).clone(), - N_hat: self.N_hats.get(j).unwrap_or(&BigInt::zero()).clone(), - N0: eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).n.clone(), - N1: self.secrets.ek.n.clone(), - NN0: eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).nn.clone(), - NN1: self.secrets.ek.nn.clone(), - C: K.get(j).unwrap_or(&BigInt::zero()).clone(), - D: D_hat_j_i.clone(), - Y: F_hat_j_i.clone(), - // We use omega_i in place of x_i, see doc on omega_i definition for - // explanation. - X: Point::::generator().as_point() * Scalar::from_bigint(&omega_i), - ek_prover: self.secrets.ek.clone(), - ek_verifier: eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).clone(), - phantom: PhantomData, - }; - let psi_hat_j_i = - PaillierAffineOpWithGroupComInRangeProof::::prove( - &witness_psi_hat_j_i, - &statement_psi_hat_j_i, - ); - - // psi_prime_j_i - let witness_psi_prime_j_i = KnowledgeOfExponentPaillierEncryptionWitness::new( - self.gamma_i.clone(), - self.nu_i.clone(), - ); - let statement_psi_prime_j_i = KnowledgeOfExponentPaillierEncryptionStatement { - N0: self.secrets.ek.n.clone(), - NN0: self.secrets.ek.nn.clone(), - C: self.G_i.clone(), - X: Gamma_i.clone(), - // g is not always the secp256k1 generator, so we have to pass it explicitly. - // See [`KnowledgeOfExponentPaillierEncryptionStatement`] inline doc for g field - // for details. - g: Point::generator().to_point(), - s: self.S.get(j).unwrap_or(&BigInt::zero()).clone(), - t: self.T.get(j).unwrap_or(&BigInt::zero()).clone(), - N_hat: self.N_hats.get(j).unwrap_or(&BigInt::zero()).clone(), - phantom: PhantomData, - }; - let psi_prime_j_i = - KnowledgeOfExponentPaillierEncryptionProof::::prove( - &witness_psi_prime_j_i, - &statement_psi_prime_j_i, - ); - - // Send Message - let body = PreSigningP2PMessage2 { - ssid: self.ssid.clone(), - i: self.ssid.X.i, - Gamma_i: Gamma_i.clone(), - D_j_i: D_j_i.clone(), - F_j_i: F_j_i.clone(), - D_hat_j_i: D_hat_j_i.clone(), - F_hat_j_i: F_hat_j_i.clone(), - psi_j_i: psi_j_i.clone(), - statement_psi_j_i, - psi_hat_j_i, - statement_psi_hat_j_i, - psi_prime_j_i, - statement_psi_prime_j_i, - }; - output.push(Msg { - sender: self.ssid.X.i, - receiver: Some(*j), - body: Box::new(body), - }); - } - } - Ok(Round2 { - ssid: self.ssid, - secrets: self.secrets, - omega_i, - eks, - gamma_i: self.gamma_i, - k_i: self.k_i, - Gamma_i, - nu_i: self.nu_i, - rho_i: self.rho_i, - G_i: self.G_i, - K_i: self.K_i, - G, - K, - beta_i, - beta_hat_i, - r_i, - r_hat_i, - s_i, - s_hat_i, - D_j, - D_hat_j, - F_j, - F_hat_j, - S: self.S, - T: self.T, - N_hats: self.N_hats, - }) - } - - pub fn is_expensive(&self) -> bool { - false - } - - pub fn expects_messages(i: u16, n: u16) -> Round0Messages { - P2PMsgsStore::new(i, n) - } + pub fn proceed( + self, + input: P2PMsgs>, + mut output: O, + ) -> Result + where + O: Push>>>, + { + // Since x_i is a (t,n) share of x, we need to transform it to a (t,t+1) + // share omega_i using the appropriate lagrangian coefficient + // lambda_{i,S} as defined by GG18 and GG20. We then use omega_i + // in place of x_i for the rest of the protocol. Ref: https://eprint.iacr.org/2021/060.pdf (Section 1.2.8) + // Ref: https://eprint.iacr.org/2019/114.pdf (Section 4.2) + // Ref: https://eprint.iacr.org/2020/540.pdf (Section 3.2) + let lambda_i_s = + VerifiableSS::::map_share_to_new_params( + &self.ssid.X.vss_scheme.parameters, + self.ssid.X.i - 1, + &self.ssid.P.iter().map(|i| i - 1).collect::>(), + ); + let omega_i = BigInt::mod_mul( + &lambda_i_s.to_bigint(), + &self.secrets.x_i, + &self.ssid.q, + ); + + let mut K: HashMap = HashMap::new(); + let mut G: HashMap = HashMap::new(); + let mut eks: HashMap = HashMap::new(); + // Verify P2P Messages + for msg in input.into_vec() { + // j + let j = msg.i; + // Insert K_j + K.insert(j, msg.K_i); + // Insert G_j + G.insert(j, msg.G_i); + // Insert j's Paillier encryption key + eks.insert(j, msg.ek); + let psi_0_i_j = msg.psi_0_j_i; + let enc_i_statement = msg.enc_j_statement; + // Verify psi_0_i_j proof + if PaillierEncryptionInRangeProof::::verify( + &psi_0_i_j, + &enc_i_statement, + ) + .is_err() + { + let error_data = ProofVerificationErrorData { + proof_symbol: "psi_0_i_j".to_string(), + verifying_party: self.ssid.X.i, + }; + return Err(PresignError::ProofVerificationError(ErrorType { + error_type: "enc".to_string(), + bad_actors: vec![j.into()], + data: bincode::serialize(&error_data).unwrap(), + })) + } + } + + // Gamma_i = g^{gamma_i} + let Gamma_i = Point::::generator().as_point() * + Scalar::from_bigint(&self.gamma_i); + // {beta, beta_hat, r, r_hat, s, s_hat}_i will store mapping from j to + // {beta, beta_hat, r, r_hat, s, s_hat}_i_j. + let mut beta_i: HashMap = HashMap::new(); + let mut beta_hat_i: HashMap = HashMap::new(); + let mut r_i: HashMap = HashMap::new(); + let mut r_hat_i: HashMap = HashMap::new(); + let mut s_i: HashMap = HashMap::new(); + let mut s_hat_i: HashMap = HashMap::new(); + let mut D_j: HashMap = HashMap::new(); + let mut F_j: HashMap = HashMap::new(); + let mut D_hat_j: HashMap = HashMap::new(); + let mut F_hat_j: HashMap = HashMap::new(); + + for j in self.ssid.P.iter() { + if j != &self.ssid.X.i { + // r_i_j <- Z_{N_j} + let r_i_j = BigInt::sample_below( + &eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).n, + ); + r_i.insert(*j, r_i_j.clone()); + // s_i_j <- Z_{N_j} + let s_i_j = BigInt::sample_below( + &eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).n, + ); + s_i.insert(*j, s_i_j.clone()); + // r_hat_i_j <- Z_{N_j} + let r_hat_i_j = BigInt::sample_below( + &eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).n, + ); + r_hat_i.insert(*j, r_hat_i_j.clone()); + // s_hat_i_j <- Z_{N_j} + let s_hat_i_j = BigInt::sample_below( + &eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()).n, + ); + s_hat_i.insert(*j, s_hat_i_j.clone()); + let upper = BigInt::pow(&BigInt::from(2), L_PRIME as u32); + let lower = BigInt::from(-1).mul(&upper); + // beta_i_j <- [-2^L_PRIME, 2^L_PRIME] + let beta_i_j = BigInt::sample_range(&lower, &upper); + beta_i.insert(*j, beta_i_j.clone()); + // beta_hat_i_j <- [-2^L_PRIME, 2^L_PRIME] + let beta_hat_i_j = BigInt::sample_range(&lower, &upper); + beta_hat_i.insert(*j, beta_hat_i_j.clone()); + + let encrypt_minus_beta_i_j = + Paillier::encrypt_with_chosen_randomness( + eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), + RawPlaintext::from( + BigInt::from(-1).mul(&beta_i_j.clone()), + ), + &Randomness::from(s_i_j.clone()), + ); + // D_j_i = (gamma_i [.] K_j ) ⊕ enc_j(-beta_i_j; s_i_j) where + // [.] is Paillier multiplication + let D_j_i: BigInt = Paillier::add( + eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), + Paillier::mul( + eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), + RawCiphertext::from( + K.get(j).unwrap_or(&BigInt::zero()), + ), + RawPlaintext::from(self.gamma_i.clone()), + ), + encrypt_minus_beta_i_j, + ) + .into(); + + D_j.insert(*j, D_j_i.clone()); + + // F_j_i = enc_i(beta_i_j, r_i_j) + let F_j_i: BigInt = Paillier::encrypt_with_chosen_randomness( + &self.secrets.ek, + // To compute F_j_i, beta_i_j is NOT multiplied by -1 in + // the paper (see Figure 7, Round 2 in + // the paper), but that makes Π^aff-g ZK proof + // verification fail (see Figure 15 in the paper). This is + // because Π^aff-g states: + // - Y = (1 + N_1)^y ρ_y ^ N_1 + // - D = C^x (1 + N_0)^y ρ ^ N_0 + // And from Figure 7, Round 2 we can see that: + // - x = gamma_i + // - y = beta_i_j + // - ρ = s_i_j + // - ρ_y = r_i_j + // - Y = F_j_i i.e (1 + N_j)^{beta_i_j} r_i_j ^ {N_j} + // :- (1) enc_j(beta_i_j, r_i_j) + // - C = K_j + // - D = D_j_i i.e K ^ {gamma_i} (1 + N_i)^{beta_i_j} s_i_j + // ^ {N_i} + // :- (2) D ≡ gamma_i ⊙ K_j ⊕ enc_i(beta_i_j, s_i_j) + // + // From (1) and (2) we can see the mismatch between Π^aff-g + // (see Figure 15) and pre-signing + // definitions (see Figure 7, Round 2) i.e the Π^aff-g + // verification will fail unless we + // apply (or don't apply) negation uniformly to beta_i_j + // before encrypting it to generate F_j_i and D_j_i. + // + // We have 2 options to solve the sign mismatch: + // - (A). We can redefine y as negative beta_i_j, update + // F_j_i to encrypt a negative beta_i_j, and leave D_j_i + // unchanged. + // - (B). We can redefine D_j_i to encrypt a positive + // beta_i_j, leave y and F_j_i unchanged, and modify + // Figure 7, Round 3 to subtract beta_i_j instead of + // adding it when computing delta_i. + // + // We choose option (A) since it entails less modifications + // to the overall protocol (i.e all + // changes are performed in Round 2). + // + // NOTE: A similar transformation has to be applied to the + // "hat" variants (i.e beta_hat_i_j, + // F_hat_j_i, D_hat_j_i) as well. (see also https://en.wikipedia.org/wiki/Paillier_cryptosystem#Homomorphic_properties) + RawPlaintext::from(BigInt::from(-1).mul(&beta_i_j.clone())), + &Randomness::from(r_i_j.clone()), + ) + .into(); + + F_j.insert(*j, F_j_i.clone()); + + // Compute D_hat_j_i + let encrypt_minus_beta_hat_i_j = + Paillier::encrypt_with_chosen_randomness( + eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), + RawPlaintext::from(BigInt::from(-1).mul(&beta_hat_i_j)), + &Randomness::from(s_hat_i_j.clone()), + ); + // D_hat_j_i = (x_i [.] K_j ) ⊕ enc_j(-beta_hat_i_j; s_hat_i_j) + // where [.] is Paillier multiplication + let D_hat_j_i: BigInt = Paillier::add( + eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), + Paillier::mul( + eks.get(j).unwrap_or(&DEFAULT_ENCRYPTION_KEY()), + RawCiphertext::from( + K.get(j).unwrap_or(&BigInt::zero()), + ), + // We use omega_i in place of x_i, see doc on omega_i + // definition for explanation. + RawPlaintext::from(omega_i.clone()), + ), + encrypt_minus_beta_hat_i_j, + ) + .into(); + + D_hat_j.insert(*j, D_hat_j_i.clone()); + + // F_hat_j_i = enc_i(beta_hat_i_j, r_hat_i_j) + let F_hat_j_i: BigInt = + Paillier::encrypt_with_chosen_randomness( + &self.secrets.ek, + // See reasoning documented under F_j_i for why we + // multiply beta_hat_i_j by -1 + // before encrypting it. + RawPlaintext::from( + BigInt::from(-1).mul(&beta_hat_i_j.clone()), + ), + &Randomness::from(r_hat_i_j.clone()), + ) + .into(); + + F_hat_j.insert(*j, F_hat_j_i.clone()); + + // psi_j_i + let witness_psi_j_i = + PaillierAffineOpWithGroupComInRangeWitness::new( + self.gamma_i.clone(), + // See reasoning documented under F_j_i for why we + // multiply beta_i_j by -1. + BigInt::from(-1).mul(&beta_i_j.clone()), + s_i_j.clone(), + r_i_j.clone(), + ); + let statement_psi_j_i = + PaillierAffineOpWithGroupComInRangeStatement { + S: self.S.get(j).unwrap_or(&BigInt::zero()).clone(), + T: self.T.get(j).unwrap_or(&BigInt::zero()).clone(), + N_hat: self + .N_hats + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + N0: eks + .get(j) + .unwrap_or(&DEFAULT_ENCRYPTION_KEY()) + .n + .clone(), + N1: self.secrets.ek.n.clone(), + NN0: eks + .get(j) + .unwrap_or(&DEFAULT_ENCRYPTION_KEY()) + .nn + .clone(), + NN1: self.secrets.ek.nn.clone(), + C: K.get(j).unwrap_or(&BigInt::zero()).clone(), + D: D_j_i.clone(), + Y: F_j_i.clone(), + X: Gamma_i.clone(), + ek_prover: self.secrets.ek.clone(), + ek_verifier: eks + .get(j) + .unwrap_or(&DEFAULT_ENCRYPTION_KEY()) + .clone(), + phantom: PhantomData, + }; + let psi_j_i = PaillierAffineOpWithGroupComInRangeProof::< + Secp256k1, + Sha256, + >::prove( + &witness_psi_j_i, &statement_psi_j_i + ); + + // psi_hat_j_i + let witness_psi_hat_j_i = + PaillierAffineOpWithGroupComInRangeWitness::new( + // We use omega_i in place of x_i, see doc on omega_i + // definition for explanation. + omega_i.clone(), + // See reasoning documented under F_j_i for why we + // multiply beta_hat_i_j by -1. + BigInt::from(-1).mul(&beta_hat_i_j.clone()), + s_hat_i_j.clone(), + r_hat_i_j.clone(), + ); + let statement_psi_hat_j_i = + PaillierAffineOpWithGroupComInRangeStatement { + S: self.S.get(j).unwrap_or(&BigInt::zero()).clone(), + T: self.T.get(j).unwrap_or(&BigInt::zero()).clone(), + N_hat: self + .N_hats + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + N0: eks + .get(j) + .unwrap_or(&DEFAULT_ENCRYPTION_KEY()) + .n + .clone(), + N1: self.secrets.ek.n.clone(), + NN0: eks + .get(j) + .unwrap_or(&DEFAULT_ENCRYPTION_KEY()) + .nn + .clone(), + NN1: self.secrets.ek.nn.clone(), + C: K.get(j).unwrap_or(&BigInt::zero()).clone(), + D: D_hat_j_i.clone(), + Y: F_hat_j_i.clone(), + // We use omega_i in place of x_i, see doc on omega_i + // definition for explanation. + X: Point::::generator().as_point() * + Scalar::from_bigint(&omega_i), + ek_prover: self.secrets.ek.clone(), + ek_verifier: eks + .get(j) + .unwrap_or(&DEFAULT_ENCRYPTION_KEY()) + .clone(), + phantom: PhantomData, + }; + let psi_hat_j_i = PaillierAffineOpWithGroupComInRangeProof::< + Secp256k1, + Sha256, + >::prove( + &witness_psi_hat_j_i, + &statement_psi_hat_j_i, + ); + + // psi_prime_j_i + let witness_psi_prime_j_i = + KnowledgeOfExponentPaillierEncryptionWitness::new( + self.gamma_i.clone(), + self.nu_i.clone(), + ); + let statement_psi_prime_j_i = + KnowledgeOfExponentPaillierEncryptionStatement { + N0: self.secrets.ek.n.clone(), + NN0: self.secrets.ek.nn.clone(), + C: self.G_i.clone(), + X: Gamma_i.clone(), + // g is not always the secp256k1 generator, so we have + // to pass it explicitly. + // See [`KnowledgeOfExponentPaillierEncryptionStatement`] inline doc for g field + // for details. + g: Point::generator().to_point(), + s: self.S.get(j).unwrap_or(&BigInt::zero()).clone(), + t: self.T.get(j).unwrap_or(&BigInt::zero()).clone(), + N_hat: self + .N_hats + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + phantom: PhantomData, + }; + let psi_prime_j_i = KnowledgeOfExponentPaillierEncryptionProof::< + Secp256k1, + Sha256, + >::prove( + &witness_psi_prime_j_i, + &statement_psi_prime_j_i, + ); + + // Send Message + let body = PreSigningP2PMessage2 { + ssid: self.ssid.clone(), + i: self.ssid.X.i, + Gamma_i: Gamma_i.clone(), + D_j_i: D_j_i.clone(), + F_j_i: F_j_i.clone(), + D_hat_j_i: D_hat_j_i.clone(), + F_hat_j_i: F_hat_j_i.clone(), + psi_j_i: psi_j_i.clone(), + statement_psi_j_i, + psi_hat_j_i, + statement_psi_hat_j_i, + psi_prime_j_i, + statement_psi_prime_j_i, + }; + output.push(Msg { + sender: self.ssid.X.i, + receiver: Some(*j), + body: Box::new(body), + }); + } + } + Ok(Round2 { + ssid: self.ssid, + secrets: self.secrets, + omega_i, + eks, + gamma_i: self.gamma_i, + k_i: self.k_i, + Gamma_i, + nu_i: self.nu_i, + rho_i: self.rho_i, + G_i: self.G_i, + K_i: self.K_i, + G, + K, + beta_i, + beta_hat_i, + r_i, + r_hat_i, + s_i, + s_hat_i, + D_j, + D_hat_j, + F_j, + F_hat_j, + S: self.S, + T: self.T, + N_hats: self.N_hats, + }) + } + + pub fn is_expensive(&self) -> bool { + false + } + + pub fn expects_messages(i: u16, n: u16) -> Round0Messages { + P2PMsgsStore::new(i, n) + } } pub struct Round2 { - pub ssid: SSID, - pub secrets: PreSigningSecrets, - pub omega_i: BigInt, - pub eks: HashMap, - pub gamma_i: BigInt, - pub Gamma_i: Point, - pub k_i: BigInt, - pub nu_i: BigInt, - pub rho_i: BigInt, - pub G_i: BigInt, - pub K_i: BigInt, - pub G: HashMap, - pub K: HashMap, - pub beta_i: HashMap, - pub beta_hat_i: HashMap, - pub r_i: HashMap, - pub r_hat_i: HashMap, - pub s_i: HashMap, - pub s_hat_i: HashMap, - pub D_j: HashMap, - pub D_hat_j: HashMap, - pub F_j: HashMap, - pub F_hat_j: HashMap, - pub S: HashMap, - pub T: HashMap, - pub N_hats: HashMap, + pub ssid: SSID, + pub secrets: PreSigningSecrets, + pub omega_i: BigInt, + pub eks: HashMap, + pub gamma_i: BigInt, + pub Gamma_i: Point, + pub k_i: BigInt, + pub nu_i: BigInt, + pub rho_i: BigInt, + pub G_i: BigInt, + pub K_i: BigInt, + pub G: HashMap, + pub K: HashMap, + pub beta_i: HashMap, + pub beta_hat_i: HashMap, + pub r_i: HashMap, + pub r_hat_i: HashMap, + pub s_i: HashMap, + pub s_hat_i: HashMap, + pub D_j: HashMap, + pub D_hat_j: HashMap, + pub F_j: HashMap, + pub F_hat_j: HashMap, + pub S: HashMap, + pub T: HashMap, + pub N_hats: HashMap, } impl Round2 { - pub fn proceed( - self, - input: P2PMsgs>, - mut output: O, - ) -> Result - where - O: Push>>>, - { - let mut D_i: HashMap = HashMap::new(); - let mut D_hat_i: HashMap = HashMap::new(); - let mut F_i: HashMap = HashMap::new(); - let mut F_hat_i: HashMap = HashMap::new(); - let mut Gammas: HashMap> = HashMap::new(); - - // Shift alpha_i_j to the interval {-N/2,...,N/2} (if necessary) as defined in Section 4.1 - // of the paper. - let shift_into_plus_minus_n_by_2_interval = |mut value: BigInt| -> BigInt { - if value > self.secrets.ek.n.div_floor(&BigInt::from(2)) { - value -= &self.secrets.ek.n; - } - value - }; - - for msg in input.into_vec() { - // j - let j = msg.i; - // Insert D_i_j - D_i.insert(j, msg.D_j_i); - // Insert D_hat_i_j - D_hat_i.insert(j, msg.D_hat_j_i); - // Insert F_i_j - F_i.insert(j, msg.F_j_i); - // Insert F_hat_i_j - F_hat_i.insert(j, msg.F_hat_j_i); - // Insert Gamma_j - Gammas.insert(j, msg.Gamma_i); - // Verify first aff-g - let psi_i_j = msg.psi_j_i; - let statement_psi_i_j = msg.statement_psi_j_i; - // Verify psi_i_j - if PaillierAffineOpWithGroupComInRangeProof::::verify( + pub fn proceed( + self, + input: P2PMsgs>, + mut output: O, + ) -> Result + where + O: Push>>>, + { + let mut D_i: HashMap = HashMap::new(); + let mut D_hat_i: HashMap = HashMap::new(); + let mut F_i: HashMap = HashMap::new(); + let mut F_hat_i: HashMap = HashMap::new(); + let mut Gammas: HashMap> = HashMap::new(); + + // Shift alpha_i_j to the interval {-N/2,...,N/2} (if necessary) as + // defined in Section 4.1 of the paper. + let shift_into_plus_minus_n_by_2_interval = + |mut value: BigInt| -> BigInt { + if value > self.secrets.ek.n.div_floor(&BigInt::from(2)) { + value -= &self.secrets.ek.n; + } + value + }; + + for msg in input.into_vec() { + // j + let j = msg.i; + // Insert D_i_j + D_i.insert(j, msg.D_j_i); + // Insert D_hat_i_j + D_hat_i.insert(j, msg.D_hat_j_i); + // Insert F_i_j + F_i.insert(j, msg.F_j_i); + // Insert F_hat_i_j + F_hat_i.insert(j, msg.F_hat_j_i); + // Insert Gamma_j + Gammas.insert(j, msg.Gamma_i); + // Verify first aff-g + let psi_i_j = msg.psi_j_i; + let statement_psi_i_j = msg.statement_psi_j_i; + // Verify psi_i_j + if PaillierAffineOpWithGroupComInRangeProof::::verify( &psi_i_j, &statement_psi_i_j, ) @@ -606,10 +696,10 @@ impl Round2 { })) } - // Verify psi_hat_i_j - let psi_hat_i_j = msg.psi_hat_j_i; - let statement_psi_hat_i_j = msg.statement_psi_hat_j_i; - if PaillierAffineOpWithGroupComInRangeProof::::verify( + // Verify psi_hat_i_j + let psi_hat_i_j = msg.psi_hat_j_i; + let statement_psi_hat_i_j = msg.statement_psi_hat_j_i; + if PaillierAffineOpWithGroupComInRangeProof::::verify( &psi_hat_i_j, &statement_psi_hat_i_j, ) @@ -626,10 +716,10 @@ impl Round2 { })) } - // Verify psi_prime_i_j - let psi_prime_i_j = msg.psi_prime_j_i; - let statement_psi_prime_i_j = msg.statement_psi_prime_j_i; - if KnowledgeOfExponentPaillierEncryptionProof::::verify( + // Verify psi_prime_i_j + let psi_prime_i_j = msg.psi_prime_j_i; + let statement_psi_prime_i_j = msg.statement_psi_prime_j_i; + if KnowledgeOfExponentPaillierEncryptionProof::::verify( &psi_prime_i_j, &statement_psi_prime_i_j, ) @@ -645,230 +735,263 @@ impl Round2 { data: bincode::serialize(&error_data).unwrap(), })) } - } - - // Gamma = Prod_j (Gamma_j) - let Gamma = Gammas.values().into_iter().fold(self.Gamma_i.clone(), |acc, x| acc + x); - - // Delta = Gamma^{k_i} - let Delta_i = Gamma.clone() * Scalar::from_bigint(&self.k_i); - - // {alpha, alpha_hat}_i will store mapping from j to {alpha, alpha_hat}_i_j - let mut alpha_i: HashMap = HashMap::new(); - let mut alpha_hat_i: HashMap = HashMap::new(); - for j in self.ssid.P.iter() { - if j != &self.ssid.X.i { - alpha_i.insert( - *j, - shift_into_plus_minus_n_by_2_interval( - Paillier::decrypt( - &self.secrets.dk, - RawCiphertext::from(D_i.get(j).unwrap_or(&BigInt::zero())), - ) - .into(), - ) - .mod_floor(&self.ssid.q), - ); - alpha_hat_i.insert( - *j, - shift_into_plus_minus_n_by_2_interval( - Paillier::decrypt( - &self.secrets.dk, - RawCiphertext::from(D_hat_i.get(j).unwrap_or(&BigInt::zero())), - ) - .into(), - ) - .mod_floor(&self.ssid.q), - ); - } - } - - // Sum alpha_i_j's - let sum_of_alphas = alpha_i.values().into_iter().fold(BigInt::zero(), |acc, x| acc.add(x)); - - // Sum alpha_hat_i_j's - let sum_of_alpha_hats = - alpha_hat_i.values().into_iter().fold(BigInt::zero(), |acc, x| acc.add(x)); - - // Sum beta_i_j's - let sum_of_betas = - self.beta_i.values().into_iter().fold(BigInt::zero(), |acc, x| acc.add(x)); - - // Sum beta_hat_i_j's - let sum_of_beta_hats = - self.beta_hat_i.values().into_iter().fold(BigInt::zero(), |acc, x| acc.add(x)); - - // delta_i = gamma_i * k_i + sum of alpha_i_j's + sum of beta_i_j's mod q - let delta_i = BigInt::mod_add( - &BigInt::mod_mul(&self.gamma_i, &self.k_i, &self.ssid.q), - &BigInt::mod_add(&sum_of_alphas, &sum_of_betas, &self.ssid.q), - &self.ssid.q, - ); - - // chi_i = x_i * k_i + sum of alpha_hat_i_j's + sum of beta_hat_i_j's - let chi_i = BigInt::mod_add( - // We use omega_i in place of x_i, see doc on omega_i definition (in Round 1) for - // explanation. - &BigInt::mod_mul(&self.omega_i, &self.k_i, &self.ssid.q), - &BigInt::mod_add(&sum_of_alpha_hats, &sum_of_beta_hats, &self.ssid.q), - &self.ssid.q, - ); - - for j in self.ssid.P.iter() { - if j != &self.ssid.X.i.clone() { - // Compute psi_prime_prime_j_i - let witness_psi_prime_prime_j_i = KnowledgeOfExponentPaillierEncryptionWitness::new( - self.k_i.clone(), - self.rho_i.clone(), - ); - - let statement_psi_prime_prime_j_i = - KnowledgeOfExponentPaillierEncryptionStatement { - N0: self.secrets.ek.n.clone(), - NN0: self.secrets.ek.nn.clone(), - C: self.K_i.clone(), - X: Delta_i.clone(), - // From the Delta_i = Gamma^{k_i} and Πlog∗ stating X = g^x, - // Since x = k_i and X = Delta_i, - // :- g = Gamma - // (see Figure 7, Round 3 and Figure 25 in paper). - g: Gamma.clone(), - s: self.S.get(j).unwrap_or(&BigInt::zero()).clone(), - t: self.T.get(j).unwrap_or(&BigInt::zero()).clone(), - N_hat: self.N_hats.get(j).unwrap_or(&BigInt::zero()).clone(), - phantom: PhantomData, - }; - let psi_prime_prime_j_i = - KnowledgeOfExponentPaillierEncryptionProof::::prove( - &witness_psi_prime_prime_j_i, - &statement_psi_prime_prime_j_i, - ); - - // Send Message - let body = PreSigningP2PMessage3 { - ssid: self.ssid.clone(), - i: self.ssid.X.i, - delta_i: delta_i.clone(), - Delta_i: Delta_i.clone(), - psi_prime_prime_j_i, - statement_psi_prime_prime_j_i, - }; - output.push(Msg { - sender: self.ssid.X.i, - receiver: Some(*j), - body: Box::new(body), - }); - } - } - Ok(Round3 { - ssid: self.ssid, - secrets: self.secrets, - eks: self.eks, - gamma_i: self.gamma_i, - Gamma_i: self.Gamma_i, - Gammas, - Gamma, - k_i: self.k_i, - nu_i: self.nu_i, - rho_i: self.rho_i, - G_i: self.G_i, - K_i: self.K_i, - G: self.G, - K: self.K, - beta_i: self.beta_i, - beta_hat_i: self.beta_hat_i, - r_i: self.r_i, - r_hat_i: self.r_hat_i, - s_i: self.s_i, - s_hat_i: self.s_hat_i, - delta_i, - chi_i, - Delta_i, - D_j: self.D_j, - D_hat_j: self.D_hat_j, - F_j: self.F_j, - F_hat_j: self.F_hat_j, - D_i, - D_hat_i, - F_i, - F_hat_i, - alpha_i, - alpha_hat_i, - S: self.S, - T: self.T, - N_hats: self.N_hats, - }) - } - - pub fn is_expensive(&self) -> bool { - false - } - pub fn expects_messages(i: u16, n: u16) -> Round1Messages { - P2PMsgsStore::new(i, n) - } + } + + // Gamma = Prod_j (Gamma_j) + let Gamma = Gammas + .values() + .into_iter() + .fold(self.Gamma_i.clone(), |acc, x| acc + x); + + // Delta = Gamma^{k_i} + let Delta_i = Gamma.clone() * Scalar::from_bigint(&self.k_i); + + // {alpha, alpha_hat}_i will store mapping from j to {alpha, + // alpha_hat}_i_j + let mut alpha_i: HashMap = HashMap::new(); + let mut alpha_hat_i: HashMap = HashMap::new(); + for j in self.ssid.P.iter() { + if j != &self.ssid.X.i { + alpha_i.insert( + *j, + shift_into_plus_minus_n_by_2_interval( + Paillier::decrypt( + &self.secrets.dk, + RawCiphertext::from( + D_i.get(j).unwrap_or(&BigInt::zero()), + ), + ) + .into(), + ) + .mod_floor(&self.ssid.q), + ); + alpha_hat_i.insert( + *j, + shift_into_plus_minus_n_by_2_interval( + Paillier::decrypt( + &self.secrets.dk, + RawCiphertext::from( + D_hat_i.get(j).unwrap_or(&BigInt::zero()), + ), + ) + .into(), + ) + .mod_floor(&self.ssid.q), + ); + } + } + + // Sum alpha_i_j's + let sum_of_alphas = alpha_i + .values() + .into_iter() + .fold(BigInt::zero(), |acc, x| acc.add(x)); + + // Sum alpha_hat_i_j's + let sum_of_alpha_hats = alpha_hat_i + .values() + .into_iter() + .fold(BigInt::zero(), |acc, x| acc.add(x)); + + // Sum beta_i_j's + let sum_of_betas = self + .beta_i + .values() + .into_iter() + .fold(BigInt::zero(), |acc, x| acc.add(x)); + + // Sum beta_hat_i_j's + let sum_of_beta_hats = self + .beta_hat_i + .values() + .into_iter() + .fold(BigInt::zero(), |acc, x| acc.add(x)); + + // delta_i = gamma_i * k_i + sum of alpha_i_j's + sum of beta_i_j's mod + // q + let delta_i = BigInt::mod_add( + &BigInt::mod_mul(&self.gamma_i, &self.k_i, &self.ssid.q), + &BigInt::mod_add(&sum_of_alphas, &sum_of_betas, &self.ssid.q), + &self.ssid.q, + ); + + // chi_i = x_i * k_i + sum of alpha_hat_i_j's + sum of beta_hat_i_j's + let chi_i = BigInt::mod_add( + // We use omega_i in place of x_i, see doc on omega_i definition + // (in Round 1) for explanation. + &BigInt::mod_mul(&self.omega_i, &self.k_i, &self.ssid.q), + &BigInt::mod_add( + &sum_of_alpha_hats, + &sum_of_beta_hats, + &self.ssid.q, + ), + &self.ssid.q, + ); + + for j in self.ssid.P.iter() { + if j != &self.ssid.X.i.clone() { + // Compute psi_prime_prime_j_i + let witness_psi_prime_prime_j_i = + KnowledgeOfExponentPaillierEncryptionWitness::new( + self.k_i.clone(), + self.rho_i.clone(), + ); + + let statement_psi_prime_prime_j_i = + KnowledgeOfExponentPaillierEncryptionStatement { + N0: self.secrets.ek.n.clone(), + NN0: self.secrets.ek.nn.clone(), + C: self.K_i.clone(), + X: Delta_i.clone(), + // From the Delta_i = Gamma^{k_i} and Πlog∗ stating X = + // g^x, Since x = k_i and X = + // Delta_i, :- g = Gamma + // (see Figure 7, Round 3 and Figure 25 in paper). + g: Gamma.clone(), + s: self.S.get(j).unwrap_or(&BigInt::zero()).clone(), + t: self.T.get(j).unwrap_or(&BigInt::zero()).clone(), + N_hat: self + .N_hats + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + phantom: PhantomData, + }; + let psi_prime_prime_j_i = + KnowledgeOfExponentPaillierEncryptionProof::< + Secp256k1, + Sha256, + >::prove( + &witness_psi_prime_prime_j_i, + &statement_psi_prime_prime_j_i, + ); + + // Send Message + let body = PreSigningP2PMessage3 { + ssid: self.ssid.clone(), + i: self.ssid.X.i, + delta_i: delta_i.clone(), + Delta_i: Delta_i.clone(), + psi_prime_prime_j_i, + statement_psi_prime_prime_j_i, + }; + output.push(Msg { + sender: self.ssid.X.i, + receiver: Some(*j), + body: Box::new(body), + }); + } + } + Ok(Round3 { + ssid: self.ssid, + secrets: self.secrets, + eks: self.eks, + gamma_i: self.gamma_i, + Gamma_i: self.Gamma_i, + Gammas, + Gamma, + k_i: self.k_i, + nu_i: self.nu_i, + rho_i: self.rho_i, + G_i: self.G_i, + K_i: self.K_i, + G: self.G, + K: self.K, + beta_i: self.beta_i, + beta_hat_i: self.beta_hat_i, + r_i: self.r_i, + r_hat_i: self.r_hat_i, + s_i: self.s_i, + s_hat_i: self.s_hat_i, + delta_i, + chi_i, + Delta_i, + D_j: self.D_j, + D_hat_j: self.D_hat_j, + F_j: self.F_j, + F_hat_j: self.F_hat_j, + D_i, + D_hat_i, + F_i, + F_hat_i, + alpha_i, + alpha_hat_i, + S: self.S, + T: self.T, + N_hats: self.N_hats, + }) + } + + pub fn is_expensive(&self) -> bool { + false + } + pub fn expects_messages(i: u16, n: u16) -> Round1Messages { + P2PMsgsStore::new(i, n) + } } pub struct Round3 { - pub ssid: SSID, - pub secrets: PreSigningSecrets, - pub eks: HashMap, - pub gamma_i: BigInt, - pub Gamma_i: Point, - pub Gammas: HashMap>, - pub Gamma: Point, - pub k_i: BigInt, - pub nu_i: BigInt, - pub rho_i: BigInt, - pub G_i: BigInt, - pub K_i: BigInt, - pub G: HashMap, - pub K: HashMap, - pub beta_i: HashMap, - pub beta_hat_i: HashMap, - pub r_i: HashMap, - pub r_hat_i: HashMap, - pub s_i: HashMap, - pub s_hat_i: HashMap, - pub delta_i: BigInt, - pub chi_i: BigInt, - pub Delta_i: Point, - pub D_j: HashMap, - pub D_hat_j: HashMap, - pub F_j: HashMap, - pub F_hat_j: HashMap, - pub D_i: HashMap, - pub D_hat_i: HashMap, - pub F_i: HashMap, - pub F_hat_i: HashMap, - pub alpha_i: HashMap, - pub alpha_hat_i: HashMap, - pub S: HashMap, - pub T: HashMap, - pub N_hats: HashMap, + pub ssid: SSID, + pub secrets: PreSigningSecrets, + pub eks: HashMap, + pub gamma_i: BigInt, + pub Gamma_i: Point, + pub Gammas: HashMap>, + pub Gamma: Point, + pub k_i: BigInt, + pub nu_i: BigInt, + pub rho_i: BigInt, + pub G_i: BigInt, + pub K_i: BigInt, + pub G: HashMap, + pub K: HashMap, + pub beta_i: HashMap, + pub beta_hat_i: HashMap, + pub r_i: HashMap, + pub r_hat_i: HashMap, + pub s_i: HashMap, + pub s_hat_i: HashMap, + pub delta_i: BigInt, + pub chi_i: BigInt, + pub Delta_i: Point, + pub D_j: HashMap, + pub D_hat_j: HashMap, + pub F_j: HashMap, + pub F_hat_j: HashMap, + pub D_i: HashMap, + pub D_hat_i: HashMap, + pub F_i: HashMap, + pub F_hat_i: HashMap, + pub alpha_i: HashMap, + pub alpha_hat_i: HashMap, + pub S: HashMap, + pub T: HashMap, + pub N_hats: HashMap, } impl Round3 { - pub fn proceed( - self, - input: P2PMsgs>, - mut output: O, - ) -> Result - where - O: Push>>>>, - { - // Mapping from j to delta_j - let mut deltas: HashMap = HashMap::new(); - // Mapping from j to Delta_j - let mut Deltas: HashMap> = HashMap::new(); - for msg in input.into_vec() { - // j - let j = msg.i; - // Verify psi_prime_prime_i_j - let psi_prime_prime_i_j = msg.psi_prime_prime_j_i; - - let statement_psi_prime_prime_i_j = msg.statement_psi_prime_prime_j_i; - - if KnowledgeOfExponentPaillierEncryptionProof::::verify( + pub fn proceed( + self, + input: P2PMsgs>, + mut output: O, + ) -> Result + where + O: Push>>>>, + { + // Mapping from j to delta_j + let mut deltas: HashMap = HashMap::new(); + // Mapping from j to Delta_j + let mut Deltas: HashMap> = HashMap::new(); + for msg in input.into_vec() { + // j + let j = msg.i; + // Verify psi_prime_prime_i_j + let psi_prime_prime_i_j = msg.psi_prime_prime_j_i; + + let statement_psi_prime_prime_i_j = + msg.statement_psi_prime_prime_j_i; + + if KnowledgeOfExponentPaillierEncryptionProof::::verify( &psi_prime_prime_i_j, &statement_psi_prime_prime_i_j, ) @@ -885,98 +1008,107 @@ impl Round3 { })) } - // Insert into deltas and Deltas - deltas.insert(j, msg.delta_i); - Deltas.insert(j, msg.Delta_i); - } - - // delta = sum of delta_j's - let delta = deltas - .values() - .into_iter() - .fold(self.delta_i.clone(), |acc, x| acc.add(x)) - .mod_floor(&self.ssid.q); - - // Compute product of Delta_j's - let product_of_Deltas = - Deltas.values().into_iter().fold(self.Delta_i.clone(), |acc, x| acc + x); - - if product_of_Deltas == - Point::::generator().as_point() * Scalar::from_bigint(&delta) - { - // R = Gamma^{delta^{-1}} - let R = self.Gamma.clone() * - Scalar::from_bigint(&BigInt::mod_inv(&delta, &self.ssid.q).unwrap()); - let presigning_output = PresigningOutput { - ssid: self.ssid.clone(), - R, - i: self.ssid.X.i, - k_i: self.k_i.clone(), - chi_i: self.chi_i.clone(), - }; - let transcript = PresigningTranscript { - ssid: self.ssid.clone(), - secrets: self.secrets, - eks: self.eks, - gamma_i: self.gamma_i, - Gamma_i: self.Gamma_i, - Gammas: self.Gammas, - Gamma: self.Gamma, - k_i: self.k_i, - nu_i: self.nu_i, - rho_i: self.rho_i, - G_i: self.G_i, - K_i: self.K_i, - G: self.G, - K: self.K, - beta_i: self.beta_i, - beta_hat_i: self.beta_hat_i, - r_i: self.r_i, - r_hat_i: self.r_hat_i, - s_i: self.s_i, - s_hat_i: self.s_hat_i, - delta_i: self.delta_i.clone(), - chi_i: self.chi_i.clone(), - Delta_i: self.Delta_i.clone(), - deltas, - Deltas, - delta, - D_j: self.D_j, - D_hat_j: self.D_hat_j, - F_j: self.F_j, - F_hat_j: self.F_hat_j, - D_i: self.D_i, - D_hat_i: self.D_hat_i, - F_i: self.F_i, - F_hat_i: self.F_hat_i, - alpha_i: self.alpha_i, - alpha_hat_i: self.alpha_hat_i, - S: self.S, - T: self.T, - N_hats: self.N_hats, - }; - - output.push(Msg { sender: self.ssid.X.i, receiver: None, body: Box::new(None) }); - - Ok(Round4 { - ssid: self.ssid, - output: Some(presigning_output), - transcript: Some(transcript), - }) - } else { - // (l,j) to proof for D_j_i - let mut proofs_D_j_i: HashMap< - (u16, u16), - PaillierAffineOpWithGroupComInRangeProof, - > = HashMap::new(); - - // (l,j) to statement for D_j_i - let mut statements_D_j_i: HashMap< - (u16, u16), - PaillierAffineOpWithGroupComInRangeStatement, - > = HashMap::new(); - - self.ssid.P.iter().zip(self.ssid.P.iter()).for_each(|(j, l)| { + // Insert into deltas and Deltas + deltas.insert(j, msg.delta_i); + Deltas.insert(j, msg.Delta_i); + } + + // delta = sum of delta_j's + let delta = deltas + .values() + .into_iter() + .fold(self.delta_i.clone(), |acc, x| acc.add(x)) + .mod_floor(&self.ssid.q); + + // Compute product of Delta_j's + let product_of_Deltas = Deltas + .values() + .into_iter() + .fold(self.Delta_i.clone(), |acc, x| acc + x); + + if product_of_Deltas == + Point::::generator().as_point() * + Scalar::from_bigint(&delta) + { + // R = Gamma^{delta^{-1}} + let R = self.Gamma.clone() * + Scalar::from_bigint( + &BigInt::mod_inv(&delta, &self.ssid.q).unwrap(), + ); + let presigning_output = PresigningOutput { + ssid: self.ssid.clone(), + R, + i: self.ssid.X.i, + k_i: self.k_i.clone(), + chi_i: self.chi_i.clone(), + }; + let transcript = PresigningTranscript { + ssid: self.ssid.clone(), + secrets: self.secrets, + eks: self.eks, + gamma_i: self.gamma_i, + Gamma_i: self.Gamma_i, + Gammas: self.Gammas, + Gamma: self.Gamma, + k_i: self.k_i, + nu_i: self.nu_i, + rho_i: self.rho_i, + G_i: self.G_i, + K_i: self.K_i, + G: self.G, + K: self.K, + beta_i: self.beta_i, + beta_hat_i: self.beta_hat_i, + r_i: self.r_i, + r_hat_i: self.r_hat_i, + s_i: self.s_i, + s_hat_i: self.s_hat_i, + delta_i: self.delta_i.clone(), + chi_i: self.chi_i.clone(), + Delta_i: self.Delta_i.clone(), + deltas, + Deltas, + delta, + D_j: self.D_j, + D_hat_j: self.D_hat_j, + F_j: self.F_j, + F_hat_j: self.F_hat_j, + D_i: self.D_i, + D_hat_i: self.D_hat_i, + F_i: self.F_i, + F_hat_i: self.F_hat_i, + alpha_i: self.alpha_i, + alpha_hat_i: self.alpha_hat_i, + S: self.S, + T: self.T, + N_hats: self.N_hats, + }; + + output.push(Msg { + sender: self.ssid.X.i, + receiver: None, + body: Box::new(None), + }); + + Ok(Round4 { + ssid: self.ssid, + output: Some(presigning_output), + transcript: Some(transcript), + }) + } else { + // (l,j) to proof for D_j_i + let mut proofs_D_j_i: HashMap< + (u16, u16), + PaillierAffineOpWithGroupComInRangeProof, + > = HashMap::new(); + + // (l,j) to statement for D_j_i + let mut statements_D_j_i: HashMap< + (u16, u16), + PaillierAffineOpWithGroupComInRangeStatement, + > = HashMap::new(); + + self.ssid.P.iter().zip(self.ssid.P.iter()).for_each(|(j, l)| { if *j != self.ssid.X.i && j != l { let D_j_i = self.D_j.get(&self.ssid.X.i.clone()).unwrap(); @@ -1015,211 +1147,254 @@ impl Round3 { } }); - // H_i proof - let H_i_randomness: BigInt = sample_relatively_prime_integer(&self.secrets.ek.n); - let H_i: BigInt = Paillier::encrypt_with_chosen_randomness( - &self.secrets.ek, - RawPlaintext::from(BigInt::mul(&self.k_i, &self.gamma_i)), - &Randomness::from(H_i_randomness.clone()), - ) - .into(); - - let witness_H_i = - PaillierMulWitness::new(self.k_i, self.nu_i.clone(), self.nu_i.mul(&self.gamma_i)); - let statement_H_i = PaillierMulStatement { - N: self.secrets.ek.n.clone(), - NN: self.secrets.ek.nn.clone(), - C: self.G_i, - Y: self.K_i, - X: H_i.clone(), - ek_prover: self.secrets.ek.clone(), - phantom: PhantomData, - }; - - let proof_H_i = - PaillierMulProof::::prove(&witness_H_i, &statement_H_i); - - // delta_i proofs - let s_j_i = BigInt::zero(); - let ciphertext_delta_i = H_i; - let delta_i_randomness = H_i_randomness.clone(); - self.ssid.P.iter().for_each(|j| { - if *j != self.ssid.X.i { - ciphertext_delta_i - .mul(self.D_i.get(j).unwrap_or(&BigInt::zero())) - .mul(self.F_j.get(&self.ssid.X.i).unwrap_or(&BigInt::zero())); - delta_i_randomness - .mul(&self.rho_i) - .mul(&s_j_i) - .mul(self.r_i.get(j).unwrap_or(&BigInt::zero())); - } - }); - - let witness_delta_i = PaillierDecryptionModQWitness::new( - Paillier::decrypt( - &self.secrets.dk, - RawCiphertext::from(ciphertext_delta_i.clone()), - ) - .into(), - H_i_randomness, - ); - - // l to statement - let mut statement_delta_i: HashMap< - u16, - PaillierDecryptionModQStatement, - > = HashMap::new(); - - // l to proof - let mut proof_delta_i: HashMap> = - HashMap::new(); - - self.ssid.P.iter().for_each(|l| { - if *l != self.ssid.X.i { - let statement_delta_l_i = PaillierDecryptionModQStatement { - S: self.S.get(l).unwrap_or(&BigInt::zero()).clone(), - T: self.T.get(l).unwrap_or(&BigInt::zero()).clone(), - N_hat: self.N_hats.get(l).unwrap_or(&BigInt::zero()).clone(), - N0: self.secrets.ek.n.clone(), - NN0: self.secrets.ek.nn.clone(), - C: ciphertext_delta_i.clone(), - x: self.delta_i.clone(), - ek_prover: self.secrets.ek.clone(), - phantom: PhantomData, - }; - - statement_delta_i.insert(*l, statement_delta_l_i.clone()); - - proof_delta_i.insert( - *l, - PaillierDecryptionModQProof::::prove( - &witness_delta_i, - &statement_delta_l_i, - ), - ); - } - }); - - let body = Some(IdentifiableAbortBroadcastMessage { - i: self.ssid.X.i, - statements_D_j_i, - proofs_D_j_i, - statement_H_i, - proof_H_i, - statement_delta_i, - proof_delta_i, - }); - - output.push(Msg { sender: self.ssid.X.i, receiver: None, body: Box::new(body) }); - Ok(Round4 { ssid: self.ssid, output: None, transcript: None }) - } - } - - pub fn is_expensive(&self) -> bool { - false - } - pub fn expects_messages(i: u16, n: u16) -> Round2Messages { - P2PMsgsStore::new(i, n) - } + // H_i proof + let H_i_randomness: BigInt = + sample_relatively_prime_integer(&self.secrets.ek.n); + let H_i: BigInt = Paillier::encrypt_with_chosen_randomness( + &self.secrets.ek, + RawPlaintext::from(BigInt::mul(&self.k_i, &self.gamma_i)), + &Randomness::from(H_i_randomness.clone()), + ) + .into(); + + let witness_H_i = PaillierMulWitness::new( + self.k_i, + self.nu_i.clone(), + self.nu_i.mul(&self.gamma_i), + ); + let statement_H_i = PaillierMulStatement { + N: self.secrets.ek.n.clone(), + NN: self.secrets.ek.nn.clone(), + C: self.G_i, + Y: self.K_i, + X: H_i.clone(), + ek_prover: self.secrets.ek.clone(), + phantom: PhantomData, + }; + + let proof_H_i = PaillierMulProof::::prove( + &witness_H_i, + &statement_H_i, + ); + + // delta_i proofs + let s_j_i = BigInt::zero(); + let ciphertext_delta_i = H_i; + let delta_i_randomness = H_i_randomness.clone(); + self.ssid.P.iter().for_each(|j| { + if *j != self.ssid.X.i { + ciphertext_delta_i + .mul(self.D_i.get(j).unwrap_or(&BigInt::zero())) + .mul( + self.F_j + .get(&self.ssid.X.i) + .unwrap_or(&BigInt::zero()), + ); + delta_i_randomness + .mul(&self.rho_i) + .mul(&s_j_i) + .mul(self.r_i.get(j).unwrap_or(&BigInt::zero())); + } + }); + + let witness_delta_i = PaillierDecryptionModQWitness::new( + Paillier::decrypt( + &self.secrets.dk, + RawCiphertext::from(ciphertext_delta_i.clone()), + ) + .into(), + H_i_randomness, + ); + + // l to statement + let mut statement_delta_i: HashMap< + u16, + PaillierDecryptionModQStatement, + > = HashMap::new(); + + // l to proof + let mut proof_delta_i: HashMap< + u16, + PaillierDecryptionModQProof, + > = HashMap::new(); + + self.ssid.P.iter().for_each(|l| { + if *l != self.ssid.X.i { + let statement_delta_l_i = PaillierDecryptionModQStatement { + S: self.S.get(l).unwrap_or(&BigInt::zero()).clone(), + T: self.T.get(l).unwrap_or(&BigInt::zero()).clone(), + N_hat: self + .N_hats + .get(l) + .unwrap_or(&BigInt::zero()) + .clone(), + N0: self.secrets.ek.n.clone(), + NN0: self.secrets.ek.nn.clone(), + C: ciphertext_delta_i.clone(), + x: self.delta_i.clone(), + ek_prover: self.secrets.ek.clone(), + phantom: PhantomData, + }; + + statement_delta_i.insert(*l, statement_delta_l_i.clone()); + + proof_delta_i.insert( + *l, + PaillierDecryptionModQProof::::prove( + &witness_delta_i, + &statement_delta_l_i, + ), + ); + } + }); + + let body = Some(IdentifiableAbortBroadcastMessage { + i: self.ssid.X.i, + statements_D_j_i, + proofs_D_j_i, + statement_H_i, + proof_H_i, + statement_delta_i, + proof_delta_i, + }); + + output.push(Msg { + sender: self.ssid.X.i, + receiver: None, + body: Box::new(body), + }); + Ok(Round4 { ssid: self.ssid, output: None, transcript: None }) + } + } + + pub fn is_expensive(&self) -> bool { + false + } + pub fn expects_messages(i: u16, n: u16) -> Round2Messages { + P2PMsgsStore::new(i, n) + } } pub struct Round4 { - ssid: SSID, - output: Option>, - transcript: Option>, + ssid: SSID, + output: Option>, + transcript: Option>, } impl Round4 { - pub fn proceed( - self, - input: BroadcastMsgs>>, - ) -> Result, PresigningTranscript)>> { - if self.output.is_some() { - Ok(Some((self.output.unwrap(), self.transcript.unwrap()))) - } else { - for msg in input.into_vec() { - let msg = msg.unwrap(); - // si stands for sender index - let si = msg.i; - let mut vec_D_si_j_proof_bad_actors: Vec = vec![]; - // Check D_i_j proofs - self.ssid.P.iter().for_each(|j| { - if *j != self.ssid.X.i { - let D_si_j_proof = msg.proofs_D_j_i.get(&(self.ssid.X.i, *j)).unwrap(); - - let statement_D_si_j = - msg.statements_D_j_i.get(&(self.ssid.X.i, *j)).unwrap(); - - if PaillierAffineOpWithGroupComInRangeProof::::verify( - D_si_j_proof, - statement_D_si_j, - ) - .is_err() - { - vec_D_si_j_proof_bad_actors.push(*j as usize); - } - } - }); - - if !vec_D_si_j_proof_bad_actors.is_empty() { - let error_data = ProofVerificationErrorData { - proof_symbol: "D_si_j".to_string(), - verifying_party: self.ssid.X.i, - }; - return Err(PresignError::ProofVerificationError(ErrorType { - error_type: "mul".to_string(), - bad_actors: vec_D_si_j_proof_bad_actors, - data: bincode::serialize(&error_data).unwrap(), - })) - } - // Check H_j proofs - let proof_H_si = msg.proof_H_i; - let statement_H_si = msg.statement_H_i; - - if PaillierMulProof::verify(&proof_H_si, &statement_H_si).is_err() { - let error_data = ProofVerificationErrorData { - proof_symbol: "H_si".to_string(), - verifying_party: self.ssid.X.i, - }; - return Err(PresignError::ProofVerificationError(ErrorType { - error_type: "mul".to_string(), - bad_actors: vec![si.into()], - data: bincode::serialize(&error_data).unwrap(), - })) - } - // Check delta_si_proof - let proof_delta_si = msg.proof_delta_i.get(&self.ssid.X.i).unwrap(); - let statement_delta_si = msg.statement_delta_i.get(&self.ssid.X.i).unwrap(); - - if PaillierDecryptionModQProof::verify(proof_delta_si, statement_delta_si).is_err() - { - let error_data = ProofVerificationErrorData { - proof_symbol: "delta_si".to_string(), - verifying_party: self.ssid.X.i, - }; - return Err(PresignError::ProofVerificationError(ErrorType { - error_type: "dec-q".to_string(), - bad_actors: vec![si.into()], - data: bincode::serialize(&error_data).unwrap(), - })) - } - } - Ok(None) - } - } - - pub fn is_expensive(&self) -> bool { - false - } - pub fn expects_messages(i: u16, n: u16) -> Round3Messages { - BroadcastMsgsStore::new(i, n) - } + pub fn proceed( + self, + input: BroadcastMsgs< + Option>, + >, + ) -> Result< + Option<(PresigningOutput, PresigningTranscript)>, + > { + if self.output.is_some() { + Ok(Some((self.output.unwrap(), self.transcript.unwrap()))) + } else { + for msg in input.into_vec() { + let msg = msg.unwrap(); + // si stands for sender index + let si = msg.i; + let mut vec_D_si_j_proof_bad_actors: Vec = vec![]; + // Check D_i_j proofs + self.ssid.P.iter().for_each(|j| { + if *j != self.ssid.X.i { + let D_si_j_proof = + msg.proofs_D_j_i.get(&(self.ssid.X.i, *j)).unwrap(); + + let statement_D_si_j = msg + .statements_D_j_i + .get(&(self.ssid.X.i, *j)) + .unwrap(); + + if PaillierAffineOpWithGroupComInRangeProof::< + Secp256k1, + Sha256, + >::verify( + D_si_j_proof, statement_D_si_j + ) + .is_err() + { + vec_D_si_j_proof_bad_actors.push(*j as usize); + } + } + }); + + if !vec_D_si_j_proof_bad_actors.is_empty() { + let error_data = ProofVerificationErrorData { + proof_symbol: "D_si_j".to_string(), + verifying_party: self.ssid.X.i, + }; + return Err(PresignError::ProofVerificationError( + ErrorType { + error_type: "mul".to_string(), + bad_actors: vec_D_si_j_proof_bad_actors, + data: bincode::serialize(&error_data).unwrap(), + }, + )) + } + // Check H_j proofs + let proof_H_si = msg.proof_H_i; + let statement_H_si = msg.statement_H_i; + + if PaillierMulProof::verify(&proof_H_si, &statement_H_si) + .is_err() + { + let error_data = ProofVerificationErrorData { + proof_symbol: "H_si".to_string(), + verifying_party: self.ssid.X.i, + }; + return Err(PresignError::ProofVerificationError( + ErrorType { + error_type: "mul".to_string(), + bad_actors: vec![si.into()], + data: bincode::serialize(&error_data).unwrap(), + }, + )) + } + // Check delta_si_proof + let proof_delta_si = + msg.proof_delta_i.get(&self.ssid.X.i).unwrap(); + let statement_delta_si = + msg.statement_delta_i.get(&self.ssid.X.i).unwrap(); + + if PaillierDecryptionModQProof::verify( + proof_delta_si, + statement_delta_si, + ) + .is_err() + { + let error_data = ProofVerificationErrorData { + proof_symbol: "delta_si".to_string(), + verifying_party: self.ssid.X.i, + }; + return Err(PresignError::ProofVerificationError( + ErrorType { + error_type: "dec-q".to_string(), + bad_actors: vec![si.into()], + data: bincode::serialize(&error_data).unwrap(), + }, + )) + } + } + Ok(None) + } + } + + pub fn is_expensive(&self) -> bool { + false + } + pub fn expects_messages(i: u16, n: u16) -> Round3Messages { + BroadcastMsgsStore::new(i, n) + } } type Result = std::result::Result; #[derive(Error, Debug, Clone)] pub enum PresignError { - #[error("Proof Verification Error")] - ProofVerificationError(ErrorType), + #[error("Proof Verification Error")] + ProofVerificationError(ErrorType), } diff --git a/src/presign/state_machine.rs b/src/presign/state_machine.rs index a652c2a7..f67e60e3 100644 --- a/src/presign/state_machine.rs +++ b/src/presign/state_machine.rs @@ -1,368 +1,454 @@ /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies. + Copyright 2022 by Webb Technologies. - This file is part of cggmp library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of cggmp library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - cggmp-threshold-ecdsa is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + cggmp-threshold-ecdsa is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ use super::{ - rounds::{Round0, Round1, Round2, Round3, Round4}, - IdentifiableAbortBroadcastMessage, PreSigningP2PMessage1, PreSigningP2PMessage2, - PreSigningP2PMessage3, PreSigningSecrets, PresigningOutput, PresigningTranscript, SSID, + rounds::{Round0, Round1, Round2, Round3, Round4}, + IdentifiableAbortBroadcastMessage, PreSigningP2PMessage1, + PreSigningP2PMessage2, PreSigningP2PMessage3, PreSigningSecrets, + PresigningOutput, PresigningTranscript, SSID, }; use curv::{elliptic::curves::Secp256k1, BigInt}; use private::InternalError; use round_based::{ - containers::{ - push::{Push, PushExt}, - BroadcastMsgs, MessageStore, P2PMsgs, Store, StoreErr, - }, - IsCritical, Msg, StateMachine, + containers::{ + push::{Push, PushExt}, + BroadcastMsgs, MessageStore, P2PMsgs, Store, StoreErr, + }, + IsCritical, Msg, StateMachine, }; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fmt, mem::replace, time::Duration}; use thiserror::Error; -// NOTE: This is a hack since in the 1st round we will need to broadcast and send P2P -// messages, but the round-based library doesn't support this. So we will use the -// `P2PMsgs` to send all the data we need peers to receive. -// FIXME: If we re-design `round-based-traits` to support sending 2 types of messages -// in the same round, we can remove this hack. +// NOTE: This is a hack since in the 1st round we will need to broadcast and +// send P2P messages, but the round-based library doesn't support this. So we +// will use the `P2PMsgs` to send all the data we need peers to receive. +// FIXME: If we re-design `round-based-traits` to support sending 2 types of +// messages in the same round, we can remove this hack. pub type Round0Messages = Store>>; pub type Round1Messages = Store>>; pub type Round2Messages = Store>>; pub type Round3Messages = - Store>>>; + Store>>>; pub struct PreSigning { - // Current round - round: R, + // Current round + round: R, - // Messages - round0_msgs: Option, - round1_msgs: Option, - round2_msgs: Option, - round3_msgs: Option, + // Messages + round0_msgs: Option, + round1_msgs: Option, + round2_msgs: Option, + round3_msgs: Option, - // Message queue - msgs_queue: Vec>, + // Message queue + msgs_queue: Vec>, - party_i: u16, + party_i: u16, - party_n: u16, + party_n: u16, } impl PreSigning { - pub fn new( - ssid: SSID, - secrets: PreSigningSecrets, - S: HashMap, - T: HashMap, - N_hats: HashMap, - l: usize, - ) -> Result { - let n = ssid.P.len(); - if n < 2 { - return Err(Error::TooFewParties) - } - - let i = ssid.X.i; - - let mut state = Self { - round: R::Round0(Box::new(Round0 { ssid, secrets, S, T, N_hats, l })), - - round0_msgs: Some(Round1::expects_messages(i, n as u16)), - round1_msgs: Some(Round2::expects_messages(i, n as u16)), - round2_msgs: Some(Round3::expects_messages(i, n as u16)), - round3_msgs: Some(Round4::expects_messages(i, n as u16)), - - msgs_queue: vec![], - party_i: i, - party_n: n as u16, - }; - - state.proceed_round(false)?; - Ok(state) - } - - fn gmap_queue<'a, T, F>(&'a mut self, mut f: F) -> impl Push> + 'a - where - F: FnMut(T) -> M + 'a, - { - (&mut self.msgs_queue).gmap(move |m: Msg| m.map_body(|m| ProtocolMessage(f(m)))) - } - - /// Proceeds round state if it received enough messages and if it's cheap to compute or - /// `may_block == true` - fn proceed_round(&mut self, may_block: bool) -> Result<()> { - let store1_wants_more = self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store3_wants_more = self.round2_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store4_wants_more = self.round3_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - - let next_state: R; - - let try_again: bool = match replace(&mut self.round, R::Gone) { - R::Round0(round) if !round.is_expensive() || may_block => { - next_state = round - .proceed(self.gmap_queue(M::Round1)) - .map(|msg| R::Round1(Box::new(msg))) - .map_err(|_e| Error::ProceedRound { msg_round: 0 })?; - true - }, - s @ R::Round0(_) => { - next_state = s; - false - }, - R::Round1(round) if !store1_wants_more && (!round.is_expensive() || may_block) => { - let store = self.round0_msgs.take().ok_or(InternalError::StoreGone)?; - let msgs = store.finish().map_err(InternalError::RetrieveRoundMessages)?; - next_state = round - .proceed(msgs, self.gmap_queue(M::Round2)) - .map(|msg| R::Round2(Box::new(msg))) - .map_err(|_e| Error::ProceedRound { msg_round: 1 })?; - true - }, - s @ R::Round1(_) => { - next_state = s; - false - }, - R::Round2(round) if !store2_wants_more && (!round.is_expensive() || may_block) => { - let store = self.round1_msgs.take().ok_or(InternalError::StoreGone)?; - let msgs = store.finish().map_err(InternalError::RetrieveRoundMessages)?; - next_state = round - .proceed(msgs, self.gmap_queue(M::Round3)) - .map(|msg| R::Round3(Box::new(msg))) - .map_err(|_e| Error::ProceedRound { msg_round: 2 })?; - true - }, - s @ R::Round2(_) => { - next_state = s; - false - }, - R::Round3(round) if !store3_wants_more && (!round.is_expensive() || may_block) => { - let store = self.round2_msgs.take().ok_or(InternalError::StoreGone)?; - let msgs = store.finish().map_err(InternalError::RetrieveRoundMessages)?; - next_state = round - .proceed(msgs, self.gmap_queue(M::Round4)) - .map(|msg| R::Round4(Box::new(msg))) - .map_err(|_e| Error::ProceedRound { msg_round: 3 })?; - true - }, - s @ R::Round3(_) => { - next_state = s; - false - }, - R::Round4(round) if !store4_wants_more && (!round.is_expensive() || may_block) => { - let store = self.round3_msgs.take().ok_or(InternalError::StoreGone)?; - let msgs = store.finish().map_err(InternalError::RetrieveRoundMessages)?; - next_state = round - .proceed(msgs) - .map(|msg| R::Final(Box::new(msg))) - .map_err(|_e| Error::ProceedRound { msg_round: 4 })?; - true - }, - s @ R::Round4(_) => { - next_state = s; - false - }, - s @ R::Final(_) | s @ R::Gone => { - next_state = s; - false - }, - }; - self.round = next_state; - if try_again { - self.proceed_round(may_block) - } else { - Ok(()) - } - } + pub fn new( + ssid: SSID, + secrets: PreSigningSecrets, + S: HashMap, + T: HashMap, + N_hats: HashMap, + l: usize, + ) -> Result { + let n = ssid.P.len(); + if n < 2 { + return Err(Error::TooFewParties) + } + + let i = ssid.X.i; + + let mut state = Self { + round: R::Round0(Box::new(Round0 { + ssid, + secrets, + S, + T, + N_hats, + l, + })), + + round0_msgs: Some(Round1::expects_messages(i, n as u16)), + round1_msgs: Some(Round2::expects_messages(i, n as u16)), + round2_msgs: Some(Round3::expects_messages(i, n as u16)), + round3_msgs: Some(Round4::expects_messages(i, n as u16)), + + msgs_queue: vec![], + party_i: i, + party_n: n as u16, + }; + + state.proceed_round(false)?; + Ok(state) + } + + fn gmap_queue<'a, T, F>(&'a mut self, mut f: F) -> impl Push> + 'a + where + F: FnMut(T) -> M + 'a, + { + (&mut self.msgs_queue) + .gmap(move |m: Msg| m.map_body(|m| ProtocolMessage(f(m)))) + } + + /// Proceeds round state if it received enough messages and if it's cheap to + /// compute or `may_block == true` + fn proceed_round(&mut self, may_block: bool) -> Result<()> { + let store1_wants_more = + self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = + self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store3_wants_more = + self.round2_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store4_wants_more = + self.round3_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + + let next_state: R; + + let try_again: bool = match replace(&mut self.round, R::Gone) { + R::Round0(round) if !round.is_expensive() || may_block => { + next_state = round + .proceed(self.gmap_queue(M::Round1)) + .map(|msg| R::Round1(Box::new(msg))) + .map_err(|_e| Error::ProceedRound { msg_round: 0 })?; + true + }, + s @ R::Round0(_) => { + next_state = s; + false + }, + R::Round1(round) + if !store1_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.round0_msgs.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs, self.gmap_queue(M::Round2)) + .map(|msg| R::Round2(Box::new(msg))) + .map_err(|_e| Error::ProceedRound { msg_round: 1 })?; + true + }, + s @ R::Round1(_) => { + next_state = s; + false + }, + R::Round2(round) + if !store2_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.round1_msgs.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs, self.gmap_queue(M::Round3)) + .map(|msg| R::Round3(Box::new(msg))) + .map_err(|_e| Error::ProceedRound { msg_round: 2 })?; + true + }, + s @ R::Round2(_) => { + next_state = s; + false + }, + R::Round3(round) + if !store3_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.round2_msgs.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs, self.gmap_queue(M::Round4)) + .map(|msg| R::Round4(Box::new(msg))) + .map_err(|_e| Error::ProceedRound { msg_round: 3 })?; + true + }, + s @ R::Round3(_) => { + next_state = s; + false + }, + R::Round4(round) + if !store4_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.round3_msgs.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs) + .map(|msg| R::Final(Box::new(msg))) + .map_err(|_e| Error::ProceedRound { msg_round: 4 })?; + true + }, + s @ R::Round4(_) => { + next_state = s; + false + }, + s @ R::Final(_) | s @ R::Gone => { + next_state = s; + false + }, + }; + self.round = next_state; + if try_again { + self.proceed_round(may_block) + } else { + Ok(()) + } + } } impl StateMachine for PreSigning { - type MessageBody = ProtocolMessage; - type Err = Error; - type Output = Option<(PresigningOutput, PresigningTranscript)>; - - fn handle_incoming(&mut self, msg: Msg) -> Result<()> { - let current_round = self.current_round(); - - match msg.body { - ProtocolMessage(M::Round1(m)) => { - let store = self - .round0_msgs - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { current_round, msg_round: 1 })?; - store - .push_msg(Msg { sender: msg.sender, receiver: msg.receiver, body: *m }) - .map_err(Error::HandleMessage)?; - self.proceed_round(false) - }, - ProtocolMessage(M::Round2(m)) => { - let store = self - .round1_msgs - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { current_round, msg_round: 2 })?; - store - .push_msg(Msg { sender: msg.sender, receiver: msg.receiver, body: *m }) - .map_err(Error::HandleMessage)?; - self.proceed_round(false) - }, - ProtocolMessage(M::Round3(m)) => { - let store = self - .round2_msgs - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { current_round, msg_round: 2 })?; - store - .push_msg(Msg { sender: msg.sender, receiver: msg.receiver, body: *m }) - .map_err(Error::HandleMessage)?; - self.proceed_round(false) - }, - ProtocolMessage(M::Round4(m)) => { - let store = self - .round3_msgs - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { current_round, msg_round: 2 })?; - store - .push_msg(Msg { sender: msg.sender, receiver: msg.receiver, body: *m }) - .map_err(Error::HandleMessage)?; - self.proceed_round(false) - }, - } - } - - fn message_queue(&mut self) -> &mut Vec> { - &mut self.msgs_queue - } - - fn wants_to_proceed(&self) -> bool { - let store1_wants_more = self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store3_wants_more = self.round2_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store4_wants_more = self.round3_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - - match &self.round { - R::Round0(_) => true, - R::Round1(_) => !store1_wants_more, - R::Round2(_) => !store2_wants_more, - R::Round3(_) => !store3_wants_more, - R::Round4(_) => !store4_wants_more, - R::Final(_) | R::Gone => false, - } - } - - fn proceed(&mut self) -> Result<()> { - self.proceed_round(true) - } - - fn round_timeout(&self) -> Option { - None - } - - fn round_timeout_reached(&mut self) -> Self::Err { - panic!("no timeout was set") - } - - fn is_finished(&self) -> bool { - matches!(self.round, R::Final(_)) - } - - fn pick_output(&mut self) -> Option> { - match self.round { - R::Final(_) => (), - R::Gone => return Some(Err(Error::DoublePickOutput)), - _ => return None, - } - - match replace(&mut self.round, R::Gone) { - R::Final(result) => Some(Ok(*result)), - _ => unreachable!("guaranteed by match expression above"), - } - } - - fn current_round(&self) -> u16 { - match &self.round { - R::Round0(_) => 0, - R::Round1(_) => 1, - R::Round2(_) => 2, - R::Round3(_) => 3, - R::Round4(_) => 4, - R::Final(_) | R::Gone => 5, - } - } - - fn total_rounds(&self) -> Option { - Some(2) - } - - fn party_ind(&self) -> u16 { - self.party_i - } - - fn parties(&self) -> u16 { - self.party_n - } + type MessageBody = ProtocolMessage; + type Err = Error; + type Output = + Option<(PresigningOutput, PresigningTranscript)>; + + fn handle_incoming(&mut self, msg: Msg) -> Result<()> { + let current_round = self.current_round(); + + match msg.body { + ProtocolMessage(M::Round1(m)) => { + let store = self.round0_msgs.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 1, + }, + )?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: *m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + }, + ProtocolMessage(M::Round2(m)) => { + let store = self.round1_msgs.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 2, + }, + )?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: *m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + }, + ProtocolMessage(M::Round3(m)) => { + let store = self.round2_msgs.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 2, + }, + )?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: *m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + }, + ProtocolMessage(M::Round4(m)) => { + let store = self.round3_msgs.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 2, + }, + )?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: *m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + }, + } + } + + fn message_queue(&mut self) -> &mut Vec> { + &mut self.msgs_queue + } + + fn wants_to_proceed(&self) -> bool { + let store1_wants_more = + self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = + self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store3_wants_more = + self.round2_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store4_wants_more = + self.round3_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + + match &self.round { + R::Round0(_) => true, + R::Round1(_) => !store1_wants_more, + R::Round2(_) => !store2_wants_more, + R::Round3(_) => !store3_wants_more, + R::Round4(_) => !store4_wants_more, + R::Final(_) | R::Gone => false, + } + } + + fn proceed(&mut self) -> Result<()> { + self.proceed_round(true) + } + + fn round_timeout(&self) -> Option { + None + } + + fn round_timeout_reached(&mut self) -> Self::Err { + panic!("no timeout was set") + } + + fn is_finished(&self) -> bool { + matches!(self.round, R::Final(_)) + } + + fn pick_output(&mut self) -> Option> { + match self.round { + R::Final(_) => (), + R::Gone => return Some(Err(Error::DoublePickOutput)), + _ => return None, + } + + match replace(&mut self.round, R::Gone) { + R::Final(result) => Some(Ok(*result)), + _ => unreachable!("guaranteed by match expression above"), + } + } + + fn current_round(&self) -> u16 { + match &self.round { + R::Round0(_) => 0, + R::Round1(_) => 1, + R::Round2(_) => 2, + R::Round3(_) => 3, + R::Round4(_) => 4, + R::Final(_) | R::Gone => 5, + } + } + + fn total_rounds(&self) -> Option { + Some(2) + } + + fn party_ind(&self) -> u16 { + self.party_i + } + + fn parties(&self) -> u16 { + self.party_n + } } impl crate::traits::RoundBlame for PreSigning { - fn round_blame(&self) -> (u16, Vec) { - let store1_blame = self.round0_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store2_blame = self.round1_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store3_blame = self.round2_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store4_blame = self.round3_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - - let default = (0, vec![]); - match &self.round { - R::Round0(_) => default, - R::Round1(_) => store1_blame, - R::Round2(_) => store2_blame, - R::Round3(_) => store3_blame, - R::Round4(_) => store4_blame, - R::Final(_) | R::Gone => default, - } - } + fn round_blame(&self) -> (u16, Vec) { + let store1_blame = + self.round0_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store2_blame = + self.round1_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store3_blame = + self.round2_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store4_blame = + self.round3_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); + + let default = (0, vec![]); + match &self.round { + R::Round0(_) => default, + R::Round1(_) => store1_blame, + R::Round2(_) => store2_blame, + R::Round3(_) => store3_blame, + R::Round4(_) => store4_blame, + R::Final(_) | R::Gone => default, + } + } } impl fmt::Debug for PreSigning { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let current_round = match &self.round { - R::Round0(_) => "0", - R::Round1(_) => "1", - R::Round2(_) => "2", - R::Round3(_) => "3", - R::Round4(_) => "4", - R::Final(_) => "[Final]", - R::Gone => "[Gone]", - }; - let round0_msgs = match self.round0_msgs.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), - None => "[None]".into(), - }; - let round1_msgs = match self.round1_msgs.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), - None => "[None]".into(), - }; - let _round2_msgs = match self.round2_msgs.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), - None => "[None]".into(), - }; - let _round3_msgs = match self.round3_msgs.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), - None => "[None]".into(), - }; - write!( + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let current_round = match &self.round { + R::Round0(_) => "0", + R::Round1(_) => "1", + R::Round2(_) => "2", + R::Round3(_) => "3", + R::Round4(_) => "4", + R::Final(_) => "[Final]", + R::Gone => "[Gone]", + }; + let round0_msgs = match self.round0_msgs.as_ref() { + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), + None => "[None]".into(), + }; + let round1_msgs = match self.round1_msgs.as_ref() { + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), + None => "[None]".into(), + }; + let _round2_msgs = match self.round2_msgs.as_ref() { + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), + None => "[None]".into(), + }; + let _round3_msgs = match self.round3_msgs.as_ref() { + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), + None => "[None]".into(), + }; + write!( f, "{{Key refresh at round={} round0_msgs={} round1_msgs={} queue=[len={}]}}", current_round, @@ -370,34 +456,42 @@ impl fmt::Debug for PreSigning { round1_msgs, self.msgs_queue.len() ) - } + } } // Rounds enum R { - Round0(Box), - Round1(Box), - Round2(Box), - Round3(Box), - Round4(Box), - Final(Box, PresigningTranscript)>>), - Gone, + Round0(Box), + Round1(Box), + Round2(Box), + Round3(Box), + Round4(Box), + Final( + Box< + Option<( + PresigningOutput, + PresigningTranscript, + )>, + >, + ), + Gone, } // Messages /// Protocol message which parties send on wire /// -/// Hides actual messages structure so it could be changed without breaking semver policy. +/// Hides actual messages structure so it could be changed without breaking +/// semver policy. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProtocolMessage(M); #[derive(Debug, Clone, Serialize, Deserialize)] enum M { - Round1(Box>), - Round2(Box>), - Round3(Box>), - Round4(Box>>), + Round1(Box>), + Round2(Box>), + Round3(Box>), + Round4(Box>>), } // Error @@ -408,65 +502,67 @@ type Result = std::result::Result; #[derive(Debug, Error)] #[non_exhaustive] pub enum Error { - /// Round proceeding resulted in error - #[error("proceed round: {msg_round}")] - ProceedRound { msg_round: usize }, - - /// Too few parties (`n < 2`) - #[error("at least 2 parties are required for keygen")] - TooFewParties, - /// Threshold value `t` is not in range `[1; n-1]` - #[error("threshold is not in range [1; n-1]")] - InvalidThreshold, - /// Party index `i` is not in range `[1; n]` - #[error("party index is not in range [1; n]")] - InvalidPartyIndex, - - /// Received message didn't pass pre-validation - #[error("received message didn't pass pre-validation: {0}")] - HandleMessage(#[source] StoreErr), - /// Received message which we didn't expect to receive now (e.g. message from previous round) - #[error( + /// Round proceeding resulted in error + #[error("proceed round: {msg_round}")] + ProceedRound { msg_round: usize }, + + /// Too few parties (`n < 2`) + #[error("at least 2 parties are required for keygen")] + TooFewParties, + /// Threshold value `t` is not in range `[1; n-1]` + #[error("threshold is not in range [1; n-1]")] + InvalidThreshold, + /// Party index `i` is not in range `[1; n]` + #[error("party index is not in range [1; n]")] + InvalidPartyIndex, + + /// Received message didn't pass pre-validation + #[error("received message didn't pass pre-validation: {0}")] + HandleMessage(#[source] StoreErr), + /// Received message which we didn't expect to receive now (e.g. message + /// from previous round) + #[error( "didn't expect to receive message from round {msg_round} (being at round {current_round})" )] - ReceivedOutOfOrderMessage { current_round: u16, msg_round: u16 }, - /// [Keygen::pick_output] called twice - #[error("pick_output called twice")] - DoublePickOutput, - - /// Some internal assertions were failed, which is a bug - #[doc(hidden)] - #[error("internal error: {0:?}")] - InternalError(InternalError), + ReceivedOutOfOrderMessage { current_round: u16, msg_round: u16 }, + /// [Keygen::pick_output] called twice + #[error("pick_output called twice")] + DoublePickOutput, + + /// Some internal assertions were failed, which is a bug + #[doc(hidden)] + #[error("internal error: {0:?}")] + InternalError(InternalError), } impl IsCritical for Error { - fn is_critical(&self) -> bool { - true - } + fn is_critical(&self) -> bool { + true + } } impl From for Error { - fn from(err: InternalError) -> Self { - Self::InternalError(err) - } + fn from(err: InternalError) -> Self { + Self::InternalError(err) + } } mod private { - #[derive(Debug)] - #[non_exhaustive] - pub enum InternalError { - /// [Messages store](super::MessageStore) reported that it received all messages it wanted - /// to receive, but refused to return message container - RetrieveRoundMessages(super::StoreErr), - #[doc(hidden)] - StoreGone, - } + #[derive(Debug)] + #[non_exhaustive] + pub enum InternalError { + /// [Messages store](super::MessageStore) reported that it received all + /// messages it wanted to receive, but refused to return + /// message container + RetrieveRoundMessages(super::StoreErr), + #[doc(hidden)] + StoreGone, + } } #[cfg(test)] pub mod test { - use super::*; + use super::*; use crate::utilities::sha2::Sha256; use curv::{ arithmetic::{ @@ -483,200 +579,231 @@ pub mod test { use round_based::dev::Simulation; use std::ops::Deref; - fn simulate_keygen(t: u16, n: u16) -> Vec> { - let mut simulation = Simulation::new(); - - for i in 1..=n { - simulation.add_party(Keygen::new(i, t, n).unwrap()); - } - - simulation.run().unwrap() - } - - fn simulate_presign( - inputs: Vec<( - SSID, - PreSigningSecrets, - HashMap, // S - HashMap, // T - HashMap, // N_hats - )>, - l: usize, // pre-signing index. - ) -> Vec, PresigningTranscript)>> { - let mut simulation = Simulation::new(); - - for (ssid, secrets, S, T, N_hats) in inputs { - simulation.add_party(PreSigning::new(ssid, secrets, S, T, N_hats, l).unwrap()); - } - - simulation.run().unwrap() - } - - pub fn extract_secret_key(local_keys: &[LocalKey]) -> Scalar { - let secret_shares: Vec> = - local_keys.iter().map(|key| key.keys_linear.x_i.clone()).collect(); - local_keys[0].vss_scheme.reconstruct( - &(0..local_keys.len() as u16).collect::>(), - &secret_shares.clone(), - ) - } - - pub fn extract_k( - presign_outputs: &[Option<( - PresigningOutput, - PresigningTranscript, - )>], - ) -> Scalar { - let q = Scalar::::group_order(); - Scalar::::from_bigint( - &presign_outputs - .iter() - .filter_map(|it| it.as_ref().map(|(output, _)| output.k_i.clone())) - .fold(BigInt::from(0), |acc, x| BigInt::mod_add(&acc, &x, q)), - ) - } - - // t = threshold, n = total number of parties, p = number of participants. - // NOTE: Quorum size = t + 1. - pub fn generate_parties_and_simulate_presign( - t: u16, - n: u16, - p: u16, - ) -> ( - Vec>, - Vec>, - Vec, PresigningTranscript)>>, - ) { - // Runs keygen simulation for test parameters. - let keys = simulate_keygen(t, n); - assert_eq!(keys.len(), n as usize); - - // Extracts and verifies the shared secret key. - let sec_key = extract_secret_key(&keys); - let pub_key = keys[0].public_key(); - assert_eq!(Point::::generator() * &sec_key, pub_key); - - // Verifies that transforming of x_i, which is a (t,n) share of x, into a (t,t+1) share - // omega_i using an appropriate lagrangian coefficient lambda_{i,S} as defined by GG18 and - // GG20 works. - // Ref: https://eprint.iacr.org/2021/060.pdf (Section 1.2.8) - // Ref: https://eprint.iacr.org/2019/114.pdf (Section 4.2) - // Ref: https://eprint.iacr.org/2020/540.pdf (Section 3.2) - let secret_shares: Vec> = - keys.iter().map(|key| key.keys_linear.x_i.clone()).collect(); - let omega_shares: Vec> = keys[0..p as usize] - .iter() - .enumerate() - .map(|(idx, key)| { - let x_i = secret_shares[idx].clone(); - let lambda_i_s = VerifiableSS::::map_share_to_new_params( - &key.vss_scheme.parameters, - key.i - 1, - &(0..p).collect::>(), - ); - lambda_i_s * x_i - }) - .collect(); - let omega_sec_key = omega_shares.iter().fold(Scalar::::zero(), |acc, x| acc + x); - assert_eq!(omega_sec_key, sec_key); - - // Generates auxiliary "ring" Pedersen parameters for all participants. - let mut aux_ring_pedersen_n_hat_values = HashMap::with_capacity(keys.len()); - let mut aux_ring_pedersen_s_values = HashMap::with_capacity(keys.len()); - let mut aux_ring_pedersen_t_values = HashMap::with_capacity(keys.len()); - for idx in 1..=p { - let (ring_pedersen_params, _) = RingPedersenStatement::::generate(); - aux_ring_pedersen_n_hat_values.insert(idx, ring_pedersen_params.N); - aux_ring_pedersen_s_values.insert(idx, ring_pedersen_params.S); - aux_ring_pedersen_t_values.insert(idx, ring_pedersen_params.T); - } - - // Creates pre-signing inputs and auxiliary parameters for ZK proofs. - let generator = Point::::generator().to_point(); - let group_order = Scalar::::group_order(); - let party_indices: Vec = (1..=p).collect(); - let inputs: Vec<( - SSID, - PreSigningSecrets, - HashMap, // S - HashMap, // T - HashMap, // N_hats - )> = keys[0..p as usize] - .iter() - .map(|key| { - // Creates SSID and pre-signing secrets. - // We already have Paillier keys from GG20 keygen or FS-DKR so we just reuse them. - let paillier_ek = key.paillier_key_vec[key.i as usize - 1].clone(); - let paillier_dk = key.paillier_dk.clone(); - // Composes SSID. - // See Figure 6, Round 1. - // Ref: . - let phi = (&paillier_dk.p - BigInt::one()) * (&paillier_dk.q - BigInt::one()); - let r = BigInt::sample_below(&paillier_ek.n); - let lambda = BigInt::sample_below(&phi); - let t = BigInt::mod_pow(&r, &BigInt::from(2), &paillier_ek.n); - let s = BigInt::mod_pow(&t, &lambda, &paillier_ek.n); - let ssid = SSID { - g: generator.clone(), - q: group_order.clone(), - P: party_indices.clone(), - rid: BigInt::strict_sample(256).to_bytes().try_into().unwrap(), - X: key.clone(), - Y: None, // Y is not needed for 4-round signing. - N: paillier_ek.n.clone(), - S: s, - T: t, - }; - // Composes pre-signing secrets. - let pre_sign_secrets = PreSigningSecrets { - x_i: BigInt::from_bytes(key.keys_linear.x_i.to_bytes().deref()), - y_i: None, // Y is not needed for 4-round signing. - ek: paillier_ek, - dk: paillier_dk, - }; - - ( - ssid, - pre_sign_secrets, - aux_ring_pedersen_s_values.clone(), - aux_ring_pedersen_t_values.clone(), - aux_ring_pedersen_n_hat_values.clone(), - ) - }) - .collect(); - let ssids = inputs.iter().map(|(ssid, ..)| ssid.clone()).collect(); - - // Runs pre-signing simulation for test parameters and verifies the outputs. - let outputs = simulate_presign(inputs, 1); - // Verifies that r, the x projection of R = g^k-1 is computed correctly. - let q = Scalar::::group_order(); - let r_dist = outputs[0].as_ref().unwrap().0.R.x_coord().unwrap(); - let k = extract_k(&outputs); - let r_direct = (Point::::generator() * k.invert().unwrap()).x_coord().unwrap(); - assert_eq!(r_dist, r_direct); - // Verifies that chi_i are additive shares of kx. - let k_x = &k * &sec_key; - let chi_i_sum = Scalar::::from_bigint( - &outputs - .iter() - .filter_map(|it| it.as_ref().map(|(output, _)| output.chi_i.clone())) - .fold(BigInt::from(0), |acc, x| BigInt::mod_add(&acc, &x, q)), - ); - assert_eq!(k_x, chi_i_sum); - - // Returns generated local keys, SSIDs and pre-signing outputs. - (keys, ssids, outputs) - } - - // All parties (2/2 pre-signing). - #[test] - fn presign_all_parties_works() { - generate_parties_and_simulate_presign(1, 2, 2); - } - - // Threshold pre-signing (subset of parties) - (3/4 pre-signing). - #[test] - fn presign_threshold_works() { - generate_parties_and_simulate_presign(2, 4, 3); - } + fn simulate_keygen(t: u16, n: u16) -> Vec> { + let mut simulation = Simulation::new(); + + for i in 1..=n { + simulation.add_party(Keygen::new(i, t, n).unwrap()); + } + + simulation.run().unwrap() + } + + fn simulate_presign( + inputs: Vec<( + SSID, + PreSigningSecrets, + HashMap, // S + HashMap, // T + HashMap, // N_hats + )>, + l: usize, // pre-signing index. + ) -> Vec< + Option<(PresigningOutput, PresigningTranscript)>, + > { + let mut simulation = Simulation::new(); + + for (ssid, secrets, S, T, N_hats) in inputs { + simulation.add_party( + PreSigning::new(ssid, secrets, S, T, N_hats, l).unwrap(), + ); + } + + simulation.run().unwrap() + } + + pub fn extract_secret_key( + local_keys: &[LocalKey], + ) -> Scalar { + let secret_shares: Vec> = + local_keys.iter().map(|key| key.keys_linear.x_i.clone()).collect(); + local_keys[0].vss_scheme.reconstruct( + &(0..local_keys.len() as u16).collect::>(), + &secret_shares.clone(), + ) + } + + pub fn extract_k( + presign_outputs: &[Option<( + PresigningOutput, + PresigningTranscript, + )>], + ) -> Scalar { + let q = Scalar::::group_order(); + Scalar::::from_bigint( + &presign_outputs + .iter() + .filter_map(|it| { + it.as_ref().map(|(output, _)| output.k_i.clone()) + }) + .fold(BigInt::from(0), |acc, x| BigInt::mod_add(&acc, &x, q)), + ) + } + + // t = threshold, n = total number of parties, p = number of participants. + // NOTE: Quorum size = t + 1. + pub fn generate_parties_and_simulate_presign( + t: u16, + n: u16, + p: u16, + ) -> ( + Vec>, + Vec>, + Vec< + Option<( + PresigningOutput, + PresigningTranscript, + )>, + >, + ) { + // Runs keygen simulation for test parameters. + let keys = simulate_keygen(t, n); + assert_eq!(keys.len(), n as usize); + + // Extracts and verifies the shared secret key. + let sec_key = extract_secret_key(&keys); + let pub_key = keys[0].public_key(); + assert_eq!(Point::::generator() * &sec_key, pub_key); + + // Verifies that transforming of x_i, which is a (t,n) share of x, into + // a (t,t+1) share omega_i using an appropriate lagrangian + // coefficient lambda_{i,S} as defined by GG18 and GG20 works. + // Ref: https://eprint.iacr.org/2021/060.pdf (Section 1.2.8) + // Ref: https://eprint.iacr.org/2019/114.pdf (Section 4.2) + // Ref: https://eprint.iacr.org/2020/540.pdf (Section 3.2) + let secret_shares: Vec> = + keys.iter().map(|key| key.keys_linear.x_i.clone()).collect(); + let omega_shares: Vec> = keys[0..p as usize] + .iter() + .enumerate() + .map(|(idx, key)| { + let x_i = secret_shares[idx].clone(); + let lambda_i_s = + VerifiableSS::::map_share_to_new_params( + &key.vss_scheme.parameters, + key.i - 1, + &(0..p).collect::>(), + ); + lambda_i_s * x_i + }) + .collect(); + let omega_sec_key = omega_shares + .iter() + .fold(Scalar::::zero(), |acc, x| acc + x); + assert_eq!(omega_sec_key, sec_key); + + // Generates auxiliary "ring" Pedersen parameters for all participants. + let mut aux_ring_pedersen_n_hat_values = + HashMap::with_capacity(keys.len()); + let mut aux_ring_pedersen_s_values = HashMap::with_capacity(keys.len()); + let mut aux_ring_pedersen_t_values = HashMap::with_capacity(keys.len()); + for idx in 1..=p { + let (ring_pedersen_params, _) = + RingPedersenStatement::::generate(); + aux_ring_pedersen_n_hat_values.insert(idx, ring_pedersen_params.N); + aux_ring_pedersen_s_values.insert(idx, ring_pedersen_params.S); + aux_ring_pedersen_t_values.insert(idx, ring_pedersen_params.T); + } + + // Creates pre-signing inputs and auxiliary parameters for ZK proofs. + let generator = Point::::generator().to_point(); + let group_order = Scalar::::group_order(); + let party_indices: Vec = (1..=p).collect(); + let inputs: Vec<( + SSID, + PreSigningSecrets, + HashMap, // S + HashMap, // T + HashMap, // N_hats + )> = keys[0..p as usize] + .iter() + .map(|key| { + // Creates SSID and pre-signing secrets. + // We already have Paillier keys from GG20 keygen or FS-DKR so + // we just reuse them. + let paillier_ek = + key.paillier_key_vec[key.i as usize - 1].clone(); + let paillier_dk = key.paillier_dk.clone(); + // Composes SSID. + // See Figure 6, Round 1. + // Ref: . + let phi = (&paillier_dk.p - BigInt::one()) * + (&paillier_dk.q - BigInt::one()); + let r = BigInt::sample_below(&paillier_ek.n); + let lambda = BigInt::sample_below(&phi); + let t = BigInt::mod_pow(&r, &BigInt::from(2), &paillier_ek.n); + let s = BigInt::mod_pow(&t, &lambda, &paillier_ek.n); + let ssid = SSID { + g: generator.clone(), + q: group_order.clone(), + P: party_indices.clone(), + rid: BigInt::strict_sample(256) + .to_bytes() + .try_into() + .unwrap(), + X: key.clone(), + Y: None, // Y is not needed for 4-round signing. + N: paillier_ek.n.clone(), + S: s, + T: t, + }; + // Composes pre-signing secrets. + let pre_sign_secrets = PreSigningSecrets { + x_i: BigInt::from_bytes( + key.keys_linear.x_i.to_bytes().deref(), + ), + y_i: None, // Y is not needed for 4-round signing. + ek: paillier_ek, + dk: paillier_dk, + }; + + ( + ssid, + pre_sign_secrets, + aux_ring_pedersen_s_values.clone(), + aux_ring_pedersen_t_values.clone(), + aux_ring_pedersen_n_hat_values.clone(), + ) + }) + .collect(); + let ssids = inputs.iter().map(|(ssid, ..)| ssid.clone()).collect(); + + // Runs pre-signing simulation for test parameters and verifies the + // outputs. + let outputs = simulate_presign(inputs, 1); + // Verifies that r, the x projection of R = g^k-1 is computed correctly. + let q = Scalar::::group_order(); + let r_dist = outputs[0].as_ref().unwrap().0.R.x_coord().unwrap(); + let k = extract_k(&outputs); + let r_direct = (Point::::generator() * k.invert().unwrap()) + .x_coord() + .unwrap(); + assert_eq!(r_dist, r_direct); + // Verifies that chi_i are additive shares of kx. + let k_x = &k * &sec_key; + let chi_i_sum = Scalar::::from_bigint( + &outputs + .iter() + .filter_map(|it| { + it.as_ref().map(|(output, _)| output.chi_i.clone()) + }) + .fold(BigInt::from(0), |acc, x| BigInt::mod_add(&acc, &x, q)), + ); + assert_eq!(k_x, chi_i_sum); + + // Returns generated local keys, SSIDs and pre-signing outputs. + (keys, ssids, outputs) + } + + // All parties (2/2 pre-signing). + #[test] + fn presign_all_parties_works() { + generate_parties_and_simulate_presign(1, 2, 2); + } + + // Threshold pre-signing (subset of parties) - (3/4 pre-signing). + #[test] + fn presign_threshold_works() { + generate_parties_and_simulate_presign(2, 4, 3); + } } diff --git a/src/refresh/rounds.rs b/src/refresh/rounds.rs index 150f0897..faf52288 100644 --- a/src/refresh/rounds.rs +++ b/src/refresh/rounds.rs @@ -3,235 +3,285 @@ use std::collections::HashMap; use curv::elliptic::curves::Secp256k1; use fs_dkr::{add_party_message::*, error::*, refresh_message::*}; use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::{ - party_i::Keys, state_machine::keygen::*, + party_i::Keys, state_machine::keygen::*, }; use paillier::DecryptionKey; use round_based::{ - containers::{push::Push, BroadcastMsgs, BroadcastMsgsStore}, - Msg, + containers::{push::Push, BroadcastMsgs, BroadcastMsgsStore}, + Msg, }; use crate::utilities::sha2::Sha256; pub enum PartyType { - Existing(Box>), - New(Box<(JoinMessage, Keys, u16)>), + Existing(Box>), + New( + Box<( + JoinMessage, + Keys, + u16, + )>, + ), } use super::state_machine::{Round0Messages, Round1Messages}; pub struct Round0 { - pub local_key_option: Option>, - pub new_party_index_option: Option, - pub old_to_new_map: HashMap, - pub new_t: u16, - pub new_n: u16, - pub current_t: u16, + pub local_key_option: Option>, + pub new_party_index_option: Option, + pub old_to_new_map: HashMap, + pub new_t: u16, + pub new_n: u16, + pub current_t: u16, } impl Round0 { - pub fn proceed(self, mut output: O) -> Result - where - O: Push>>>, - { - match self.local_key_option { - Some(local_key) => { - output.push(Msg { sender: local_key.i, receiver: None, body: None }); - match self.new_party_index_option { - None => Ok(Round1 { - party_type: PartyType::Existing(Box::new(local_key)), - old_to_new_map: self.old_to_new_map, - new_t: self.new_t, - new_n: self.new_n, - current_t: self.current_t, - }), - _ => Err(FsDkrError::NewPartyUnassignedIndexError), - } - }, - None => { - let (mut join_message, paillier_keys) = JoinMessage::distribute(); - match self.new_party_index_option { - Some(new_party_index) => { - join_message.set_party_index(new_party_index); - output.push(Msg { - sender: join_message.clone().get_party_index()?, - receiver: None, - body: Some(join_message.clone()), - }); - Ok(Round1 { - party_type: PartyType::New(Box::new(( - join_message.clone(), - paillier_keys, - new_party_index, - ))), - old_to_new_map: self.old_to_new_map, - new_t: self.new_t, - new_n: self.new_n, - current_t: self.current_t, - }) - }, - None => Err(FsDkrError::NewPartyUnassignedIndexError), - } - }, - } - } - pub fn is_expensive(&self) -> bool { - false - } + pub fn proceed(self, mut output: O) -> Result + where + O: Push< + Msg< + Option< + JoinMessage< + Secp256k1, + Sha256, + { crate::utilities::STAT_PARAM }, + >, + >, + >, + >, + { + match self.local_key_option { + Some(local_key) => { + output.push(Msg { + sender: local_key.i, + receiver: None, + body: None, + }); + match self.new_party_index_option { + None => Ok(Round1 { + party_type: PartyType::Existing(Box::new(local_key)), + old_to_new_map: self.old_to_new_map, + new_t: self.new_t, + new_n: self.new_n, + current_t: self.current_t, + }), + _ => Err(FsDkrError::NewPartyUnassignedIndexError), + } + }, + None => { + let (mut join_message, paillier_keys) = + JoinMessage::distribute(); + match self.new_party_index_option { + Some(new_party_index) => { + join_message.set_party_index(new_party_index); + output.push(Msg { + sender: join_message.clone().get_party_index()?, + receiver: None, + body: Some(join_message.clone()), + }); + Ok(Round1 { + party_type: PartyType::New(Box::new(( + join_message.clone(), + paillier_keys, + new_party_index, + ))), + old_to_new_map: self.old_to_new_map, + new_t: self.new_t, + new_n: self.new_n, + current_t: self.current_t, + }) + }, + None => Err(FsDkrError::NewPartyUnassignedIndexError), + } + }, + } + } + pub fn is_expensive(&self) -> bool { + false + } } pub struct Round1 { - pub party_type: PartyType, - pub old_to_new_map: HashMap, - new_t: u16, - new_n: u16, - current_t: u16, + pub party_type: PartyType, + pub old_to_new_map: HashMap, + new_t: u16, + new_n: u16, + current_t: u16, } impl Round1 { - pub fn proceed( - self, - input: BroadcastMsgs< - Option>, - >, - mut output: O, - ) -> Result - where - O: Push>>>, - { - let join_message_option_vec = input.into_vec(); - let mut join_message_vec: Vec< - JoinMessage, - > = Vec::new(); - for join_message_option in join_message_option_vec.into_iter().flatten() { - join_message_vec.push(join_message_option) - } - match self.party_type { - PartyType::Existing(mut local_key) => { - // Existing parties form a refresh message and broadcast it. - let old_i = local_key.i; - let join_message_slice = join_message_vec.as_slice(); - let refresh_message_result = RefreshMessage::replace( - join_message_slice, - &mut local_key, - &self.old_to_new_map, - self.new_t, - self.new_n, - ); - let refresh_message = refresh_message_result.unwrap(); - let new_paillier_dk = refresh_message.clone().1; - output.push(Msg { - sender: old_i, - receiver: None, - body: Some(refresh_message.clone().0), - }); - Ok(Round2 { - party_type: PartyType::Existing(local_key), - join_messages: join_message_vec, - refresh_message: Some(refresh_message.0), - new_paillier_decryption_key: new_paillier_dk, - new_t: self.new_t, - new_n: self.new_n, - current_t: self.current_t, - }) - }, - - PartyType::New(boxed_new) => { - let (join_message, paillier_keys, new_party_index) = *boxed_new; - // New parties don't need to form a refresh message. - output.push(Msg { - sender: join_message.get_party_index()?, - receiver: None, - body: None, - }); - Ok(Round2 { - party_type: PartyType::New(Box::new(( - join_message, - paillier_keys.clone(), - new_party_index, - ))), - join_messages: join_message_vec, - new_paillier_decryption_key: paillier_keys.dk, - refresh_message: None, - new_t: self.new_t, - new_n: self.new_n, - current_t: self.current_t, - }) - }, - } - } - - pub fn is_expensive(&self) -> bool { - false - } - - pub fn expects_messages(i: u16, n: u16) -> Round0Messages { - BroadcastMsgsStore::new(i, n) - } + pub fn proceed( + self, + input: BroadcastMsgs< + Option< + JoinMessage< + Secp256k1, + Sha256, + { crate::utilities::STAT_PARAM }, + >, + >, + >, + mut output: O, + ) -> Result + where + O: Push< + Msg< + Option< + RefreshMessage< + Secp256k1, + Sha256, + { crate::utilities::STAT_PARAM }, + >, + >, + >, + >, + { + let join_message_option_vec = input.into_vec(); + let mut join_message_vec: Vec< + JoinMessage, + > = Vec::new(); + for join_message_option in join_message_option_vec.into_iter().flatten() + { + join_message_vec.push(join_message_option) + } + match self.party_type { + PartyType::Existing(mut local_key) => { + // Existing parties form a refresh message and broadcast it. + let old_i = local_key.i; + let join_message_slice = join_message_vec.as_slice(); + let refresh_message_result = RefreshMessage::replace( + join_message_slice, + &mut local_key, + &self.old_to_new_map, + self.new_t, + self.new_n, + ); + let refresh_message = refresh_message_result.unwrap(); + let new_paillier_dk = refresh_message.clone().1; + output.push(Msg { + sender: old_i, + receiver: None, + body: Some(refresh_message.clone().0), + }); + Ok(Round2 { + party_type: PartyType::Existing(local_key), + join_messages: join_message_vec, + refresh_message: Some(refresh_message.0), + new_paillier_decryption_key: new_paillier_dk, + new_t: self.new_t, + new_n: self.new_n, + current_t: self.current_t, + }) + }, + + PartyType::New(boxed_new) => { + let (join_message, paillier_keys, new_party_index) = *boxed_new; + // New parties don't need to form a refresh message. + output.push(Msg { + sender: join_message.get_party_index()?, + receiver: None, + body: None, + }); + Ok(Round2 { + party_type: PartyType::New(Box::new(( + join_message, + paillier_keys.clone(), + new_party_index, + ))), + join_messages: join_message_vec, + new_paillier_decryption_key: paillier_keys.dk, + refresh_message: None, + new_t: self.new_t, + new_n: self.new_n, + current_t: self.current_t, + }) + }, + } + } + + pub fn is_expensive(&self) -> bool { + false + } + + pub fn expects_messages(i: u16, n: u16) -> Round0Messages { + BroadcastMsgsStore::new(i, n) + } } pub struct Round2 { - pub party_type: PartyType, - pub join_messages: Vec>, - pub refresh_message: - Option>, - pub new_paillier_decryption_key: DecryptionKey, - new_t: u16, - new_n: u16, - current_t: u16, + pub party_type: PartyType, + pub join_messages: + Vec>, + pub refresh_message: Option< + RefreshMessage, + >, + pub new_paillier_decryption_key: DecryptionKey, + new_t: u16, + new_n: u16, + current_t: u16, } impl Round2 { - pub fn proceed( - self, - input: BroadcastMsgs< - Option>, - >, - ) -> Result> { - let refresh_message_option_vec = input.into_vec_including_me(self.refresh_message); - let mut refresh_message_vec: Vec< - RefreshMessage, - > = Vec::new(); - for refresh_message_option in refresh_message_option_vec.into_iter().flatten() { - refresh_message_vec.push(refresh_message_option) - } - - match self.party_type { - PartyType::Existing(mut local_key) => { - let join_message_slice = self.join_messages.as_slice(); - let refresh_message_slice = refresh_message_vec.as_slice(); - RefreshMessage::collect( - refresh_message_slice, - &mut local_key, - self.new_paillier_decryption_key, - join_message_slice, - self.current_t, - )?; - Ok(*local_key) - }, - PartyType::New(boxed_new) => { - let (join_message, paillier_keys, _new_party_index) = *boxed_new; - let join_message_slice = self.join_messages.as_slice(); - let refresh_message_slice = refresh_message_vec.as_slice(); - JoinMessage::collect( - &join_message, - refresh_message_slice, - paillier_keys, - join_message_slice, - self.new_t, - self.new_n, - self.current_t, - ) - }, - } - } - - pub fn is_expensive(&self) -> bool { - false - } - pub fn expects_messages(i: u16, n: u16) -> Round1Messages { - BroadcastMsgsStore::new(i, n) - } + pub fn proceed( + self, + input: BroadcastMsgs< + Option< + RefreshMessage< + Secp256k1, + Sha256, + { crate::utilities::STAT_PARAM }, + >, + >, + >, + ) -> Result> { + let refresh_message_option_vec = + input.into_vec_including_me(self.refresh_message); + let mut refresh_message_vec: Vec< + RefreshMessage, + > = Vec::new(); + for refresh_message_option in + refresh_message_option_vec.into_iter().flatten() + { + refresh_message_vec.push(refresh_message_option) + } + + match self.party_type { + PartyType::Existing(mut local_key) => { + let join_message_slice = self.join_messages.as_slice(); + let refresh_message_slice = refresh_message_vec.as_slice(); + RefreshMessage::collect( + refresh_message_slice, + &mut local_key, + self.new_paillier_decryption_key, + join_message_slice, + self.current_t, + )?; + Ok(*local_key) + }, + PartyType::New(boxed_new) => { + let (join_message, paillier_keys, _new_party_index) = + *boxed_new; + let join_message_slice = self.join_messages.as_slice(); + let refresh_message_slice = refresh_message_vec.as_slice(); + JoinMessage::collect( + &join_message, + refresh_message_slice, + paillier_keys, + join_message_slice, + self.new_t, + self.new_n, + self.current_t, + ) + }, + } + } + + pub fn is_expensive(&self) -> bool { + false + } + pub fn expects_messages(i: u16, n: u16) -> Round1Messages { + BroadcastMsgsStore::new(i, n) + } } type Result = std::result::Result; diff --git a/src/refresh/state_machine.rs b/src/refresh/state_machine.rs index 2dafcbce..45d4d1e0 100644 --- a/src/refresh/state_machine.rs +++ b/src/refresh/state_machine.rs @@ -19,281 +19,337 @@ use crate::utilities::sha2::Sha256; use std::{collections::HashMap, fmt, mem::replace, time::Duration}; use thiserror::Error; -pub type Round0Messages = - Store>>>; +pub type Round0Messages = Store< + BroadcastMsgs< + Option< + JoinMessage, + >, + >, +>; pub type Round1Messages = Store< - BroadcastMsgs>>, + BroadcastMsgs< + Option< + RefreshMessage, + >, + >, >; pub struct KeyRefresh { - // Current round - round: R, + // Current round + round: R, - // Messages - round0_msgs: Option, - round1_msgs: Option, + // Messages + round0_msgs: Option, + round1_msgs: Option, - // Message queue - msgs_queue: Vec>, + // Message queue + msgs_queue: Vec>, - party_i: u16, + party_i: u16, - party_n: u16, + party_n: u16, } impl KeyRefresh { - pub fn new( - local_key_option: Option>, - new_party_index_option: Option, - old_to_new_map: &HashMap, - new_t: u16, - new_n: u16, - current_t_option: Option, - ) -> Result { - if new_n < 2 { - return Err(Error::TooFewParties) - } - if new_t == 0 || new_t >= new_n { - return Err(Error::InvalidThreshold) - } - - // Sets the party index either from the `local_key_option` or `new_party_index_option`, - // otherwise returns an error if neither is Some. - let i = match local_key_option.as_ref().map(|it| it.i).or(new_party_index_option) { - Some(it) => it, - None => return Err(Error::MissingPartyIndex), - }; - - // Sets the current/old threshold either from the `local_key_option` or `current_t_option`, - // otherwise returns an error if neither is Some. - let current_t = match local_key_option.as_ref().map(|it| it.t).or(current_t_option) { - Some(it) => it, - None => return Err(Error::UnknownCurrentThreshold), - }; - - let mut state = Self { - round: R::Round0(Box::new(Round0 { - local_key_option, - new_party_index_option, - old_to_new_map: old_to_new_map.clone(), - new_t, - new_n, - current_t, - })), - - round0_msgs: Some(Round1::expects_messages(i, new_n)), - round1_msgs: Some(Round2::expects_messages(i, new_n)), - - msgs_queue: vec![], - - party_i: i, - - party_n: new_n, - }; - - state.proceed_round(false)?; - Ok(state) - } - - fn gmap_queue<'a, T, F>(&'a mut self, mut f: F) -> impl Push> + 'a - where - F: FnMut(T) -> M + 'a, - { - (&mut self.msgs_queue).gmap(move |m: Msg| m.map_body(|m| ProtocolMessage(f(m)))) - } - - /// Proceeds round state if it received enough messages and if it's cheap to compute or - /// `may_block == true` - fn proceed_round(&mut self, may_block: bool) -> Result<()> { - let store1_wants_more = self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - - let next_state: R; - - let try_again: bool = match replace(&mut self.round, R::Gone) { - R::Round0(round) if !round.is_expensive() || may_block => { - next_state = round - .proceed(self.gmap_queue(M::Round1)) - .map(|msg| R::Round1(Box::new(msg))) - .map_err(Error::ProceedRound)?; - true - }, - s @ R::Round0(_) => { - next_state = s; - false - }, - R::Round1(round) if !store1_wants_more && (!round.is_expensive() || may_block) => { - let store = self.round0_msgs.take().ok_or(InternalError::StoreGone)?; - let msgs = store.finish().map_err(InternalError::RetrieveRoundMessages)?; - next_state = round - .proceed(msgs, self.gmap_queue(M::Round2)) - .map(|msg| R::Round2(Box::new(msg))) - .map_err(Error::ProceedRound)?; - true - }, - s @ R::Round1(_) => { - next_state = s; - false - }, - R::Round2(round) if !store2_wants_more && (!round.is_expensive() || may_block) => { - let store = self.round1_msgs.take().ok_or(InternalError::StoreGone)?; - let msgs = store.finish().map_err(InternalError::RetrieveRoundMessages)?; - next_state = round - .proceed(msgs) - .map(|msg| R::Final(Box::new(msg))) - .map_err(Error::ProceedRound)?; - true - }, - s @ R::Round2(_) => { - next_state = s; - false - }, - s @ R::Final(_) | s @ R::Gone => { - next_state = s; - false - }, - }; - self.round = next_state; - if try_again { - self.proceed_round(may_block) - } else { - Ok(()) - } - } + pub fn new( + local_key_option: Option>, + new_party_index_option: Option, + old_to_new_map: &HashMap, + new_t: u16, + new_n: u16, + current_t_option: Option, + ) -> Result { + if new_n < 2 { + return Err(Error::TooFewParties) + } + if new_t == 0 || new_t >= new_n { + return Err(Error::InvalidThreshold) + } + + // Sets the party index either from the `local_key_option` or + // `new_party_index_option`, otherwise returns an error if + // neither is Some. + let i = match local_key_option + .as_ref() + .map(|it| it.i) + .or(new_party_index_option) + { + Some(it) => it, + None => return Err(Error::MissingPartyIndex), + }; + + // Sets the current/old threshold either from the `local_key_option` or + // `current_t_option`, otherwise returns an error if neither is + // Some. + let current_t = + match local_key_option.as_ref().map(|it| it.t).or(current_t_option) + { + Some(it) => it, + None => return Err(Error::UnknownCurrentThreshold), + }; + + let mut state = Self { + round: R::Round0(Box::new(Round0 { + local_key_option, + new_party_index_option, + old_to_new_map: old_to_new_map.clone(), + new_t, + new_n, + current_t, + })), + + round0_msgs: Some(Round1::expects_messages(i, new_n)), + round1_msgs: Some(Round2::expects_messages(i, new_n)), + + msgs_queue: vec![], + + party_i: i, + + party_n: new_n, + }; + + state.proceed_round(false)?; + Ok(state) + } + + fn gmap_queue<'a, T, F>(&'a mut self, mut f: F) -> impl Push> + 'a + where + F: FnMut(T) -> M + 'a, + { + (&mut self.msgs_queue) + .gmap(move |m: Msg| m.map_body(|m| ProtocolMessage(f(m)))) + } + + /// Proceeds round state if it received enough messages and if it's cheap to + /// compute or `may_block == true` + fn proceed_round(&mut self, may_block: bool) -> Result<()> { + let store1_wants_more = + self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = + self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + + let next_state: R; + + let try_again: bool = match replace(&mut self.round, R::Gone) { + R::Round0(round) if !round.is_expensive() || may_block => { + next_state = round + .proceed(self.gmap_queue(M::Round1)) + .map(|msg| R::Round1(Box::new(msg))) + .map_err(Error::ProceedRound)?; + true + }, + s @ R::Round0(_) => { + next_state = s; + false + }, + R::Round1(round) + if !store1_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.round0_msgs.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs, self.gmap_queue(M::Round2)) + .map(|msg| R::Round2(Box::new(msg))) + .map_err(Error::ProceedRound)?; + true + }, + s @ R::Round1(_) => { + next_state = s; + false + }, + R::Round2(round) + if !store2_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.round1_msgs.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs) + .map(|msg| R::Final(Box::new(msg))) + .map_err(Error::ProceedRound)?; + true + }, + s @ R::Round2(_) => { + next_state = s; + false + }, + s @ R::Final(_) | s @ R::Gone => { + next_state = s; + false + }, + }; + self.round = next_state; + if try_again { + self.proceed_round(may_block) + } else { + Ok(()) + } + } } impl StateMachine for KeyRefresh { - type MessageBody = ProtocolMessage; - type Err = Error; - type Output = LocalKey; - - fn handle_incoming(&mut self, msg: Msg) -> Result<()> { - let current_round = self.current_round(); - - match msg.body { - ProtocolMessage(M::Round1(m)) => { - let store = self - .round0_msgs - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { current_round, msg_round: 1 })?; - store - .push_msg(Msg { sender: msg.sender, receiver: msg.receiver, body: m }) - .map_err(Error::HandleMessage)?; - self.proceed_round(false) - }, - ProtocolMessage(M::Round2(m)) => { - let store = self - .round1_msgs - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { current_round, msg_round: 2 })?; - store - .push_msg(Msg { sender: msg.sender, receiver: msg.receiver, body: m }) - .map_err(Error::HandleMessage)?; - self.proceed_round(false) - }, - } - } - - fn message_queue(&mut self) -> &mut Vec> { - &mut self.msgs_queue - } - - fn wants_to_proceed(&self) -> bool { - let store1_wants_more = self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - - match &self.round { - R::Round0(_) => true, - R::Round1(_) => !store1_wants_more, - R::Round2(_) => !store2_wants_more, - R::Final(_) | R::Gone => false, - } - } - - fn proceed(&mut self) -> Result<()> { - self.proceed_round(true) - } - - fn round_timeout(&self) -> Option { - None - } - - fn round_timeout_reached(&mut self) -> Self::Err { - panic!("no timeout was set") - } - - fn is_finished(&self) -> bool { - matches!(self.round, R::Final(_)) - } - - fn pick_output(&mut self) -> Option> { - match self.round { - R::Final(_) => (), - R::Gone => return Some(Err(Error::DoublePickOutput)), - _ => return None, - } - - match replace(&mut self.round, R::Gone) { - R::Final(result) => Some(Ok(*result)), - _ => unreachable!("guaranteed by match expression above"), - } - } - - fn current_round(&self) -> u16 { - match &self.round { - R::Round0(_) => 0, - R::Round1(_) => 1, - R::Round2(_) => 2, - R::Final(_) | R::Gone => 3, - } - } - - fn total_rounds(&self) -> Option { - Some(2) - } - - fn party_ind(&self) -> u16 { - self.party_i - } - - fn parties(&self) -> u16 { - self.party_n - } + type MessageBody = ProtocolMessage; + type Err = Error; + type Output = LocalKey; + + fn handle_incoming(&mut self, msg: Msg) -> Result<()> { + let current_round = self.current_round(); + + match msg.body { + ProtocolMessage(M::Round1(m)) => { + let store = self.round0_msgs.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 1, + }, + )?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + }, + ProtocolMessage(M::Round2(m)) => { + let store = self.round1_msgs.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 2, + }, + )?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + }, + } + } + + fn message_queue(&mut self) -> &mut Vec> { + &mut self.msgs_queue + } + + fn wants_to_proceed(&self) -> bool { + let store1_wants_more = + self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = + self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + + match &self.round { + R::Round0(_) => true, + R::Round1(_) => !store1_wants_more, + R::Round2(_) => !store2_wants_more, + R::Final(_) | R::Gone => false, + } + } + + fn proceed(&mut self) -> Result<()> { + self.proceed_round(true) + } + + fn round_timeout(&self) -> Option { + None + } + + fn round_timeout_reached(&mut self) -> Self::Err { + panic!("no timeout was set") + } + + fn is_finished(&self) -> bool { + matches!(self.round, R::Final(_)) + } + + fn pick_output(&mut self) -> Option> { + match self.round { + R::Final(_) => (), + R::Gone => return Some(Err(Error::DoublePickOutput)), + _ => return None, + } + + match replace(&mut self.round, R::Gone) { + R::Final(result) => Some(Ok(*result)), + _ => unreachable!("guaranteed by match expression above"), + } + } + + fn current_round(&self) -> u16 { + match &self.round { + R::Round0(_) => 0, + R::Round1(_) => 1, + R::Round2(_) => 2, + R::Final(_) | R::Gone => 3, + } + } + + fn total_rounds(&self) -> Option { + Some(2) + } + + fn party_ind(&self) -> u16 { + self.party_i + } + + fn parties(&self) -> u16 { + self.party_n + } } impl crate::traits::RoundBlame for KeyRefresh { - fn round_blame(&self) -> (u16, Vec) { - let store1_blame = self.round0_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store2_blame = self.round1_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - - let default = (0, vec![]); - match &self.round { - R::Round0(_) => default, - R::Round1(_) => store1_blame, - R::Round2(_) => store2_blame, - R::Final(_) | R::Gone => default, - } - } + fn round_blame(&self) -> (u16, Vec) { + let store1_blame = + self.round0_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store2_blame = + self.round1_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); + + let default = (0, vec![]); + match &self.round { + R::Round0(_) => default, + R::Round1(_) => store1_blame, + R::Round2(_) => store2_blame, + R::Final(_) | R::Gone => default, + } + } } impl fmt::Debug for KeyRefresh { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let current_round = match &self.round { - R::Round0(_) => "0", - R::Round1(_) => "1", - R::Round2(_) => "2", - R::Final(_) => "[Final]", - R::Gone => "[Gone]", - }; - let round0_msgs = match self.round0_msgs.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), - None => "[None]".into(), - }; - let round1_msgs = match self.round1_msgs.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), - None => "[None]".into(), - }; - write!( + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let current_round = match &self.round { + R::Round0(_) => "0", + R::Round1(_) => "1", + R::Round2(_) => "2", + R::Final(_) => "[Final]", + R::Gone => "[Gone]", + }; + let round0_msgs = match self.round0_msgs.as_ref() { + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), + None => "[None]".into(), + }; + let round1_msgs = match self.round1_msgs.as_ref() { + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), + None => "[None]".into(), + }; + write!( f, "{{Key refresh at round={} round0_msgs={} round1_msgs={} queue=[len={}]}}", current_round, @@ -301,30 +357,39 @@ impl fmt::Debug for KeyRefresh { round1_msgs, self.msgs_queue.len() ) - } + } } // Rounds enum R { - Round0(Box), - Round1(Box), - Round2(Box), - Final(Box>), - Gone, + Round0(Box), + Round1(Box), + Round2(Box), + Final(Box>), + Gone, } // Messages /// Protocol message which parties send on wire /// -/// Hides actual messages structure so it could be changed without breaking semver policy. +/// Hides actual messages structure so it could be changed without breaking +/// semver policy. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProtocolMessage(M); #[derive(Debug, Clone, Serialize, Deserialize)] enum M { - Round1(Option>), - Round2(Option>), + Round1( + Option< + JoinMessage, + >, + ), + Round2( + Option< + RefreshMessage, + >, + ), } // Error @@ -335,73 +400,75 @@ type Result = std::result::Result; #[derive(Debug, Error)] #[non_exhaustive] pub enum Error { - /// Round proceeding resulted in error - #[error("proceed round: {0}")] - ProceedRound(#[source] FsDkrError), - - /// Too few parties (`n < 2`) - #[error("at least 2 parties are required for keygen")] - TooFewParties, - /// Threshold value `t` is not in range `[1; n-1]` - #[error("threshold is not in range [1; n-1]")] - InvalidThreshold, - /// Party index `i` is not in range `[1; n]` - #[error("party index is not in range [1; n]")] - InvalidPartyIndex, - /// No index set for new party - #[error("no index set for party")] - MissingPartyIndex, - /// Party doesn't know the current threshold - #[error("party doesn't know the current threshold")] - UnknownCurrentThreshold, - - /// Received message didn't pass pre-validation - #[error("received message didn't pass pre-validation: {0}")] - HandleMessage(#[source] StoreErr), - /// Received message which we didn't expect to receive now (e.g. message from previous round) - #[error( + /// Round proceeding resulted in error + #[error("proceed round: {0}")] + ProceedRound(#[source] FsDkrError), + + /// Too few parties (`n < 2`) + #[error("at least 2 parties are required for keygen")] + TooFewParties, + /// Threshold value `t` is not in range `[1; n-1]` + #[error("threshold is not in range [1; n-1]")] + InvalidThreshold, + /// Party index `i` is not in range `[1; n]` + #[error("party index is not in range [1; n]")] + InvalidPartyIndex, + /// No index set for new party + #[error("no index set for party")] + MissingPartyIndex, + /// Party doesn't know the current threshold + #[error("party doesn't know the current threshold")] + UnknownCurrentThreshold, + + /// Received message didn't pass pre-validation + #[error("received message didn't pass pre-validation: {0}")] + HandleMessage(#[source] StoreErr), + /// Received message which we didn't expect to receive now (e.g. message + /// from previous round) + #[error( "didn't expect to receive message from round {msg_round} (being at round {current_round})" )] - ReceivedOutOfOrderMessage { current_round: u16, msg_round: u16 }, - /// [Keygen::pick_output] called twice - #[error("pick_output called twice")] - DoublePickOutput, - - /// Some internal assertions were failed, which is a bug - #[doc(hidden)] - #[error("internal error: {0:?}")] - InternalError(InternalError), + ReceivedOutOfOrderMessage { current_round: u16, msg_round: u16 }, + /// [Keygen::pick_output] called twice + #[error("pick_output called twice")] + DoublePickOutput, + + /// Some internal assertions were failed, which is a bug + #[doc(hidden)] + #[error("internal error: {0:?}")] + InternalError(InternalError), } impl IsCritical for Error { - fn is_critical(&self) -> bool { - true - } + fn is_critical(&self) -> bool { + true + } } impl From for Error { - fn from(err: InternalError) -> Self { - Self::InternalError(err) - } + fn from(err: InternalError) -> Self { + Self::InternalError(err) + } } mod private { - #[derive(Debug)] - #[non_exhaustive] - pub enum InternalError { - /// [Messages store](super::MessageStore) reported that it received all messages it wanted - /// to receive, but refused to return message container - RetrieveRoundMessages(super::StoreErr), - #[doc(hidden)] - StoreGone, - } + #[derive(Debug)] + #[non_exhaustive] + pub enum InternalError { + /// [Messages store](super::MessageStore) reported that it received all + /// messages it wanted to receive, but refused to return + /// message container + RetrieveRoundMessages(super::StoreErr), + #[doc(hidden)] + StoreGone, + } } #[cfg(test)] pub mod test { - use std::collections::HashMap; + use std::collections::HashMap; - use crate::refresh::state_machine::KeyRefresh; + use crate::refresh::state_machine::KeyRefresh; #[allow(unused_imports)] use curv::{ cryptographic_primitives::{ @@ -413,389 +480,482 @@ pub mod test { use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::keygen::*; use round_based::dev::Simulation; - #[allow(dead_code)] - fn simulate_keygen(t: u16, n: u16) -> Vec> { - //simulate keygen - let mut simulation = Simulation::new(); - simulation.enable_benchmarks(false); - - for i in 1..=n { - simulation.add_party(Keygen::new(i, t, n).unwrap()); - } - - simulation.run().unwrap() - } - - // Refresh Keys: Only Existing Parties (No New Parties) - pub fn simulate_dkr_with_no_replacements( - old_local_keys: Vec>, - old_to_new_map: &HashMap, - ) -> Vec> { - let mut simulation = Simulation::new(); - simulation.enable_benchmarks(false); - - for old_local_key in old_local_keys { - simulation.add_party( - KeyRefresh::new( - Some(old_local_key.clone()), - None, - old_to_new_map, - old_local_key.t, - old_local_key.n, - None, - ) - .unwrap(), - ); - } - - simulation.run().unwrap() - } - - #[test] - pub fn test_dkr_with_no_new_parties() { - let t = 2; - let n = 5; - let local_keys = simulate_keygen(t, n); - - let old_local_keys = local_keys.clone(); - let mut old_to_new_map = HashMap::new(); - old_to_new_map.insert(1, 2); - old_to_new_map.insert(2, 1); - old_to_new_map.insert(3, 4); - old_to_new_map.insert(4, 3); - old_to_new_map.insert(5, 5); - let mut new_local_keys = simulate_dkr_with_no_replacements(local_keys, &old_to_new_map); - new_local_keys.sort_by(|a, b| a.i.cmp(&b.i)); - let old_linear_secret_key: Vec<_> = (0..old_local_keys.len()) - .map(|i| old_local_keys[i].keys_linear.x_i.clone()) - .collect(); - - let new_linear_secret_key: Vec<_> = (0..new_local_keys.len()) - .map(|i| new_local_keys[i].keys_linear.x_i.clone()) - .collect(); - let indices: Vec<_> = (0..(t + 1)).collect(); - let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, - commitments: Vec::new(), - proof: DLogProof::::prove(&Scalar::random()), - }; - - assert_eq!( - vss.reconstruct(&indices[..], &old_linear_secret_key[0..(t + 1) as usize]), - vss.reconstruct(&indices[..], &new_linear_secret_key[0..(t + 1) as usize]) - ); - assert_ne!(old_linear_secret_key, new_linear_secret_key); - } - - // Refresh Keys: All Existing Parties Stay, New Parties Join - pub fn simulate_dkr_with_new_parties( - old_local_keys: Vec>, - new_party_indices: Vec, - old_to_new_map: HashMap, - t: u16, - n: u16, - ) -> Vec> { - let mut simulation = Simulation::new(); - simulation.enable_benchmarks(false); - - for old_local_key in old_local_keys { - simulation.add_party( - KeyRefresh::new( - Some(old_local_key.clone()), - None, - &old_to_new_map, - old_local_key.clone().t, - n, - None, - ) - .unwrap(), - ); - } - - for index in new_party_indices { - simulation.add_party( - KeyRefresh::new(None, Some(index), &old_to_new_map, t, n, Some(t)).unwrap(), - ); - } - simulation.run().unwrap() - } - - #[test] - pub fn test_dkr_with_new_parties() { - let t = 2; - let n = 5; - let local_keys = simulate_keygen(t, n); - let mut old_to_new_map = HashMap::new(); - old_to_new_map.insert(1, 1); - old_to_new_map.insert(2, 2); - old_to_new_map.insert(3, 3); - old_to_new_map.insert(4, 4); - old_to_new_map.insert(5, 5); - let old_local_keys = local_keys.clone(); - let new_local_keys = - simulate_dkr_with_new_parties(local_keys, vec![6, 7], old_to_new_map, t, n + 2); - - let old_linear_secret_key: Vec<_> = (0..old_local_keys.len()) - .map(|i| old_local_keys[i].keys_linear.x_i.clone()) - .collect(); - - let new_linear_secret_key: Vec<_> = (0..new_local_keys.len()) - .map(|i| new_local_keys[i].keys_linear.x_i.clone()) - .collect(); - let indices: Vec<_> = (0..(t + 1)).collect(); - let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, - commitments: Vec::new(), - proof: DLogProof::::prove(&Scalar::random()), - }; - - let new_vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n + 2 }, - commitments: Vec::new(), - proof: DLogProof::::prove(&Scalar::random()), - }; - - assert_eq!( - vss.reconstruct(&indices[..], &old_linear_secret_key[0..(t + 1) as usize]), - new_vss.reconstruct(&indices[..], &new_linear_secret_key[0..(t + 1) as usize]) - ); - assert_ne!(old_linear_secret_key, new_linear_secret_key); - } - - // Refresh keys with remove parties - - pub fn simulate_dkr_with_remove_parties( - old_local_keys: Vec>, - remaining_old_party_indices: Vec, - new_party_indices: Vec, - old_to_new_map: HashMap, - t: u16, - n: u16, - ) -> Vec> { - let mut simulation = Simulation::new(); - simulation.enable_benchmarks(false); - - let mut non_removed_parties_vec: Vec> = vec![]; - for old_local_key in old_local_keys { - if remaining_old_party_indices.contains(&old_local_key.i) { - non_removed_parties_vec.push(old_local_key); - } else { - } - } - - for non_removed_local_key in non_removed_parties_vec { - simulation.add_party( - KeyRefresh::new( - Some(non_removed_local_key.clone()), - None, - &old_to_new_map, - non_removed_local_key.clone().t, - n, - None, - ) - .unwrap(), - ); - } - - for index in new_party_indices { - simulation.add_party( - KeyRefresh::new(None, Some(index), &old_to_new_map, t, n, Some(t)).unwrap(), - ); - } - - simulation.run().unwrap() - } - - #[test] - pub fn test_dkr_with_remove_parties() { - let t = 2; - let n = 5; - let local_keys = simulate_keygen(t, n); - - let old_local_keys = local_keys.clone(); - let mut old_to_new_map = HashMap::new(); - old_to_new_map.insert(1, 1); - old_to_new_map.insert(2, 2); - old_to_new_map.insert(3, 3); - old_to_new_map.insert(4, 4); - let new_local_keys = simulate_dkr_with_remove_parties( - local_keys, - vec![1, 2, 3, 4], - vec![], - old_to_new_map, - t, - n - 1, - ); - - let old_linear_secret_key: Vec<_> = (0..old_local_keys.len()) - .map(|i| old_local_keys[i].keys_linear.x_i.clone()) - .collect(); - - let new_linear_secret_key: Vec<_> = (0..new_local_keys.len()) - .map(|i| new_local_keys[i].keys_linear.x_i.clone()) - .collect(); - let indices: Vec<_> = (0..(t + 1)).collect(); - let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, - commitments: Vec::new(), - proof: DLogProof::::prove(&Scalar::random()), - }; - - assert_eq!( - vss.reconstruct(&indices[..], &old_linear_secret_key[0..(t + 1) as usize]), - vss.reconstruct(&indices[..], &new_linear_secret_key[0..(t + 1) as usize]) - ); - assert_ne!(old_linear_secret_key, new_linear_secret_key); - } - - // Refresh Keys: Some Existing Parties Leave, New Parties Replace Them - pub fn simulate_dkr_with_replace_parties( - old_local_keys: Vec>, - new_party_indices: Vec, - old_to_new_map: HashMap, - t: u16, - n: u16, - ) -> Vec> { - let mut simulation = Simulation::new(); - simulation.enable_benchmarks(false); - - for old_local_key in old_local_keys { - if !new_party_indices.contains(&old_local_key.i) { - simulation.add_party( - KeyRefresh::new( - Some(old_local_key.clone()), - None, - &old_to_new_map, - old_local_key.clone().t, - n, - None, - ) - .unwrap(), - ); - } else { - } - } - - for index in new_party_indices { - simulation.add_party( - KeyRefresh::new(None, Some(index), &old_to_new_map, t, n, Some(t)).unwrap(), - ); - } - simulation.run().unwrap() - } - - #[test] - pub fn test_dkr_with_replace_parties() { - let t = 2; - let n = 5; - let local_keys = simulate_keygen(t, n); - let old_local_keys = local_keys.clone(); - let mut old_to_new_map = HashMap::new(); - old_to_new_map.insert(1, 3); - old_to_new_map.insert(3, 1); - old_to_new_map.insert(4, 5); - old_to_new_map.insert(5, 4); - let mut new_local_keys = - simulate_dkr_with_replace_parties(local_keys, vec![2, 6], old_to_new_map, t, n + 1); - new_local_keys.sort_by(|a, b| a.i.cmp(&b.i)); - let old_linear_secret_key: Vec<_> = (0..old_local_keys.len()) - .map(|i| old_local_keys[i].keys_linear.x_i.clone()) - .collect(); - - let new_linear_secret_key: Vec<_> = (0..new_local_keys.len()) - .map(|i| new_local_keys[i].keys_linear.x_i.clone()) - .collect(); - - let old_indices = vec![0, 1, 2]; - let new_indices = vec![0, 1, 2]; - let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n }, - commitments: Vec::new(), - proof: DLogProof::::prove(&Scalar::random()), - }; - - let new_vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: t, share_count: n + 1 }, - commitments: Vec::new(), - proof: DLogProof::::prove(&Scalar::random()), - }; - - assert_eq!( - vss.reconstruct(&old_indices[..], &old_linear_secret_key[0..(t + 1) as usize]), - new_vss.reconstruct(&new_indices[..], &new_linear_secret_key[0..(t + 1) as usize]) - ); - assert_ne!(old_linear_secret_key, new_linear_secret_key); - } - - // Refresh Keys: New Threshold, Only Existing Parties (No New Parties). - pub fn simulate_dkr_with_new_threshold( - old_local_keys: Vec>, - old_to_new_map: &HashMap, - new_t: u16, - ) -> Vec> { - let mut simulation = Simulation::new(); - simulation.enable_benchmarks(false); - - for old_local_key in old_local_keys { - simulation.add_party( - KeyRefresh::new( - Some(old_local_key.clone()), - None, - old_to_new_map, - new_t, - old_local_key.n, - None, - ) - .unwrap(), - ); - } - - simulation.run().unwrap() - } - - #[test] - pub fn test_dkr_with_new_threshold() { - let init_t = 2; // initial threshold. - let new_t = 1; // new/final threshold. - let n = 5; - let local_keys = simulate_keygen(init_t, n); - - let old_local_keys = local_keys.clone(); - let mut old_to_new_map = HashMap::new(); - old_to_new_map.insert(1, 2); - old_to_new_map.insert(2, 1); - old_to_new_map.insert(3, 4); - old_to_new_map.insert(4, 3); - old_to_new_map.insert(5, 5); - let mut new_local_keys = - simulate_dkr_with_new_threshold(local_keys, &old_to_new_map, new_t); - new_local_keys.sort_by(|a, b| a.i.cmp(&b.i)); - - let old_linear_secret_key: Vec<_> = (0..old_local_keys.len()) - .map(|i| old_local_keys[i].keys_linear.x_i.clone()) - .collect(); - let new_linear_secret_key: Vec<_> = (0..new_local_keys.len()) - .map(|i| new_local_keys[i].keys_linear.x_i.clone()) - .collect(); - - let old_indices: Vec<_> = (0..(init_t + 1)).collect(); - let new_indices: Vec<_> = (0..(new_t + 1)).collect(); - - let vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: init_t, share_count: n }, - commitments: Vec::new(), - proof: DLogProof::::prove(&Scalar::random()), - }; - let new_vss = VerifiableSS:: { - parameters: ShamirSecretSharing { threshold: new_t, share_count: n }, - commitments: Vec::new(), - proof: DLogProof::::prove(&Scalar::random()), - }; - - assert_eq!( - vss.reconstruct(&old_indices[..], &old_linear_secret_key[0..(init_t + 1) as usize]), - new_vss.reconstruct(&new_indices[..], &new_linear_secret_key[0..(new_t + 1) as usize]) - ); - assert_ne!(old_linear_secret_key, new_linear_secret_key); - } + #[allow(dead_code)] + fn simulate_keygen(t: u16, n: u16) -> Vec> { + //simulate keygen + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + for i in 1..=n { + simulation.add_party(Keygen::new(i, t, n).unwrap()); + } + + simulation.run().unwrap() + } + + // Refresh Keys: Only Existing Parties (No New Parties) + pub fn simulate_dkr_with_no_replacements( + old_local_keys: Vec>, + old_to_new_map: &HashMap, + ) -> Vec> { + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + for old_local_key in old_local_keys { + simulation.add_party( + KeyRefresh::new( + Some(old_local_key.clone()), + None, + old_to_new_map, + old_local_key.t, + old_local_key.n, + None, + ) + .unwrap(), + ); + } + + simulation.run().unwrap() + } + + #[test] + pub fn test_dkr_with_no_new_parties() { + let t = 2; + let n = 5; + let local_keys = simulate_keygen(t, n); + + let old_local_keys = local_keys.clone(); + let mut old_to_new_map = HashMap::new(); + old_to_new_map.insert(1, 2); + old_to_new_map.insert(2, 1); + old_to_new_map.insert(3, 4); + old_to_new_map.insert(4, 3); + old_to_new_map.insert(5, 5); + let mut new_local_keys = + simulate_dkr_with_no_replacements(local_keys, &old_to_new_map); + new_local_keys.sort_by(|a, b| a.i.cmp(&b.i)); + let old_linear_secret_key: Vec<_> = (0..old_local_keys.len()) + .map(|i| old_local_keys[i].keys_linear.x_i.clone()) + .collect(); + + let new_linear_secret_key: Vec<_> = (0..new_local_keys.len()) + .map(|i| new_local_keys[i].keys_linear.x_i.clone()) + .collect(); + let indices: Vec<_> = (0..(t + 1)).collect(); + let vss = VerifiableSS:: { + parameters: ShamirSecretSharing { threshold: t, share_count: n }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random(), + ), + }; + + assert_eq!( + vss.reconstruct( + &indices[..], + &old_linear_secret_key[0..(t + 1) as usize] + ), + vss.reconstruct( + &indices[..], + &new_linear_secret_key[0..(t + 1) as usize] + ) + ); + assert_ne!(old_linear_secret_key, new_linear_secret_key); + } + + // Refresh Keys: All Existing Parties Stay, New Parties Join + pub fn simulate_dkr_with_new_parties( + old_local_keys: Vec>, + new_party_indices: Vec, + old_to_new_map: HashMap, + t: u16, + n: u16, + ) -> Vec> { + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + for old_local_key in old_local_keys { + simulation.add_party( + KeyRefresh::new( + Some(old_local_key.clone()), + None, + &old_to_new_map, + old_local_key.clone().t, + n, + None, + ) + .unwrap(), + ); + } + + for index in new_party_indices { + simulation.add_party( + KeyRefresh::new( + None, + Some(index), + &old_to_new_map, + t, + n, + Some(t), + ) + .unwrap(), + ); + } + simulation.run().unwrap() + } + + #[test] + pub fn test_dkr_with_new_parties() { + let t = 2; + let n = 5; + let local_keys = simulate_keygen(t, n); + let mut old_to_new_map = HashMap::new(); + old_to_new_map.insert(1, 1); + old_to_new_map.insert(2, 2); + old_to_new_map.insert(3, 3); + old_to_new_map.insert(4, 4); + old_to_new_map.insert(5, 5); + let old_local_keys = local_keys.clone(); + let new_local_keys = simulate_dkr_with_new_parties( + local_keys, + vec![6, 7], + old_to_new_map, + t, + n + 2, + ); + + let old_linear_secret_key: Vec<_> = (0..old_local_keys.len()) + .map(|i| old_local_keys[i].keys_linear.x_i.clone()) + .collect(); + + let new_linear_secret_key: Vec<_> = (0..new_local_keys.len()) + .map(|i| new_local_keys[i].keys_linear.x_i.clone()) + .collect(); + let indices: Vec<_> = (0..(t + 1)).collect(); + let vss = VerifiableSS:: { + parameters: ShamirSecretSharing { threshold: t, share_count: n }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random(), + ), + }; + + let new_vss = VerifiableSS:: { + parameters: ShamirSecretSharing { + threshold: t, + share_count: n + 2, + }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random(), + ), + }; + + assert_eq!( + vss.reconstruct( + &indices[..], + &old_linear_secret_key[0..(t + 1) as usize] + ), + new_vss.reconstruct( + &indices[..], + &new_linear_secret_key[0..(t + 1) as usize] + ) + ); + assert_ne!(old_linear_secret_key, new_linear_secret_key); + } + + // Refresh keys with remove parties + + pub fn simulate_dkr_with_remove_parties( + old_local_keys: Vec>, + remaining_old_party_indices: Vec, + new_party_indices: Vec, + old_to_new_map: HashMap, + t: u16, + n: u16, + ) -> Vec> { + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + let mut non_removed_parties_vec: Vec> = vec![]; + for old_local_key in old_local_keys { + if remaining_old_party_indices.contains(&old_local_key.i) { + non_removed_parties_vec.push(old_local_key); + } else { + } + } + + for non_removed_local_key in non_removed_parties_vec { + simulation.add_party( + KeyRefresh::new( + Some(non_removed_local_key.clone()), + None, + &old_to_new_map, + non_removed_local_key.clone().t, + n, + None, + ) + .unwrap(), + ); + } + + for index in new_party_indices { + simulation.add_party( + KeyRefresh::new( + None, + Some(index), + &old_to_new_map, + t, + n, + Some(t), + ) + .unwrap(), + ); + } + + simulation.run().unwrap() + } + + #[test] + pub fn test_dkr_with_remove_parties() { + let t = 2; + let n = 5; + let local_keys = simulate_keygen(t, n); + + let old_local_keys = local_keys.clone(); + let mut old_to_new_map = HashMap::new(); + old_to_new_map.insert(1, 1); + old_to_new_map.insert(2, 2); + old_to_new_map.insert(3, 3); + old_to_new_map.insert(4, 4); + let new_local_keys = simulate_dkr_with_remove_parties( + local_keys, + vec![1, 2, 3, 4], + vec![], + old_to_new_map, + t, + n - 1, + ); + + let old_linear_secret_key: Vec<_> = (0..old_local_keys.len()) + .map(|i| old_local_keys[i].keys_linear.x_i.clone()) + .collect(); + + let new_linear_secret_key: Vec<_> = (0..new_local_keys.len()) + .map(|i| new_local_keys[i].keys_linear.x_i.clone()) + .collect(); + let indices: Vec<_> = (0..(t + 1)).collect(); + let vss = VerifiableSS:: { + parameters: ShamirSecretSharing { threshold: t, share_count: n }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random(), + ), + }; + + assert_eq!( + vss.reconstruct( + &indices[..], + &old_linear_secret_key[0..(t + 1) as usize] + ), + vss.reconstruct( + &indices[..], + &new_linear_secret_key[0..(t + 1) as usize] + ) + ); + assert_ne!(old_linear_secret_key, new_linear_secret_key); + } + + // Refresh Keys: Some Existing Parties Leave, New Parties Replace Them + pub fn simulate_dkr_with_replace_parties( + old_local_keys: Vec>, + new_party_indices: Vec, + old_to_new_map: HashMap, + t: u16, + n: u16, + ) -> Vec> { + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + for old_local_key in old_local_keys { + if !new_party_indices.contains(&old_local_key.i) { + simulation.add_party( + KeyRefresh::new( + Some(old_local_key.clone()), + None, + &old_to_new_map, + old_local_key.clone().t, + n, + None, + ) + .unwrap(), + ); + } else { + } + } + + for index in new_party_indices { + simulation.add_party( + KeyRefresh::new( + None, + Some(index), + &old_to_new_map, + t, + n, + Some(t), + ) + .unwrap(), + ); + } + simulation.run().unwrap() + } + + #[test] + pub fn test_dkr_with_replace_parties() { + let t = 2; + let n = 5; + let local_keys = simulate_keygen(t, n); + let old_local_keys = local_keys.clone(); + let mut old_to_new_map = HashMap::new(); + old_to_new_map.insert(1, 3); + old_to_new_map.insert(3, 1); + old_to_new_map.insert(4, 5); + old_to_new_map.insert(5, 4); + let mut new_local_keys = simulate_dkr_with_replace_parties( + local_keys, + vec![2, 6], + old_to_new_map, + t, + n + 1, + ); + new_local_keys.sort_by(|a, b| a.i.cmp(&b.i)); + let old_linear_secret_key: Vec<_> = (0..old_local_keys.len()) + .map(|i| old_local_keys[i].keys_linear.x_i.clone()) + .collect(); + + let new_linear_secret_key: Vec<_> = (0..new_local_keys.len()) + .map(|i| new_local_keys[i].keys_linear.x_i.clone()) + .collect(); + + let old_indices = vec![0, 1, 2]; + let new_indices = vec![0, 1, 2]; + let vss = VerifiableSS:: { + parameters: ShamirSecretSharing { threshold: t, share_count: n }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random(), + ), + }; + + let new_vss = VerifiableSS:: { + parameters: ShamirSecretSharing { + threshold: t, + share_count: n + 1, + }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random(), + ), + }; + + assert_eq!( + vss.reconstruct( + &old_indices[..], + &old_linear_secret_key[0..(t + 1) as usize] + ), + new_vss.reconstruct( + &new_indices[..], + &new_linear_secret_key[0..(t + 1) as usize] + ) + ); + assert_ne!(old_linear_secret_key, new_linear_secret_key); + } + + // Refresh Keys: New Threshold, Only Existing Parties (No New Parties). + pub fn simulate_dkr_with_new_threshold( + old_local_keys: Vec>, + old_to_new_map: &HashMap, + new_t: u16, + ) -> Vec> { + let mut simulation = Simulation::new(); + simulation.enable_benchmarks(false); + + for old_local_key in old_local_keys { + simulation.add_party( + KeyRefresh::new( + Some(old_local_key.clone()), + None, + old_to_new_map, + new_t, + old_local_key.n, + None, + ) + .unwrap(), + ); + } + + simulation.run().unwrap() + } + + #[test] + pub fn test_dkr_with_new_threshold() { + let init_t = 2; // initial threshold. + let new_t = 1; // new/final threshold. + let n = 5; + let local_keys = simulate_keygen(init_t, n); + + let old_local_keys = local_keys.clone(); + let mut old_to_new_map = HashMap::new(); + old_to_new_map.insert(1, 2); + old_to_new_map.insert(2, 1); + old_to_new_map.insert(3, 4); + old_to_new_map.insert(4, 3); + old_to_new_map.insert(5, 5); + let mut new_local_keys = + simulate_dkr_with_new_threshold(local_keys, &old_to_new_map, new_t); + new_local_keys.sort_by(|a, b| a.i.cmp(&b.i)); + + let old_linear_secret_key: Vec<_> = (0..old_local_keys.len()) + .map(|i| old_local_keys[i].keys_linear.x_i.clone()) + .collect(); + let new_linear_secret_key: Vec<_> = (0..new_local_keys.len()) + .map(|i| new_local_keys[i].keys_linear.x_i.clone()) + .collect(); + + let old_indices: Vec<_> = (0..(init_t + 1)).collect(); + let new_indices: Vec<_> = (0..(new_t + 1)).collect(); + + let vss = VerifiableSS:: { + parameters: ShamirSecretSharing { + threshold: init_t, + share_count: n, + }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random(), + ), + }; + let new_vss = VerifiableSS:: { + parameters: ShamirSecretSharing { + threshold: new_t, + share_count: n, + }, + commitments: Vec::new(), + proof: DLogProof::::prove( + &Scalar::random(), + ), + }; + + assert_eq!( + vss.reconstruct( + &old_indices[..], + &old_linear_secret_key[0..(init_t + 1) as usize] + ), + new_vss.reconstruct( + &new_indices[..], + &new_linear_secret_key[0..(new_t + 1) as usize] + ) + ); + assert_ne!(old_linear_secret_key, new_linear_secret_key); + } } diff --git a/src/sign/mod.rs b/src/sign/mod.rs index a74e4938..79bcd48c 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -1,17 +1,17 @@ /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies. + Copyright 2022 by Webb Technologies. - This file is part of cggmp library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of cggmp library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - cggmp-threshold-ecdsa is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + cggmp-threshold-ecdsa is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ use std::collections::HashMap; @@ -21,13 +21,15 @@ use serde::{Deserialize, Serialize}; use sha2::Sha256; use crate::utilities::{ - aff_g::{ - PaillierAffineOpWithGroupComInRangeProof, PaillierAffineOpWithGroupComInRangeStatement, - }, - dec_q::{PaillierDecryptionModQProof, PaillierDecryptionModQStatement}, - mul_star::{ - PaillierMultiplicationVersusGroupProof, PaillierMultiplicationVersusGroupStatement, - }, + aff_g::{ + PaillierAffineOpWithGroupComInRangeProof, + PaillierAffineOpWithGroupComInRangeStatement, + }, + dec_q::{PaillierDecryptionModQProof, PaillierDecryptionModQStatement}, + mul_star::{ + PaillierMultiplicationVersusGroupProof, + PaillierMultiplicationVersusGroupStatement, + }, }; use crate::presign::SSID; @@ -36,27 +38,35 @@ pub mod state_machine; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SigningBroadcastMessage1 { - pub ssid: SSID, - pub i: u16, - pub sigma_i: BigInt, + pub ssid: SSID, + pub i: u16, + pub sigma_i: BigInt, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SigningOutput { - pub ssid: SSID, - pub m: BigInt, - pub r: BigInt, - pub sigma: BigInt, + pub ssid: SSID, + pub m: BigInt, + pub r: BigInt, + pub sigma: BigInt, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SigningIdentifiableAbortMessage { - pub i: u16, - pub proofs_D_hat_j_i: HashMap<(u16, u16), PaillierAffineOpWithGroupComInRangeProof>, - pub statements_D_hat_j_i: - HashMap<(u16, u16), PaillierAffineOpWithGroupComInRangeStatement>, - pub proof_H_hat_i: HashMap>, - pub statement_H_hat_i: HashMap>, - pub proof_sigma_i: HashMap>, - pub statement_sigma_i: HashMap>, + pub i: u16, + pub proofs_D_hat_j_i: HashMap< + (u16, u16), + PaillierAffineOpWithGroupComInRangeProof, + >, + pub statements_D_hat_j_i: HashMap< + (u16, u16), + PaillierAffineOpWithGroupComInRangeStatement, + >, + pub proof_H_hat_i: + HashMap>, + pub statement_H_hat_i: + HashMap>, + pub proof_sigma_i: HashMap>, + pub statement_sigma_i: + HashMap>, } diff --git a/src/sign/rounds.rs b/src/sign/rounds.rs index b6a43f82..4723d4e3 100644 --- a/src/sign/rounds.rs +++ b/src/sign/rounds.rs @@ -1,204 +1,232 @@ /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies. + Copyright 2022 by Webb Technologies. - This file is part of cggmp library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of cggmp library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - cggmp-threshold-ecdsa is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + cggmp-threshold-ecdsa is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ use std::{collections::HashMap, marker::PhantomData}; use curv::{ - arithmetic::{traits::*, Modulo}, - elliptic::curves::{Point, Scalar, Secp256k1}, - BigInt, + arithmetic::{traits::*, Modulo}, + elliptic::curves::{Point, Scalar, Secp256k1}, + BigInt, }; use round_based::{ - containers::{push::Push, BroadcastMsgs, BroadcastMsgsStore}, - Msg, + containers::{push::Push, BroadcastMsgs, BroadcastMsgsStore}, + Msg, }; use sha2::Sha256; use paillier::*; use crate::{ - presign::{PresigningOutput, PresigningTranscript, DEFAULT_ENCRYPTION_KEY}, - utilities::{ - aff_g::{ - PaillierAffineOpWithGroupComInRangeProof, PaillierAffineOpWithGroupComInRangeStatement, - PaillierAffineOpWithGroupComInRangeWitness, - }, - dec_q::{ - PaillierDecryptionModQProof, PaillierDecryptionModQStatement, - PaillierDecryptionModQWitness, - }, - mul_star::{ - PaillierMultiplicationVersusGroupProof, PaillierMultiplicationVersusGroupStatement, - PaillierMultiplicationVersusGroupWitness, - }, - sample_relatively_prime_integer, - }, - ErrorType, NoOfflineStageErrorData, ProofVerificationErrorData, + presign::{PresigningOutput, PresigningTranscript, DEFAULT_ENCRYPTION_KEY}, + utilities::{ + aff_g::{ + PaillierAffineOpWithGroupComInRangeProof, + PaillierAffineOpWithGroupComInRangeStatement, + PaillierAffineOpWithGroupComInRangeWitness, + }, + dec_q::{ + PaillierDecryptionModQProof, PaillierDecryptionModQStatement, + PaillierDecryptionModQWitness, + }, + mul_star::{ + PaillierMultiplicationVersusGroupProof, + PaillierMultiplicationVersusGroupStatement, + PaillierMultiplicationVersusGroupWitness, + }, + sample_relatively_prime_integer, + }, + ErrorType, NoOfflineStageErrorData, ProofVerificationErrorData, }; use thiserror::Error; use zeroize::Zeroize; -use super::{SigningBroadcastMessage1, SigningIdentifiableAbortMessage, SigningOutput, SSID}; +use super::{ + SigningBroadcastMessage1, SigningIdentifiableAbortMessage, SigningOutput, + SSID, +}; use super::state_machine::{Round0Messages, Round1Messages}; pub struct Round0 { - pub ssid: SSID, - pub l: usize, // This is the number of presignings to run in parallel - pub m: BigInt, - pub presigning_data: - HashMap, PresigningTranscript)>, + pub ssid: SSID, + pub l: usize, // This is the number of presignings to run in parallel + pub m: BigInt, + pub presigning_data: HashMap< + u16, + (PresigningOutput, PresigningTranscript), + >, } impl Round0 { - pub fn proceed(mut self, mut output: O) -> Result - where - O: Push>>>, - { - // If there is a record for (l,...) - if let Some((presigning_output, presigning_transcript)) = - self.presigning_data.get_mut(&(self.l as u16)) - { - // r = R projected onto x axis - let r = presigning_output.R.x_coord().unwrap_or_else(BigInt::zero); - // sigma_i = k*m + r*chi - let sigma_i = presigning_output - .k_i - .mul(&self.m) - .add(&r.mul(&presigning_output.chi_i)) - .mod_floor(&self.ssid.q); - let body = SigningBroadcastMessage1 { - ssid: self.ssid.clone(), - i: self.ssid.X.i, - sigma_i: sigma_i.clone(), - }; - output.push(Msg { sender: self.ssid.X.i, receiver: None, body: Box::new(body) }); - // Erase output from memory - presigning_output.zeroize(); - Ok(Round1 { - ssid: self.ssid.clone(), - i: self.ssid.X.i, - presigning_transcript: presigning_transcript.clone(), - m: self.m, - r, - sigma_i, - }) - } else { - let error_data = NoOfflineStageErrorData { l: self.l }; - Err(SignError::NoOfflineStageError(ErrorType { - error_type: "mul".to_string(), - bad_actors: vec![], - data: bincode::serialize(&error_data).unwrap(), - })) - } - } - pub fn is_expensive(&self) -> bool { - false - } + pub fn proceed(mut self, mut output: O) -> Result + where + O: Push>>>, + { + // If there is a record for (l,...) + if let Some((presigning_output, presigning_transcript)) = + self.presigning_data.get_mut(&(self.l as u16)) + { + // r = R projected onto x axis + let r = presigning_output.R.x_coord().unwrap_or_else(BigInt::zero); + // sigma_i = k*m + r*chi + let sigma_i = presigning_output + .k_i + .mul(&self.m) + .add(&r.mul(&presigning_output.chi_i)) + .mod_floor(&self.ssid.q); + let body = SigningBroadcastMessage1 { + ssid: self.ssid.clone(), + i: self.ssid.X.i, + sigma_i: sigma_i.clone(), + }; + output.push(Msg { + sender: self.ssid.X.i, + receiver: None, + body: Box::new(body), + }); + // Erase output from memory + presigning_output.zeroize(); + Ok(Round1 { + ssid: self.ssid.clone(), + i: self.ssid.X.i, + presigning_transcript: presigning_transcript.clone(), + m: self.m, + r, + sigma_i, + }) + } else { + let error_data = NoOfflineStageErrorData { l: self.l }; + Err(SignError::NoOfflineStageError(ErrorType { + error_type: "mul".to_string(), + bad_actors: vec![], + data: bincode::serialize(&error_data).unwrap(), + })) + } + } + pub fn is_expensive(&self) -> bool { + false + } } pub struct Round1 { - pub ssid: SSID, - pub i: u16, - pub m: BigInt, - pub r: BigInt, - pub sigma_i: BigInt, - pub presigning_transcript: PresigningTranscript, + pub ssid: SSID, + pub i: u16, + pub m: BigInt, + pub r: BigInt, + pub sigma_i: BigInt, + pub presigning_transcript: PresigningTranscript, } impl Round1 { - pub fn proceed( - self, - input: BroadcastMsgs>, - mut output: O, - ) -> Result - where - O: Push>>>>, - { - // Mapping from j to sigma_j - let mut sigmas: HashMap = HashMap::new(); - for msg in input.into_vec() { - sigmas.insert(msg.i, msg.sigma_i); - } - let sigma: BigInt = sigmas - .values() - .into_iter() - .fold(self.sigma_i.clone(), |acc, x| acc.add(x)) - .mod_floor(&self.ssid.q); - - // Verify (r, sigma) is a valid signature - // sigma^{-1} - let sigma_inv = BigInt::mod_inv(&sigma, &self.ssid.q).unwrap(); - // m*sigma^{-1} - let m_sigma_inv = self.m.mul(&sigma_inv); - // r*sigma^{-1} - let r_sigma_inv = self.r.mul(&sigma_inv); - let g = Point::::generator(); - let X = self.ssid.X.public_key(); - let x_projection = ((g * Scalar::from_bigint(&m_sigma_inv)) + - (X * Scalar::from_bigint(&r_sigma_inv))) - .x_coord() - .unwrap_or_else(BigInt::zero); - - if self.r == x_projection { - let signing_output = - SigningOutput { ssid: self.ssid.clone(), m: self.m, r: self.r, sigma }; - output.push(Msg { sender: self.ssid.X.i, receiver: None, body: Box::new(None) }); - Ok(Round2 { ssid: self.ssid, output: Some(signing_output) }) - } else { - // (l,j) to proof for D_j_i - let mut proofs_D_hat_j_i: HashMap< - (u16, u16), - PaillierAffineOpWithGroupComInRangeProof, - > = HashMap::new(); - - // (l,j) to statement for D_j_i - let mut statements_D_hat_j_i: HashMap< - (u16, u16), - PaillierAffineOpWithGroupComInRangeStatement, - > = HashMap::new(); - - self.ssid.P.iter().zip(self.ssid.P.iter()).for_each(|(j, l)| { - if *j != self.ssid.X.i && j != l { - let D_hat_j_i = - self.presigning_transcript.D_hat_j.get(&self.ssid.X.i).unwrap().clone(); - - // F_hat_j_i = enc_i(beta_hat_i_j, r_hat_i_j) - let F_hat_j_i = - self.presigning_transcript.F_hat_j.get(&self.ssid.X.i).unwrap().clone(); - - let witness_D_hat_j_i = PaillierAffineOpWithGroupComInRangeWitness::new( - self.presigning_transcript.secrets.x_i.clone(), - self.presigning_transcript - .beta_hat_i - .get(j) - .unwrap_or(&BigInt::zero()) - .clone(), - self.presigning_transcript - .s_hat_i - .get(j) - .unwrap_or(&BigInt::zero()) - .clone(), - self.presigning_transcript - .r_hat_i - .get(j) - .unwrap_or(&BigInt::zero()) - .clone(), - ); - let statement_D_hat_j_i = + pub fn proceed( + self, + input: BroadcastMsgs>, + mut output: O, + ) -> Result + where + O: Push>>>>, + { + // Mapping from j to sigma_j + let mut sigmas: HashMap = HashMap::new(); + for msg in input.into_vec() { + sigmas.insert(msg.i, msg.sigma_i); + } + let sigma: BigInt = sigmas + .values() + .into_iter() + .fold(self.sigma_i.clone(), |acc, x| acc.add(x)) + .mod_floor(&self.ssid.q); + + // Verify (r, sigma) is a valid signature + // sigma^{-1} + let sigma_inv = BigInt::mod_inv(&sigma, &self.ssid.q).unwrap(); + // m*sigma^{-1} + let m_sigma_inv = self.m.mul(&sigma_inv); + // r*sigma^{-1} + let r_sigma_inv = self.r.mul(&sigma_inv); + let g = Point::::generator(); + let X = self.ssid.X.public_key(); + let x_projection = ((g * Scalar::from_bigint(&m_sigma_inv)) + + (X * Scalar::from_bigint(&r_sigma_inv))) + .x_coord() + .unwrap_or_else(BigInt::zero); + + if self.r == x_projection { + let signing_output = SigningOutput { + ssid: self.ssid.clone(), + m: self.m, + r: self.r, + sigma, + }; + output.push(Msg { + sender: self.ssid.X.i, + receiver: None, + body: Box::new(None), + }); + Ok(Round2 { ssid: self.ssid, output: Some(signing_output) }) + } else { + // (l,j) to proof for D_j_i + let mut proofs_D_hat_j_i: HashMap< + (u16, u16), + PaillierAffineOpWithGroupComInRangeProof, + > = HashMap::new(); + + // (l,j) to statement for D_j_i + let mut statements_D_hat_j_i: HashMap< + (u16, u16), + PaillierAffineOpWithGroupComInRangeStatement, + > = HashMap::new(); + + self.ssid.P.iter().zip(self.ssid.P.iter()).for_each(|(j, l)| { + if *j != self.ssid.X.i && j != l { + let D_hat_j_i = self + .presigning_transcript + .D_hat_j + .get(&self.ssid.X.i) + .unwrap() + .clone(); + + // F_hat_j_i = enc_i(beta_hat_i_j, r_hat_i_j) + let F_hat_j_i = self + .presigning_transcript + .F_hat_j + .get(&self.ssid.X.i) + .unwrap() + .clone(); + + let witness_D_hat_j_i = + PaillierAffineOpWithGroupComInRangeWitness::new( + self.presigning_transcript.secrets.x_i.clone(), + self.presigning_transcript + .beta_hat_i + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + self.presigning_transcript + .s_hat_i + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + self.presigning_transcript + .r_hat_i + .get(j) + .unwrap_or(&BigInt::zero()) + .clone(), + ); + let statement_D_hat_j_i = crate::utilities::aff_g::PaillierAffineOpWithGroupComInRangeStatement { S: self .presigning_transcript @@ -253,285 +281,359 @@ impl Round1 { .clone(), phantom: PhantomData, }; - let proof_D_hat_j_i = + let proof_D_hat_j_i = crate::utilities::aff_g::PaillierAffineOpWithGroupComInRangeProof::< Secp256k1, Sha256, >::prove(&witness_D_hat_j_i, &statement_D_hat_j_i); - proofs_D_hat_j_i.insert((*l, *j), proof_D_hat_j_i); - statements_D_hat_j_i.insert((*l, *j), statement_D_hat_j_i); - } - }); - - // mul* H_hat_i proof - let H_hat_i_randomness = - sample_relatively_prime_integer(&self.presigning_transcript.secrets.ek.n); - let H_hat_i: BigInt = Paillier::encrypt_with_chosen_randomness( - &self.presigning_transcript.secrets.ek, - RawPlaintext::from( - self.presigning_transcript.k_i.mul(&self.presigning_transcript.secrets.x_i), - ), - &Randomness::from(H_hat_i_randomness.clone()), - ) - .into(); - let witness_H_hat_i = PaillierMultiplicationVersusGroupWitness::new( - self.presigning_transcript.secrets.x_i.clone(), - self.presigning_transcript.rho_i.mul(&H_hat_i_randomness), - ); - - let X_i = Point::::generator() * - Scalar::from_bigint(&self.presigning_transcript.secrets.x_i); - - let mut proof_H_hat_i: HashMap< - u16, - PaillierMultiplicationVersusGroupProof, - > = HashMap::new(); - let mut statement_H_hat_i: HashMap< - u16, - PaillierMultiplicationVersusGroupStatement, - > = HashMap::new(); - - self.ssid.P.iter().for_each(|l| { - if *l != self.ssid.X.i { - let statement_H_hat_l_i = PaillierMultiplicationVersusGroupStatement { - N0: self.presigning_transcript.secrets.ek.n.clone(), - NN0: self.presigning_transcript.secrets.ek.nn.clone(), - C: self.presigning_transcript.K_i.clone(), - D: H_hat_i.clone(), - X: X_i.clone(), - N_hat: self - .presigning_transcript - .N_hats - .get(l) - .unwrap_or(&BigInt::zero()) - .clone(), - s: self.presigning_transcript.S.get(l).unwrap_or(&BigInt::zero()).clone(), - t: self.presigning_transcript.T.get(l).unwrap_or(&BigInt::zero()).clone(), - phantom: PhantomData, - }; - - statement_H_hat_i.insert(*l, statement_H_hat_l_i.clone()); - - proof_H_hat_i.insert( - *l, - PaillierMultiplicationVersusGroupProof::::prove( - &witness_H_hat_i, - &statement_H_hat_l_i, - ), - ); - } - }); - - // dec proof - let s_hat_j_i = BigInt::zero(); - let ciphertext = H_hat_i; - let ciphertext_randomness = H_hat_i_randomness; - self.ssid.P.iter().for_each(|j| { - if *j != self.ssid.X.i { - ciphertext - .mul(self.presigning_transcript.D_hat_i.get(j).unwrap_or(&BigInt::zero())) - .mul(self.presigning_transcript.F_hat_j.get(&self.ssid.X.i).unwrap()); - ciphertext_randomness - .mul(&s_hat_j_i) - .mul(self.presigning_transcript.r_hat_i.get(j).unwrap_or(&BigInt::zero())); - } - }); - - BigInt::mod_pow(&ciphertext, &self.r, &self.presigning_transcript.secrets.ek.nn); - ciphertext.mul(&BigInt::mod_pow( - &self.presigning_transcript.K_i, - &self.m, - &self.presigning_transcript.secrets.ek.nn, - )); - BigInt::mod_pow( - &ciphertext_randomness, - &self.r, - &self.presigning_transcript.secrets.ek.nn, - ); - ciphertext_randomness.mul(&BigInt::mod_pow( - &self.presigning_transcript.K_i, - &self.m, - &self.presigning_transcript.secrets.ek.nn, - )); - - let witness_sigma_i = PaillierDecryptionModQWitness::new( - Paillier::decrypt( - &self.presigning_transcript.secrets.dk, - RawCiphertext::from(ciphertext.clone()), - ) - .into(), - ciphertext_randomness, - ); - - // l to statement - let mut statement_sigma_i: HashMap< - u16, - PaillierDecryptionModQStatement, - > = HashMap::new(); - - // l to proof - let mut proof_sigma_i: HashMap> = - HashMap::new(); - - self.ssid.P.iter().for_each(|l| { - if *l != self.ssid.X.i { - let statement_sigma_l_i = PaillierDecryptionModQStatement { - S: self.presigning_transcript.S.get(l).unwrap_or(&BigInt::zero()).clone(), - T: self.presigning_transcript.T.get(l).unwrap_or(&BigInt::zero()).clone(), - N_hat: self - .presigning_transcript - .N_hats - .get(l) - .unwrap_or(&BigInt::zero()) - .clone(), - N0: self.presigning_transcript.secrets.ek.n.clone(), - NN0: self.presigning_transcript.secrets.ek.nn.clone(), - C: ciphertext.clone(), - x: self.sigma_i.clone(), - ek_prover: self.presigning_transcript.secrets.ek.clone(), - phantom: PhantomData, - }; - - statement_sigma_i.insert(*l, statement_sigma_l_i.clone()); - - proof_sigma_i.insert( - *l, - PaillierDecryptionModQProof::::prove( - &witness_sigma_i, - &statement_sigma_l_i, - ), - ); - } - }); - - let body = Some(SigningIdentifiableAbortMessage { - i: self.ssid.X.i, - proofs_D_hat_j_i, - statements_D_hat_j_i, - proof_H_hat_i, - statement_H_hat_i, - proof_sigma_i, - statement_sigma_i, - }); - output.push(Msg { sender: self.ssid.X.i, receiver: None, body: Box::new(body) }); - Ok(Round2 { ssid: self.ssid, output: None }) - } - } - - pub fn is_expensive(&self) -> bool { - false - } - - pub fn expects_messages(i: u16, n: u16) -> Round0Messages { - BroadcastMsgsStore::new(i, n) - } + proofs_D_hat_j_i.insert((*l, *j), proof_D_hat_j_i); + statements_D_hat_j_i.insert((*l, *j), statement_D_hat_j_i); + } + }); + + // mul* H_hat_i proof + let H_hat_i_randomness = sample_relatively_prime_integer( + &self.presigning_transcript.secrets.ek.n, + ); + let H_hat_i: BigInt = Paillier::encrypt_with_chosen_randomness( + &self.presigning_transcript.secrets.ek, + RawPlaintext::from( + self.presigning_transcript + .k_i + .mul(&self.presigning_transcript.secrets.x_i), + ), + &Randomness::from(H_hat_i_randomness.clone()), + ) + .into(); + let witness_H_hat_i = PaillierMultiplicationVersusGroupWitness::new( + self.presigning_transcript.secrets.x_i.clone(), + self.presigning_transcript.rho_i.mul(&H_hat_i_randomness), + ); + + let X_i = Point::::generator() * + Scalar::from_bigint(&self.presigning_transcript.secrets.x_i); + + let mut proof_H_hat_i: HashMap< + u16, + PaillierMultiplicationVersusGroupProof, + > = HashMap::new(); + let mut statement_H_hat_i: HashMap< + u16, + PaillierMultiplicationVersusGroupStatement, + > = HashMap::new(); + + self.ssid.P.iter().for_each(|l| { + if *l != self.ssid.X.i { + let statement_H_hat_l_i = + PaillierMultiplicationVersusGroupStatement { + N0: self.presigning_transcript.secrets.ek.n.clone(), + NN0: self + .presigning_transcript + .secrets + .ek + .nn + .clone(), + C: self.presigning_transcript.K_i.clone(), + D: H_hat_i.clone(), + X: X_i.clone(), + N_hat: self + .presigning_transcript + .N_hats + .get(l) + .unwrap_or(&BigInt::zero()) + .clone(), + s: self + .presigning_transcript + .S + .get(l) + .unwrap_or(&BigInt::zero()) + .clone(), + t: self + .presigning_transcript + .T + .get(l) + .unwrap_or(&BigInt::zero()) + .clone(), + phantom: PhantomData, + }; + + statement_H_hat_i.insert(*l, statement_H_hat_l_i.clone()); + + proof_H_hat_i.insert( + *l, + PaillierMultiplicationVersusGroupProof::< + Secp256k1, + Sha256, + >::prove( + &witness_H_hat_i, &statement_H_hat_l_i + ), + ); + } + }); + + // dec proof + let s_hat_j_i = BigInt::zero(); + let ciphertext = H_hat_i; + let ciphertext_randomness = H_hat_i_randomness; + self.ssid.P.iter().for_each(|j| { + if *j != self.ssid.X.i { + ciphertext + .mul( + self.presigning_transcript + .D_hat_i + .get(j) + .unwrap_or(&BigInt::zero()), + ) + .mul( + self.presigning_transcript + .F_hat_j + .get(&self.ssid.X.i) + .unwrap(), + ); + ciphertext_randomness.mul(&s_hat_j_i).mul( + self.presigning_transcript + .r_hat_i + .get(j) + .unwrap_or(&BigInt::zero()), + ); + } + }); + + BigInt::mod_pow( + &ciphertext, + &self.r, + &self.presigning_transcript.secrets.ek.nn, + ); + ciphertext.mul(&BigInt::mod_pow( + &self.presigning_transcript.K_i, + &self.m, + &self.presigning_transcript.secrets.ek.nn, + )); + BigInt::mod_pow( + &ciphertext_randomness, + &self.r, + &self.presigning_transcript.secrets.ek.nn, + ); + ciphertext_randomness.mul(&BigInt::mod_pow( + &self.presigning_transcript.K_i, + &self.m, + &self.presigning_transcript.secrets.ek.nn, + )); + + let witness_sigma_i = PaillierDecryptionModQWitness::new( + Paillier::decrypt( + &self.presigning_transcript.secrets.dk, + RawCiphertext::from(ciphertext.clone()), + ) + .into(), + ciphertext_randomness, + ); + + // l to statement + let mut statement_sigma_i: HashMap< + u16, + PaillierDecryptionModQStatement, + > = HashMap::new(); + + // l to proof + let mut proof_sigma_i: HashMap< + u16, + PaillierDecryptionModQProof, + > = HashMap::new(); + + self.ssid.P.iter().for_each(|l| { + if *l != self.ssid.X.i { + let statement_sigma_l_i = PaillierDecryptionModQStatement { + S: self + .presigning_transcript + .S + .get(l) + .unwrap_or(&BigInt::zero()) + .clone(), + T: self + .presigning_transcript + .T + .get(l) + .unwrap_or(&BigInt::zero()) + .clone(), + N_hat: self + .presigning_transcript + .N_hats + .get(l) + .unwrap_or(&BigInt::zero()) + .clone(), + N0: self.presigning_transcript.secrets.ek.n.clone(), + NN0: self.presigning_transcript.secrets.ek.nn.clone(), + C: ciphertext.clone(), + x: self.sigma_i.clone(), + ek_prover: self + .presigning_transcript + .secrets + .ek + .clone(), + phantom: PhantomData, + }; + + statement_sigma_i.insert(*l, statement_sigma_l_i.clone()); + + proof_sigma_i.insert( + *l, + PaillierDecryptionModQProof::::prove( + &witness_sigma_i, + &statement_sigma_l_i, + ), + ); + } + }); + + let body = Some(SigningIdentifiableAbortMessage { + i: self.ssid.X.i, + proofs_D_hat_j_i, + statements_D_hat_j_i, + proof_H_hat_i, + statement_H_hat_i, + proof_sigma_i, + statement_sigma_i, + }); + output.push(Msg { + sender: self.ssid.X.i, + receiver: None, + body: Box::new(body), + }); + Ok(Round2 { ssid: self.ssid, output: None }) + } + } + + pub fn is_expensive(&self) -> bool { + false + } + + pub fn expects_messages(i: u16, n: u16) -> Round0Messages { + BroadcastMsgsStore::new(i, n) + } } pub struct Round2 { - ssid: SSID, - output: Option>, + ssid: SSID, + output: Option>, } impl Round2 { - pub fn proceed( - self, - input: BroadcastMsgs>>, - ) -> Result>> { - if self.output.is_some() { - Ok(Some(self.output.unwrap())) - } else { - for msg in input.into_vec() { - let msg = msg.unwrap(); - // si stands for sender index - let si = msg.i; - // Check D_hat_i_j proofs - let mut vec_D_hat_si_j_proof_bad_actors: Vec = vec![]; - self.ssid.P.iter().for_each(|j| { - if *j != self.ssid.X.i { - let D_hat_si_j_proof = - msg.proofs_D_hat_j_i.get(&(self.ssid.X.i, *j)).unwrap(); - - let statement_D_hat_si_j = - msg.statements_D_hat_j_i.get(&(self.ssid.X.i, *j)).unwrap(); - - if PaillierAffineOpWithGroupComInRangeProof::::verify( - D_hat_si_j_proof, - statement_D_hat_si_j, - ) - .is_err() - { - vec_D_hat_si_j_proof_bad_actors.push(*j as usize); - } - } - }); - - if !vec_D_hat_si_j_proof_bad_actors.is_empty() { - let error_data = ProofVerificationErrorData { - proof_symbol: "D_hat_si_j".to_string(), - verifying_party: self.ssid.X.i, - }; - return Err(SignError::ProofVerificationError(ErrorType { - error_type: "aff-g".to_string(), - bad_actors: vec_D_hat_si_j_proof_bad_actors, - data: bincode::serialize(&error_data).unwrap(), - })) - } - // Check H_j proofs - let proof_H_hat_si = msg.proof_H_hat_i.get(&self.ssid.X.i).unwrap(); - let statement_H_hat_si = msg.statement_H_hat_i.get(&self.ssid.X.i).unwrap(); - - if PaillierMultiplicationVersusGroupProof::verify( - proof_H_hat_si, - statement_H_hat_si, - ) - .is_err() - { - let error_data = ProofVerificationErrorData { - proof_symbol: "H_hat_si".to_string(), - verifying_party: self.ssid.X.i, - }; - return Err(SignError::ProofVerificationError(ErrorType { - error_type: "mul".to_string(), - bad_actors: vec![si.into()], - data: bincode::serialize(&error_data).unwrap(), - })) - } - // Check delta_si_proof - let proof_sigma_si = msg.proof_sigma_i.get(&self.ssid.X.i).unwrap(); - let statement_sigma_si = msg.statement_sigma_i.get(&self.ssid.X.i).unwrap(); - - if PaillierDecryptionModQProof::verify(proof_sigma_si, statement_sigma_si).is_err() - { - let error_data = ProofVerificationErrorData { - proof_symbol: "sigma_si".to_string(), - verifying_party: self.ssid.X.i, - }; - return Err(SignError::ProofVerificationError(ErrorType { - error_type: "dec-q".to_string(), - bad_actors: vec![si.into()], - data: bincode::serialize(&error_data).unwrap(), - })) - } - } - Ok(None) - } - } - - pub fn is_expensive(&self) -> bool { - false - } - pub fn expects_messages(i: u16, n: u16) -> Round1Messages { - BroadcastMsgsStore::new(i, n) - } + pub fn proceed( + self, + input: BroadcastMsgs< + Option>, + >, + ) -> Result>> { + if self.output.is_some() { + Ok(Some(self.output.unwrap())) + } else { + for msg in input.into_vec() { + let msg = msg.unwrap(); + // si stands for sender index + let si = msg.i; + // Check D_hat_i_j proofs + let mut vec_D_hat_si_j_proof_bad_actors: Vec = vec![]; + self.ssid.P.iter().for_each(|j| { + if *j != self.ssid.X.i { + let D_hat_si_j_proof = msg + .proofs_D_hat_j_i + .get(&(self.ssid.X.i, *j)) + .unwrap(); + + let statement_D_hat_si_j = msg + .statements_D_hat_j_i + .get(&(self.ssid.X.i, *j)) + .unwrap(); + + if PaillierAffineOpWithGroupComInRangeProof::< + Secp256k1, + Sha256, + >::verify( + D_hat_si_j_proof, statement_D_hat_si_j + ) + .is_err() + { + vec_D_hat_si_j_proof_bad_actors.push(*j as usize); + } + } + }); + + if !vec_D_hat_si_j_proof_bad_actors.is_empty() { + let error_data = ProofVerificationErrorData { + proof_symbol: "D_hat_si_j".to_string(), + verifying_party: self.ssid.X.i, + }; + return Err(SignError::ProofVerificationError(ErrorType { + error_type: "aff-g".to_string(), + bad_actors: vec_D_hat_si_j_proof_bad_actors, + data: bincode::serialize(&error_data).unwrap(), + })) + } + // Check H_j proofs + let proof_H_hat_si = + msg.proof_H_hat_i.get(&self.ssid.X.i).unwrap(); + let statement_H_hat_si = + msg.statement_H_hat_i.get(&self.ssid.X.i).unwrap(); + + if PaillierMultiplicationVersusGroupProof::verify( + proof_H_hat_si, + statement_H_hat_si, + ) + .is_err() + { + let error_data = ProofVerificationErrorData { + proof_symbol: "H_hat_si".to_string(), + verifying_party: self.ssid.X.i, + }; + return Err(SignError::ProofVerificationError(ErrorType { + error_type: "mul".to_string(), + bad_actors: vec![si.into()], + data: bincode::serialize(&error_data).unwrap(), + })) + } + // Check delta_si_proof + let proof_sigma_si = + msg.proof_sigma_i.get(&self.ssid.X.i).unwrap(); + let statement_sigma_si = + msg.statement_sigma_i.get(&self.ssid.X.i).unwrap(); + + if PaillierDecryptionModQProof::verify( + proof_sigma_si, + statement_sigma_si, + ) + .is_err() + { + let error_data = ProofVerificationErrorData { + proof_symbol: "sigma_si".to_string(), + verifying_party: self.ssid.X.i, + }; + return Err(SignError::ProofVerificationError(ErrorType { + error_type: "dec-q".to_string(), + bad_actors: vec![si.into()], + data: bincode::serialize(&error_data).unwrap(), + })) + } + } + Ok(None) + } + } + + pub fn is_expensive(&self) -> bool { + false + } + pub fn expects_messages(i: u16, n: u16) -> Round1Messages { + BroadcastMsgsStore::new(i, n) + } } type Result = std::result::Result; #[derive(Error, Debug, Clone)] pub enum SignError { - #[error("Proof Verification Error")] - ProofVerificationError(ErrorType), + #[error("Proof Verification Error")] + ProofVerificationError(ErrorType), - #[error("No Offline Stage Error")] - NoOfflineStageError(ErrorType), + #[error("No Offline Stage Error")] + NoOfflineStageError(ErrorType), } diff --git a/src/sign/state_machine.rs b/src/sign/state_machine.rs index 6a95dcbc..aa413e1c 100644 --- a/src/sign/state_machine.rs +++ b/src/sign/state_machine.rs @@ -1,293 +1,334 @@ /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies. + Copyright 2022 by Webb Technologies. - This file is part of cggmp library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of cggmp library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - cggmp-threshold-ecdsa is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + cggmp-threshold-ecdsa is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ use crate::presign::{PresigningOutput, PresigningTranscript, SSID}; use super::{ - rounds::{Round0, Round1, Round2}, - SigningBroadcastMessage1, SigningIdentifiableAbortMessage, SigningOutput, + rounds::{Round0, Round1, Round2}, + SigningBroadcastMessage1, SigningIdentifiableAbortMessage, SigningOutput, }; use curv::{elliptic::curves::Secp256k1, BigInt}; use private::InternalError; use round_based::{ - containers::{ - push::{Push, PushExt}, - BroadcastMsgs, MessageStore, Store, StoreErr, - }, - IsCritical, Msg, StateMachine, + containers::{ + push::{Push, PushExt}, + BroadcastMsgs, MessageStore, Store, StoreErr, + }, + IsCritical, Msg, StateMachine, }; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fmt, mem::replace, time::Duration}; use thiserror::Error; -// NOTE: This is a hack since in the 1st round we will need to broadcast and send P2P -// messages, but the round-based library doesn't support this. So we will use the -// `P2PMsgs` to send all the data we need peers to receive. -// FIXME: If we re-design `round-based-traits` to support sending 2 types of messages -// in the same round, we can remove this hack. -pub type Round0Messages = Store>>; -pub type Round1Messages = Store>>>; +// NOTE: This is a hack since in the 1st round we will need to broadcast and +// send P2P messages, but the round-based library doesn't support this. So we +// will use the `P2PMsgs` to send all the data we need peers to receive. +// FIXME: If we re-design `round-based-traits` to support sending 2 types of +// messages in the same round, we can remove this hack. +pub type Round0Messages = + Store>>; +pub type Round1Messages = + Store>>>; pub struct Signing { - // Current round - round: R, + // Current round + round: R, - // Messages - round0_msgs: Option, - round1_msgs: Option, + // Messages + round0_msgs: Option, + round1_msgs: Option, - // Message queue - msgs_queue: Vec>, - party_i: u16, - party_n: u16, + // Message queue + msgs_queue: Vec>, + party_i: u16, + party_n: u16, } impl Signing { - pub fn new( - ssid: SSID, - l: usize, // This is the number of presignings to run in parallel - m: BigInt, - presigning_data: HashMap< - u16, - (PresigningOutput, PresigningTranscript), - >, - ) -> Result { - let n = ssid.P.len() as u16; - let i = ssid.X.i; - if n < 2 { - return Err(Error::TooFewParties) - } - - let mut state = Self { - round: R::Round0(Box::new(Round0 { ssid, l, m, presigning_data })), - - round0_msgs: Some(Round1::expects_messages(i, n)), - round1_msgs: Some(Round2::expects_messages(i, n)), - - msgs_queue: vec![], - - party_i: i, - party_n: n, - }; - - state.proceed_round(false)?; - Ok(state) - } - - fn gmap_queue<'a, T, F>(&'a mut self, mut f: F) -> impl Push> + 'a - where - F: FnMut(T) -> M + 'a, - { - (&mut self.msgs_queue).gmap(move |m: Msg| m.map_body(|m| ProtocolMessage(f(m)))) - } - - /// Proceeds round state if it received enough messages and if it's cheap to compute or - /// `may_block == true` - fn proceed_round(&mut self, may_block: bool) -> Result<()> { - let store1_wants_more = self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - - let next_state: R; - - let try_again: bool = match replace(&mut self.round, R::Gone) { - R::Round0(round) if !round.is_expensive() || may_block => { - next_state = round - .proceed(self.gmap_queue(M::Round1)) - .map(|msg| R::Round1(Box::new(msg))) - .map_err(|_e| Error::ProceedRound { msg_round: 0 })?; - true - }, - s @ R::Round0(_) => { - next_state = s; - false - }, - R::Round1(round) if !store1_wants_more && (!round.is_expensive() || may_block) => { - let store = self.round0_msgs.take().ok_or(InternalError::StoreGone)?; - let msgs = store.finish().map_err(InternalError::RetrieveRoundMessages)?; - next_state = round - .proceed(msgs, self.gmap_queue(M::Round2)) - .map(|msg| R::Round2(Box::new(msg))) - .map_err(|_e| Error::ProceedRound { msg_round: 1 })?; - true - }, - s @ R::Round1(_) => { - next_state = s; - false - }, - R::Round2(round) if !store2_wants_more && (!round.is_expensive() || may_block) => { - let store = self.round1_msgs.take().ok_or(InternalError::StoreGone)?; - let msgs = store.finish().map_err(InternalError::RetrieveRoundMessages)?; - next_state = round - .proceed(msgs) - .map(|msg| R::Final(Box::new(msg))) - .map_err(|_e| Error::ProceedRound { msg_round: 2 })?; - true - }, - s @ R::Round2(_) => { - next_state = s; - false - }, - - s @ R::Final(_) | s @ R::Gone => { - next_state = s; - false - }, - }; - self.round = next_state; - if try_again { - self.proceed_round(may_block) - } else { - Ok(()) - } - } + pub fn new( + ssid: SSID, + l: usize, // This is the number of presignings to run in parallel + m: BigInt, + presigning_data: HashMap< + u16, + (PresigningOutput, PresigningTranscript), + >, + ) -> Result { + let n = ssid.P.len() as u16; + let i = ssid.X.i; + if n < 2 { + return Err(Error::TooFewParties) + } + + let mut state = Self { + round: R::Round0(Box::new(Round0 { ssid, l, m, presigning_data })), + + round0_msgs: Some(Round1::expects_messages(i, n)), + round1_msgs: Some(Round2::expects_messages(i, n)), + + msgs_queue: vec![], + + party_i: i, + party_n: n, + }; + + state.proceed_round(false)?; + Ok(state) + } + + fn gmap_queue<'a, T, F>(&'a mut self, mut f: F) -> impl Push> + 'a + where + F: FnMut(T) -> M + 'a, + { + (&mut self.msgs_queue) + .gmap(move |m: Msg| m.map_body(|m| ProtocolMessage(f(m)))) + } + + /// Proceeds round state if it received enough messages and if it's cheap to + /// compute or `may_block == true` + fn proceed_round(&mut self, may_block: bool) -> Result<()> { + let store1_wants_more = + self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = + self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + + let next_state: R; + + let try_again: bool = match replace(&mut self.round, R::Gone) { + R::Round0(round) if !round.is_expensive() || may_block => { + next_state = round + .proceed(self.gmap_queue(M::Round1)) + .map(|msg| R::Round1(Box::new(msg))) + .map_err(|_e| Error::ProceedRound { msg_round: 0 })?; + true + }, + s @ R::Round0(_) => { + next_state = s; + false + }, + R::Round1(round) + if !store1_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.round0_msgs.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs, self.gmap_queue(M::Round2)) + .map(|msg| R::Round2(Box::new(msg))) + .map_err(|_e| Error::ProceedRound { msg_round: 1 })?; + true + }, + s @ R::Round1(_) => { + next_state = s; + false + }, + R::Round2(round) + if !store2_wants_more && + (!round.is_expensive() || may_block) => + { + let store = + self.round1_msgs.take().ok_or(InternalError::StoreGone)?; + let msgs = store + .finish() + .map_err(InternalError::RetrieveRoundMessages)?; + next_state = round + .proceed(msgs) + .map(|msg| R::Final(Box::new(msg))) + .map_err(|_e| Error::ProceedRound { msg_round: 2 })?; + true + }, + s @ R::Round2(_) => { + next_state = s; + false + }, + + s @ R::Final(_) | s @ R::Gone => { + next_state = s; + false + }, + }; + self.round = next_state; + if try_again { + self.proceed_round(may_block) + } else { + Ok(()) + } + } } impl StateMachine for Signing { - type MessageBody = ProtocolMessage; - type Err = Error; - type Output = Option>; - - fn handle_incoming(&mut self, msg: Msg) -> Result<()> { - let current_round = self.current_round(); - - match msg.body { - ProtocolMessage(M::Round1(m)) => { - let store = self - .round0_msgs - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { current_round, msg_round: 1 })?; - store - .push_msg(Msg { sender: msg.sender, receiver: msg.receiver, body: *m }) - .map_err(Error::HandleMessage)?; - self.proceed_round(false) - }, - ProtocolMessage(M::Round2(m)) => { - let store = self - .round1_msgs - .as_mut() - .ok_or(Error::ReceivedOutOfOrderMessage { current_round, msg_round: 2 })?; - store - .push_msg(Msg { sender: msg.sender, receiver: msg.receiver, body: *m }) - .map_err(Error::HandleMessage)?; - self.proceed_round(false) - }, - } - } - - fn message_queue(&mut self) -> &mut Vec> { - &mut self.msgs_queue - } - - fn wants_to_proceed(&self) -> bool { - let store1_wants_more = self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - let store2_wants_more = self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); - - match &self.round { - R::Round0(_) => true, - R::Round1(_) => !store1_wants_more, - R::Round2(_) => !store2_wants_more, - R::Final(_) | R::Gone => false, - } - } - - fn proceed(&mut self) -> Result<()> { - self.proceed_round(true) - } - - fn round_timeout(&self) -> Option { - None - } - - fn round_timeout_reached(&mut self) -> Self::Err { - panic!("no timeout was set") - } - - fn is_finished(&self) -> bool { - matches!(self.round, R::Final(_)) - } - - fn pick_output(&mut self) -> Option> { - match self.round { - R::Final(_) => (), - R::Gone => return Some(Err(Error::DoublePickOutput)), - _ => return None, - } - - match replace(&mut self.round, R::Gone) { - R::Final(result) => Some(Ok(*result)), - _ => unreachable!("guaranteed by match expression above"), - } - } - - fn current_round(&self) -> u16 { - match &self.round { - R::Round0(_) => 0, - R::Round1(_) => 1, - R::Round2(_) => 2, - R::Final(_) | R::Gone => 3, - } - } - - fn total_rounds(&self) -> Option { - Some(2) - } - - fn party_ind(&self) -> u16 { - self.party_i - } - - fn parties(&self) -> u16 { - self.party_n - } + type MessageBody = ProtocolMessage; + type Err = Error; + type Output = Option>; + + fn handle_incoming(&mut self, msg: Msg) -> Result<()> { + let current_round = self.current_round(); + + match msg.body { + ProtocolMessage(M::Round1(m)) => { + let store = self.round0_msgs.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 1, + }, + )?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: *m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + }, + ProtocolMessage(M::Round2(m)) => { + let store = self.round1_msgs.as_mut().ok_or( + Error::ReceivedOutOfOrderMessage { + current_round, + msg_round: 2, + }, + )?; + store + .push_msg(Msg { + sender: msg.sender, + receiver: msg.receiver, + body: *m, + }) + .map_err(Error::HandleMessage)?; + self.proceed_round(false) + }, + } + } + + fn message_queue(&mut self) -> &mut Vec> { + &mut self.msgs_queue + } + + fn wants_to_proceed(&self) -> bool { + let store1_wants_more = + self.round0_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + let store2_wants_more = + self.round1_msgs.as_ref().map(|s| s.wants_more()).unwrap_or(false); + + match &self.round { + R::Round0(_) => true, + R::Round1(_) => !store1_wants_more, + R::Round2(_) => !store2_wants_more, + R::Final(_) | R::Gone => false, + } + } + + fn proceed(&mut self) -> Result<()> { + self.proceed_round(true) + } + + fn round_timeout(&self) -> Option { + None + } + + fn round_timeout_reached(&mut self) -> Self::Err { + panic!("no timeout was set") + } + + fn is_finished(&self) -> bool { + matches!(self.round, R::Final(_)) + } + + fn pick_output(&mut self) -> Option> { + match self.round { + R::Final(_) => (), + R::Gone => return Some(Err(Error::DoublePickOutput)), + _ => return None, + } + + match replace(&mut self.round, R::Gone) { + R::Final(result) => Some(Ok(*result)), + _ => unreachable!("guaranteed by match expression above"), + } + } + + fn current_round(&self) -> u16 { + match &self.round { + R::Round0(_) => 0, + R::Round1(_) => 1, + R::Round2(_) => 2, + R::Final(_) | R::Gone => 3, + } + } + + fn total_rounds(&self) -> Option { + Some(2) + } + + fn party_ind(&self) -> u16 { + self.party_i + } + + fn parties(&self) -> u16 { + self.party_n + } } impl crate::traits::RoundBlame for Signing { - fn round_blame(&self) -> (u16, Vec) { - let store1_blame = self.round0_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - let store2_blame = self.round1_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); - - let default = (0, vec![]); - match &self.round { - R::Round0(_) => default, - R::Round1(_) => store1_blame, - R::Round2(_) => store2_blame, - R::Final(_) | R::Gone => default, - } - } + fn round_blame(&self) -> (u16, Vec) { + let store1_blame = + self.round0_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); + let store2_blame = + self.round1_msgs.as_ref().map(|s| s.blame()).unwrap_or_default(); + + let default = (0, vec![]); + match &self.round { + R::Round0(_) => default, + R::Round1(_) => store1_blame, + R::Round2(_) => store2_blame, + R::Final(_) | R::Gone => default, + } + } } impl fmt::Debug for Signing { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let current_round = match &self.round { - R::Round0(_) => "0", - R::Round1(_) => "1", - R::Round2(_) => "2", - R::Final(_) => "[Final]", - R::Gone => "[Gone]", - }; - let round0_msgs = match self.round0_msgs.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), - None => "[None]".into(), - }; - let round1_msgs = match self.round1_msgs.as_ref() { - Some(msgs) => format!("[{}/{}]", msgs.messages_received(), msgs.messages_total()), - None => "[None]".into(), - }; - write!( + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let current_round = match &self.round { + R::Round0(_) => "0", + R::Round1(_) => "1", + R::Round2(_) => "2", + R::Final(_) => "[Final]", + R::Gone => "[Gone]", + }; + let round0_msgs = match self.round0_msgs.as_ref() { + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), + None => "[None]".into(), + }; + let round1_msgs = match self.round1_msgs.as_ref() { + Some(msgs) => format!( + "[{}/{}]", + msgs.messages_received(), + msgs.messages_total() + ), + None => "[None]".into(), + }; + write!( f, "{{Key refresh at round={} round0_msgs={} round1_msgs={} queue=[len={}]}}", current_round, @@ -295,30 +336,31 @@ impl fmt::Debug for Signing { round1_msgs, self.msgs_queue.len() ) - } + } } // Rounds enum R { - Round0(Box), - Round1(Box), - Round2(Box), - Final(Box>>), - Gone, + Round0(Box), + Round1(Box), + Round2(Box), + Final(Box>>), + Gone, } // Messages /// Protocol message which parties send on wire /// -/// Hides actual messages structure so it could be changed without breaking semver policy. +/// Hides actual messages structure so it could be changed without breaking +/// semver policy. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProtocolMessage(M); #[derive(Debug, Clone, Serialize, Deserialize)] enum M { - Round1(Box>), - Round2(Box>>), + Round1(Box>), + Round2(Box>>), } // Error @@ -329,151 +371,174 @@ type Result = std::result::Result; #[derive(Debug, Error)] #[non_exhaustive] pub enum Error { - /// Round proceeding resulted in error - #[error("proceed round: {msg_round}")] - ProceedRound { msg_round: usize }, - - /// Too few parties (`n < 2`) - #[error("at least 2 parties are required for keygen")] - TooFewParties, - /// Threshold value `t` is not in range `[1; n-1]` - #[error("threshold is not in range [1; n-1]")] - InvalidThreshold, - /// Party index `i` is not in range `[1; n]` - #[error("party index is not in range [1; n]")] - InvalidPartyIndex, - - /// Received message didn't pass pre-validation - #[error("received message didn't pass pre-validation: {0}")] - HandleMessage(#[source] StoreErr), - /// Received message which we didn't expect to receive now (e.g. message from previous round) - #[error( + /// Round proceeding resulted in error + #[error("proceed round: {msg_round}")] + ProceedRound { msg_round: usize }, + + /// Too few parties (`n < 2`) + #[error("at least 2 parties are required for keygen")] + TooFewParties, + /// Threshold value `t` is not in range `[1; n-1]` + #[error("threshold is not in range [1; n-1]")] + InvalidThreshold, + /// Party index `i` is not in range `[1; n]` + #[error("party index is not in range [1; n]")] + InvalidPartyIndex, + + /// Received message didn't pass pre-validation + #[error("received message didn't pass pre-validation: {0}")] + HandleMessage(#[source] StoreErr), + /// Received message which we didn't expect to receive now (e.g. message + /// from previous round) + #[error( "didn't expect to receive message from round {msg_round} (being at round {current_round})" )] - ReceivedOutOfOrderMessage { current_round: u16, msg_round: u16 }, - /// [Keygen::pick_output] called twice - #[error("pick_output called twice")] - DoublePickOutput, - - /// Some internal assertions were failed, which is a bug - #[doc(hidden)] - #[error("internal error: {0:?}")] - InternalError(InternalError), + ReceivedOutOfOrderMessage { current_round: u16, msg_round: u16 }, + /// [Keygen::pick_output] called twice + #[error("pick_output called twice")] + DoublePickOutput, + + /// Some internal assertions were failed, which is a bug + #[doc(hidden)] + #[error("internal error: {0:?}")] + InternalError(InternalError), } impl IsCritical for Error { - fn is_critical(&self) -> bool { - true - } + fn is_critical(&self) -> bool { + true + } } impl From for Error { - fn from(err: InternalError) -> Self { - Self::InternalError(err) - } + fn from(err: InternalError) -> Self { + Self::InternalError(err) + } } mod private { - #[derive(Debug)] - #[non_exhaustive] - pub enum InternalError { - /// [Messages store](super::MessageStore) reported that it received all messages it wanted - /// to receive, but refused to return message container - RetrieveRoundMessages(super::StoreErr), - #[doc(hidden)] - StoreGone, - } + #[derive(Debug)] + #[non_exhaustive] + pub enum InternalError { + /// [Messages store](super::MessageStore) reported that it received all + /// messages it wanted to receive, but refused to return + /// message container + RetrieveRoundMessages(super::StoreErr), + #[doc(hidden)] + StoreGone, + } } #[cfg(test)] mod test { - use super::*; - use crate::presign::state_machine::test::{ - extract_k, extract_secret_key, generate_parties_and_simulate_presign, - }; - use curv::{ - arithmetic::{Converter, Integer}, - elliptic::curves::{Point, Scalar}, - }; - use round_based::dev::Simulation; - - fn simulate_sign( - inputs: Vec<( - SSID, - HashMap, PresigningTranscript)>, - )>, - m: BigInt, // message digest. - l: usize, // pre-signing index. - ) -> Vec>> { - let mut simulation = Simulation::new(); - - for (ssid, presigning_data) in inputs { - simulation.add_party(Signing::new(ssid, l, m.clone(), presigning_data).unwrap()); - } - - simulation.run().unwrap() - } - - // t = threshold, n = total number of parties, p = number of participants. - // NOTE: Quorum size = t + 1. - pub fn generate_parties_and_simulate_sign(t: u16, n: u16, p: u16) { - // Runs pre-sign simulation for test parameters. - let (keys, ssids, presigning_outputs) = generate_parties_and_simulate_presign(t, n, p); - assert_eq!(keys.len(), n as usize); - assert_eq!(ssids.len(), p as usize); - - // Creates inputs for signing simulation based on test parameters and pre-signing outputs. - let pre_signing_output_idx = 1; // l in the CGGMP20 paper. - // Creates signing parameters. - let inputs: Vec<( - SSID, - HashMap, PresigningTranscript)>, - )> = presigning_outputs - .iter() - .filter_map(|it| { - it.as_ref().map(|(output, transcript)| { - let idx = output.i as usize - 1; - ( - ssids[idx].clone(), - HashMap::from([( - pre_signing_output_idx as u16, - (output.clone(), transcript.clone()), - )]), - ) - }) - }) - .collect(); - // Create SHA256 message digest. - let message = b"Hello, world!"; - use sha2::Digest; - let mut hasher = sha2::Sha256::new(); - hasher.update(message); - let message_digest = BigInt::from_bytes(&hasher.finalize()); - - // Runs signing simulation for test parameters and verifies the output signature. - let results = simulate_sign(inputs, message_digest.clone(), pre_signing_output_idx); - // Extracts signature from results. - let signature = results[0].as_ref().map(|it| (it.r.clone(), it.sigma.clone())).unwrap(); - // Verifies against expected signature. - let q = Scalar::::group_order(); - let sec_key = extract_secret_key(&keys); - let k = extract_k(&presigning_outputs); - let r_direct = (Point::::generator() * k.invert().unwrap()).x_coord().unwrap(); - let s_direct = - (k.to_bigint() * (message_digest + (&r_direct * &sec_key.to_bigint()))).mod_floor(q); - let expected_signature = (r_direct, s_direct); - assert_eq!(signature, expected_signature); - } - - // All parties (2/2 signing). - #[test] - fn sign_all_parties_works() { - generate_parties_and_simulate_sign(1, 2, 2); - } - - // Threshold signing (subset of parties) - (3/4 signing). - #[test] - fn sign_threshold_works() { - generate_parties_and_simulate_sign(2, 4, 3); - } + use super::*; + use crate::presign::state_machine::test::{ + extract_k, extract_secret_key, generate_parties_and_simulate_presign, + }; + use curv::{ + arithmetic::{Converter, Integer}, + elliptic::curves::{Point, Scalar}, + }; + use round_based::dev::Simulation; + + fn simulate_sign( + inputs: Vec<( + SSID, + HashMap< + u16, + (PresigningOutput, PresigningTranscript), + >, + )>, + m: BigInt, // message digest. + l: usize, // pre-signing index. + ) -> Vec>> { + let mut simulation = Simulation::new(); + + for (ssid, presigning_data) in inputs { + simulation.add_party( + Signing::new(ssid, l, m.clone(), presigning_data).unwrap(), + ); + } + + simulation.run().unwrap() + } + + // t = threshold, n = total number of parties, p = number of participants. + // NOTE: Quorum size = t + 1. + pub fn generate_parties_and_simulate_sign(t: u16, n: u16, p: u16) { + // Runs pre-sign simulation for test parameters. + let (keys, ssids, presigning_outputs) = + generate_parties_and_simulate_presign(t, n, p); + assert_eq!(keys.len(), n as usize); + assert_eq!(ssids.len(), p as usize); + + // Creates inputs for signing simulation based on test parameters and + // pre-signing outputs. + let pre_signing_output_idx = 1; // l in the CGGMP20 paper. + // Creates signing parameters. + let inputs: Vec<( + SSID, + HashMap< + u16, + (PresigningOutput, PresigningTranscript), + >, + )> = presigning_outputs + .iter() + .filter_map(|it| { + it.as_ref().map(|(output, transcript)| { + let idx = output.i as usize - 1; + ( + ssids[idx].clone(), + HashMap::from([( + pre_signing_output_idx as u16, + (output.clone(), transcript.clone()), + )]), + ) + }) + }) + .collect(); + // Create SHA256 message digest. + let message = b"Hello, world!"; + use sha2::Digest; + let mut hasher = sha2::Sha256::new(); + hasher.update(message); + let message_digest = BigInt::from_bytes(&hasher.finalize()); + + // Runs signing simulation for test parameters and verifies the output + // signature. + let results = simulate_sign( + inputs, + message_digest.clone(), + pre_signing_output_idx, + ); + // Extracts signature from results. + let signature = results[0] + .as_ref() + .map(|it| (it.r.clone(), it.sigma.clone())) + .unwrap(); + // Verifies against expected signature. + let q = Scalar::::group_order(); + let sec_key = extract_secret_key(&keys); + let k = extract_k(&presigning_outputs); + let r_direct = (Point::::generator() * k.invert().unwrap()) + .x_coord() + .unwrap(); + let s_direct = (k.to_bigint() * + (message_digest + (&r_direct * &sec_key.to_bigint()))) + .mod_floor(q); + let expected_signature = (r_direct, s_direct); + assert_eq!(signature, expected_signature); + } + + // All parties (2/2 signing). + #[test] + fn sign_all_parties_works() { + generate_parties_and_simulate_sign(1, 2, 2); + } + + // Threshold signing (subset of parties) - (3/4 signing). + #[test] + fn sign_threshold_works() { + generate_parties_and_simulate_sign(2, 4, 3); + } } diff --git a/src/traits.rs b/src/traits.rs index dcc0e43c..184f5fb3 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,25 +1,25 @@ /* - Multi-party ECDSA + Multi-party ECDSA - Copyright 2022 by Webb Technologies. + Copyright 2022 by Webb Technologies. - This file is part of cggmp library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of cggmp library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - This file is derived/inspired from Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) + This file is derived/inspired from Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) - cggmp-threshold-ecdsa is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + cggmp-threshold-ecdsa is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ pub trait RoundBlame { - /// Retrieves a list of uncorporative parties - /// - /// Returns a numbers of messages yet to recieve and list of parties to send messages for the - /// current round - fn round_blame(&self) -> (u16, Vec); + /// Retrieves a list of uncorporative parties + /// + /// Returns a numbers of messages yet to recieve and list of parties to send + /// messages for the current round + fn round_blame(&self) -> (u16, Vec); } diff --git a/src/utilities/aff_g/mod.rs b/src/utilities/aff_g/mod.rs index a0861650..c689728e 100644 --- a/src/utilities/aff_g/mod.rs +++ b/src/utilities/aff_g/mod.rs @@ -1,468 +1,555 @@ #![allow(non_snake_case)] /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies + Copyright 2022 by Webb Technologies - This file is part of CGGMP Threshold ECDSA library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of CGGMP Threshold ECDSA library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - CGGMP Threshold ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + CGGMP Threshold ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ //! Paillier Affine Operation with Paillier Commitment ZK-Proof – Π^{aff-p} -//! For parameters (G, g, N0, N1), consisting of element g and in group G and Paillier -//! public keys N0, N1, verify the ciphertext C \in Z∗_{N0^2} was obtained as an affine-like -//! transformation on C0 such that the multiplicative coefficient (i.e. ε) is equal to the -//! exponent of X ∈ G in the range I, and the additive coefficient (i.e. δ) is equal to the -//! plaintext-value of Y ∈ Z_N1 and resides in the the range J. +//! For parameters (G, g, N0, N1), consisting of element g and in group G and +//! Paillier public keys N0, N1, verify the ciphertext C \in Z∗_{N0^2} was +//! obtained as an affine-like transformation on C0 such that the multiplicative +//! coefficient (i.e. ε) is equal to the exponent of X ∈ G in the range I, and +//! the additive coefficient (i.e. δ) is equal to the plaintext-value of Y ∈ +//! Z_N1 and resides in the the range J. -//! Setup: Auxiliary Paillier Modulus Nˆ and Ring-Pedersen parameters s, t ∈ Z∗_{Nˆ}. +//! Setup: Auxiliary Paillier Modulus Nˆ and Ring-Pedersen parameters s, t ∈ +//! Z∗_{Nˆ}. //! -//! Inputs: Common input is (G,g,N0,N1,C,D,Y,X) where q = |G| and g is a generator of G. -//! The Prover has secret input (x,y,ρ,ρy) such that +//! Inputs: Common input is (G,g,N0,N1,C,D,Y,X) where q = |G| and g is a +//! generator of G. The Prover has secret input (x,y,ρ,ρy) such that //! x ∈ ± 2l, y ∈ ± 2l′, g^{x} = X, (1 + N1)^{y} · ρ^{N1} = Y mod N^2, //! and //! D = C^{x} · (1+N0)^{y} · ρ^{N0} mod N0^{2}. use super::sample_relatively_prime_integer; use crate::{ - utilities::{mod_pow_with_negative, L, L_PLUS_EPSILON, L_PRIME, L_PRIME_PLUS_EPSILON}, - Error, + utilities::{ + mod_pow_with_negative, L, L_PLUS_EPSILON, L_PRIME, L_PRIME_PLUS_EPSILON, + }, + Error, }; use curv::{ - arithmetic::{traits::*, Modulo}, - cryptographic_primitives::hashing::{Digest, DigestExt}, - elliptic::curves::{Curve, Point, Scalar}, - BigInt, + arithmetic::{traits::*, Modulo}, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{Curve, Point, Scalar}, + BigInt, +}; +use paillier::{ + EncryptWithChosenRandomness, EncryptionKey, Paillier, Randomness, + RawPlaintext, }; -use paillier::{EncryptWithChosenRandomness, EncryptionKey, Paillier, Randomness, RawPlaintext}; use rand::Rng; use rand_chacha::{rand_core::SeedableRng, ChaChaRng}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PaillierAffineOpWithGroupComInRangeStatement { - pub S: BigInt, - pub T: BigInt, - pub N_hat: BigInt, - pub N0: BigInt, - pub N1: BigInt, - pub NN0: BigInt, - pub NN1: BigInt, - pub C: BigInt, - pub D: BigInt, - pub Y: BigInt, - pub X: Point, - pub ek_prover: EncryptionKey, - pub ek_verifier: EncryptionKey, - pub phantom: PhantomData<(E, H)>, +pub struct PaillierAffineOpWithGroupComInRangeStatement< + E: Curve, + H: Digest + Clone, +> { + pub S: BigInt, + pub T: BigInt, + pub N_hat: BigInt, + pub N0: BigInt, + pub N1: BigInt, + pub NN0: BigInt, + pub NN1: BigInt, + pub C: BigInt, + pub D: BigInt, + pub Y: BigInt, + pub X: Point, + pub ek_prover: EncryptionKey, + pub ek_verifier: EncryptionKey, + pub phantom: PhantomData<(E, H)>, } -pub struct PaillierAffineOpWithGroupComInRangeWitness { - x: BigInt, - y: BigInt, - rho: BigInt, - rho_y: BigInt, - phantom: PhantomData<(E, H)>, +pub struct PaillierAffineOpWithGroupComInRangeWitness< + E: Curve, + H: Digest + Clone, +> { + x: BigInt, + y: BigInt, + rho: BigInt, + rho_y: BigInt, + phantom: PhantomData<(E, H)>, } -impl PaillierAffineOpWithGroupComInRangeWitness { - pub fn new(x: BigInt, y: BigInt, rho: BigInt, rho_y: BigInt) -> Self { - PaillierAffineOpWithGroupComInRangeWitness { x, y, rho, rho_y, phantom: PhantomData } - } +impl + PaillierAffineOpWithGroupComInRangeWitness +{ + pub fn new(x: BigInt, y: BigInt, rho: BigInt, rho_y: BigInt) -> Self { + PaillierAffineOpWithGroupComInRangeWitness { + x, + y, + rho, + rho_y, + phantom: PhantomData, + } + } } -impl PaillierAffineOpWithGroupComInRangeStatement { - #[allow(clippy::too_many_arguments)] - pub fn generate( - S: BigInt, - T: BigInt, - N_hat: BigInt, - rho: BigInt, - rho_y: BigInt, - prover: EncryptionKey, - verifier: EncryptionKey, - C: BigInt, - ) -> (Self, PaillierAffineOpWithGroupComInRangeWitness) { - // Set up exponents - let l_exp = BigInt::pow(&BigInt::from(2), L as u32); - let lprime_exp = BigInt::pow(&BigInt::from(2), L_PRIME as u32); - // Set up moduli - let N0 = verifier.clone().n; - let NN0 = verifier.clone().nn; - let N1 = prover.clone().n; - let NN1 = prover.clone().nn; - let ek_verifier = verifier; - let ek_prover = prover; +impl + PaillierAffineOpWithGroupComInRangeStatement +{ + #[allow(clippy::too_many_arguments)] + pub fn generate( + S: BigInt, + T: BigInt, + N_hat: BigInt, + rho: BigInt, + rho_y: BigInt, + prover: EncryptionKey, + verifier: EncryptionKey, + C: BigInt, + ) -> (Self, PaillierAffineOpWithGroupComInRangeWitness) { + // Set up exponents + let l_exp = BigInt::pow(&BigInt::from(2), L as u32); + let lprime_exp = BigInt::pow(&BigInt::from(2), L_PRIME as u32); + // Set up moduli + let N0 = verifier.clone().n; + let NN0 = verifier.clone().nn; + let N1 = prover.clone().n; + let NN1 = prover.clone().nn; + let ek_verifier = verifier; + let ek_prover = prover; - let x = BigInt::sample_range(&BigInt::from(-1).mul(&l_exp), &l_exp); - let y = BigInt::sample_range(&BigInt::from(-1).mul(&lprime_exp), &lprime_exp); + let x = BigInt::sample_range(&BigInt::from(-1).mul(&l_exp), &l_exp); + let y = BigInt::sample_range( + &BigInt::from(-1).mul(&lprime_exp), + &lprime_exp, + ); - let X = Point::::generator().as_point() * Scalar::from(&x); - // Y = (1 + N1)^{y} · ρ_y^{N1} - let Y = Paillier::encrypt_with_chosen_randomness( - &ek_prover, - RawPlaintext::from(&y), - &Randomness::from(&rho_y), - ); - // (1 + N0)^y mod N0^2 - let D = { - let D_temp = Paillier::encrypt_with_chosen_randomness( - &ek_verifier, - RawPlaintext::from(&y), - &Randomness::from(&rho), - ); - BigInt::mod_mul(&mod_pow_with_negative(&C, &x, &NN0), &D_temp.into(), &NN0) - }; + let X = Point::::generator().as_point() * Scalar::from(&x); + // Y = (1 + N1)^{y} · ρ_y^{N1} + let Y = Paillier::encrypt_with_chosen_randomness( + &ek_prover, + RawPlaintext::from(&y), + &Randomness::from(&rho_y), + ); + // (1 + N0)^y mod N0^2 + let D = { + let D_temp = Paillier::encrypt_with_chosen_randomness( + &ek_verifier, + RawPlaintext::from(&y), + &Randomness::from(&rho), + ); + BigInt::mod_mul( + &mod_pow_with_negative(&C, &x, &NN0), + &D_temp.into(), + &NN0, + ) + }; - ( - Self { - S, - T, - N_hat, - N0, - N1, - NN0, - NN1, - C, - D, - Y: Y.clone().into(), - X, - ek_prover, - ek_verifier, - phantom: PhantomData, - }, - PaillierAffineOpWithGroupComInRangeWitness { x, y, rho, rho_y, phantom: PhantomData }, - ) - } + ( + Self { + S, + T, + N_hat, + N0, + N1, + NN0, + NN1, + C, + D, + Y: Y.clone().into(), + X, + ek_prover, + ek_verifier, + phantom: PhantomData, + }, + PaillierAffineOpWithGroupComInRangeWitness { + x, + y, + rho, + rho_y, + phantom: PhantomData, + }, + ) + } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierAffineOpWithGroupComInRangeCommitment { - A: BigInt, - B_x: Point, - B_y: BigInt, - E: BigInt, - F: BigInt, - big_S: BigInt, - big_T: BigInt, + A: BigInt, + B_x: Point, + B_y: BigInt, + E: BigInt, + F: BigInt, + big_S: BigInt, + big_T: BigInt, } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PaillierAffineOpWithGroupComInRangeProof { - z1: BigInt, - z2: BigInt, - z3: BigInt, - z4: BigInt, - w: BigInt, - wy: BigInt, - commitment: PaillierAffineOpWithGroupComInRangeCommitment, - phantom: PhantomData<(E, H)>, +pub struct PaillierAffineOpWithGroupComInRangeProof +{ + z1: BigInt, + z2: BigInt, + z3: BigInt, + z4: BigInt, + w: BigInt, + wy: BigInt, + commitment: PaillierAffineOpWithGroupComInRangeCommitment, + phantom: PhantomData<(E, H)>, } // Link to the UC non-interactive threshold ECDSA paper -impl PaillierAffineOpWithGroupComInRangeProof { - pub fn prove( - witness: &PaillierAffineOpWithGroupComInRangeWitness, - statement: &PaillierAffineOpWithGroupComInRangeStatement, - ) -> PaillierAffineOpWithGroupComInRangeProof { - // Set up exponents - let l_exp = BigInt::pow(&BigInt::from(2), L as u32); - let lplus_exp = BigInt::pow(&BigInt::from(2), L_PLUS_EPSILON as u32); - let lprimeplus_exp = BigInt::pow(&BigInt::from(2), L_PRIME_PLUS_EPSILON as u32); +impl + PaillierAffineOpWithGroupComInRangeProof +{ + pub fn prove( + witness: &PaillierAffineOpWithGroupComInRangeWitness, + statement: &PaillierAffineOpWithGroupComInRangeStatement, + ) -> PaillierAffineOpWithGroupComInRangeProof { + // Set up exponents + let l_exp = BigInt::pow(&BigInt::from(2), L as u32); + let lplus_exp = BigInt::pow(&BigInt::from(2), L_PLUS_EPSILON as u32); + let lprimeplus_exp = + BigInt::pow(&BigInt::from(2), L_PRIME_PLUS_EPSILON as u32); - // α ← ± 2^{l+ε} - let alpha = BigInt::sample_range(&BigInt::from(-1).mul(&lplus_exp), &lplus_exp); - // β ← ± 2^{l'+ε} - let beta = BigInt::sample_range(&BigInt::from(-1).mul(&lprimeplus_exp), &lprimeplus_exp); - // Sample r, ry as unit values from Z_N0, Z_N1 - let r = sample_relatively_prime_integer(&statement.N0); - let ry = sample_relatively_prime_integer(&statement.N1); - // γ ← ± 2^{l+ε} · Nˆ - let gamma = BigInt::sample_range( - &BigInt::from(-1).mul(&lplus_exp).mul(&statement.N_hat), - &lplus_exp.mul(&statement.N_hat), - ); - // m ← ± 2l · Nˆ - let m = BigInt::sample_range( - &BigInt::from(-1).mul(&l_exp).mul(&statement.N_hat), - &l_exp.mul(&statement.N_hat), - ); - // δ ← ± 2^{l+ε} · Nˆ - let delta = BigInt::sample_range( - &BigInt::from(-1).mul(&lplus_exp).mul(&statement.N_hat), - &lplus_exp.mul(&statement.N_hat), - ); - // mu ← ± 2l · Nˆ - let mu = BigInt::sample_range( - &BigInt::from(-1).mul(&l_exp).mul(&statement.N_hat), - &l_exp.mul(&statement.N_hat), - ); - // A = C^α · (1 + N0)^β · r^N0 mod N0^2 - let A = { - let A_temp = Paillier::encrypt_with_chosen_randomness( - &statement.ek_verifier, - RawPlaintext::from(&beta), - &Randomness::from(&r), - ); - BigInt::mod_mul( - &mod_pow_with_negative(&statement.C, &alpha, &statement.NN0), - &A_temp.into(), - &statement.NN0, - ) - }; + // α ← ± 2^{l+ε} + let alpha = + BigInt::sample_range(&BigInt::from(-1).mul(&lplus_exp), &lplus_exp); + // β ← ± 2^{l'+ε} + let beta = BigInt::sample_range( + &BigInt::from(-1).mul(&lprimeplus_exp), + &lprimeplus_exp, + ); + // Sample r, ry as unit values from Z_N0, Z_N1 + let r = sample_relatively_prime_integer(&statement.N0); + let ry = sample_relatively_prime_integer(&statement.N1); + // γ ← ± 2^{l+ε} · Nˆ + let gamma = BigInt::sample_range( + &BigInt::from(-1).mul(&lplus_exp).mul(&statement.N_hat), + &lplus_exp.mul(&statement.N_hat), + ); + // m ← ± 2l · Nˆ + let m = BigInt::sample_range( + &BigInt::from(-1).mul(&l_exp).mul(&statement.N_hat), + &l_exp.mul(&statement.N_hat), + ); + // δ ← ± 2^{l+ε} · Nˆ + let delta = BigInt::sample_range( + &BigInt::from(-1).mul(&lplus_exp).mul(&statement.N_hat), + &lplus_exp.mul(&statement.N_hat), + ); + // mu ← ± 2l · Nˆ + let mu = BigInt::sample_range( + &BigInt::from(-1).mul(&l_exp).mul(&statement.N_hat), + &l_exp.mul(&statement.N_hat), + ); + // A = C^α · (1 + N0)^β · r^N0 mod N0^2 + let A = { + let A_temp = Paillier::encrypt_with_chosen_randomness( + &statement.ek_verifier, + RawPlaintext::from(&beta), + &Randomness::from(&r), + ); + BigInt::mod_mul( + &mod_pow_with_negative(&statement.C, &alpha, &statement.NN0), + &A_temp.into(), + &statement.NN0, + ) + }; - let B_x: Point = Point::::generator().as_point() * Scalar::from_bigint(&alpha); - // By = (1 + N1)^β · ry^N1 mod N1^2 - let B_y = Paillier::encrypt_with_chosen_randomness( - &statement.ek_prover, - RawPlaintext::from(&beta), - &Randomness::from(&ry), - ); - // E = s^α · t^γ mod Nˆ - let E = BigInt::mod_mul( - &mod_pow_with_negative(&statement.S, &alpha, &statement.N_hat), - &mod_pow_with_negative(&statement.T, &gamma, &statement.N_hat), - &statement.N_hat, - ); - // big S = s^x · t^m mod Nˆ - let big_S = BigInt::mod_mul( - &mod_pow_with_negative(&statement.S, &witness.x, &statement.N_hat), - &mod_pow_with_negative(&statement.T, &m, &statement.N_hat), - &statement.N_hat, - ); - // F = s^β · t^δ mod Nˆ - let F = BigInt::mod_mul( - &mod_pow_with_negative(&statement.S, &beta, &statement.N_hat), - &mod_pow_with_negative(&statement.T, &delta, &statement.N_hat), - &statement.N_hat, - ); - // big T = s^y · t^mu mod Nˆ - let big_T = BigInt::mod_mul( - &mod_pow_with_negative(&statement.S, &witness.y, &statement.N_hat), - &mod_pow_with_negative(&statement.T, &mu, &statement.N_hat), - &statement.N_hat, - ); - // Hash all prover messages to generate NIZK challenge - let mut e: BigInt = H::new() - .chain_bigint(&big_S) - .chain_bigint(&big_T) - .chain_bigint(&A) - .chain_point(&B_x) - .chain_bigint(&B_y.clone().into()) - .chain_bigint(&E) - .chain_bigint(&F) - .result_bigint(); - let mut rng: ChaChaRng = ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); - let val = rng.gen_range(0..2); - e = BigInt::from(val).mul(&BigInt::from(-2)).add(&BigInt::one()).mul(&e); - // Compute Fiat-Shamir commitment preimage - let commitment = PaillierAffineOpWithGroupComInRangeCommitment:: { - A, - B_x, - B_y: B_y.clone().into(), - E, - F, - big_S, - big_T, - }; - // z1 = α + ex - let z1 = BigInt::add(&alpha, &e.mul(&witness.x)); - // z2 = β + ey - let z2 = BigInt::add(&beta, &e.mul(&witness.y)); - // z3 = γ + em - let z3 = BigInt::add(&gamma, &e.mul(&m)); - // z4 = δ + (e · mu) - let z4 = BigInt::add(&delta, &e.mul(&mu)); - // w = r · rho^e mod N0 - let w = BigInt::mod_mul( - &r, - &mod_pow_with_negative(&witness.rho, &e, &statement.N0), - &statement.N0, - ); - // wy = ry · rho_y^e mod N1 - let wy = BigInt::mod_mul( - &ry, - &mod_pow_with_negative(&witness.rho_y, &e, &statement.N1), - &statement.N1, - ); + let B_x: Point = + Point::::generator().as_point() * Scalar::from_bigint(&alpha); + // By = (1 + N1)^β · ry^N1 mod N1^2 + let B_y = Paillier::encrypt_with_chosen_randomness( + &statement.ek_prover, + RawPlaintext::from(&beta), + &Randomness::from(&ry), + ); + // E = s^α · t^γ mod Nˆ + let E = BigInt::mod_mul( + &mod_pow_with_negative(&statement.S, &alpha, &statement.N_hat), + &mod_pow_with_negative(&statement.T, &gamma, &statement.N_hat), + &statement.N_hat, + ); + // big S = s^x · t^m mod Nˆ + let big_S = BigInt::mod_mul( + &mod_pow_with_negative(&statement.S, &witness.x, &statement.N_hat), + &mod_pow_with_negative(&statement.T, &m, &statement.N_hat), + &statement.N_hat, + ); + // F = s^β · t^δ mod Nˆ + let F = BigInt::mod_mul( + &mod_pow_with_negative(&statement.S, &beta, &statement.N_hat), + &mod_pow_with_negative(&statement.T, &delta, &statement.N_hat), + &statement.N_hat, + ); + // big T = s^y · t^mu mod Nˆ + let big_T = BigInt::mod_mul( + &mod_pow_with_negative(&statement.S, &witness.y, &statement.N_hat), + &mod_pow_with_negative(&statement.T, &mu, &statement.N_hat), + &statement.N_hat, + ); + // Hash all prover messages to generate NIZK challenge + let mut e: BigInt = H::new() + .chain_bigint(&big_S) + .chain_bigint(&big_T) + .chain_bigint(&A) + .chain_point(&B_x) + .chain_bigint(&B_y.clone().into()) + .chain_bigint(&E) + .chain_bigint(&F) + .result_bigint(); + let mut rng: ChaChaRng = + ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); + let val = rng.gen_range(0..2); + e = BigInt::from(val) + .mul(&BigInt::from(-2)) + .add(&BigInt::one()) + .mul(&e); + // Compute Fiat-Shamir commitment preimage + let commitment = PaillierAffineOpWithGroupComInRangeCommitment:: { + A, + B_x, + B_y: B_y.clone().into(), + E, + F, + big_S, + big_T, + }; + // z1 = α + ex + let z1 = BigInt::add(&alpha, &e.mul(&witness.x)); + // z2 = β + ey + let z2 = BigInt::add(&beta, &e.mul(&witness.y)); + // z3 = γ + em + let z3 = BigInt::add(&gamma, &e.mul(&m)); + // z4 = δ + (e · mu) + let z4 = BigInt::add(&delta, &e.mul(&mu)); + // w = r · rho^e mod N0 + let w = BigInt::mod_mul( + &r, + &mod_pow_with_negative(&witness.rho, &e, &statement.N0), + &statement.N0, + ); + // wy = ry · rho_y^e mod N1 + let wy = BigInt::mod_mul( + &ry, + &mod_pow_with_negative(&witness.rho_y, &e, &statement.N1), + &statement.N1, + ); - Self { z1, z2, z3, z4, w, wy, commitment, phantom: PhantomData } - } + Self { z1, z2, z3, z4, w, wy, commitment, phantom: PhantomData } + } - pub fn verify( - proof: &PaillierAffineOpWithGroupComInRangeProof, - statement: &PaillierAffineOpWithGroupComInRangeStatement, - ) -> Result<(), Error> { - // Hash all prover messages to generate NIZK challenge - let mut e: BigInt = H::new() - .chain_bigint(&proof.commitment.big_S.clone()) - .chain_bigint(&proof.commitment.big_T.clone()) - .chain_bigint(&proof.commitment.A.clone()) - .chain_point(&proof.commitment.B_x.clone()) - .chain_bigint(&proof.commitment.B_y.clone()) - .chain_bigint(&proof.commitment.E.clone()) - .chain_bigint(&proof.commitment.F.clone()) - .result_bigint(); - let mut rng: ChaChaRng = ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); - let val = rng.gen_range(0..2); - e = BigInt::from(val).mul(&BigInt::from(-2)).add(&BigInt::one()).mul(&e); + pub fn verify( + proof: &PaillierAffineOpWithGroupComInRangeProof, + statement: &PaillierAffineOpWithGroupComInRangeStatement, + ) -> Result<(), Error> { + // Hash all prover messages to generate NIZK challenge + let mut e: BigInt = H::new() + .chain_bigint(&proof.commitment.big_S.clone()) + .chain_bigint(&proof.commitment.big_T.clone()) + .chain_bigint(&proof.commitment.A.clone()) + .chain_point(&proof.commitment.B_x.clone()) + .chain_bigint(&proof.commitment.B_y.clone()) + .chain_bigint(&proof.commitment.E.clone()) + .chain_bigint(&proof.commitment.F.clone()) + .result_bigint(); + let mut rng: ChaChaRng = + ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); + let val = rng.gen_range(0..2); + e = BigInt::from(val) + .mul(&BigInt::from(-2)) + .add(&BigInt::one()) + .mul(&e); - /* - RANGE CHECKS - */ - // z1 ∈ [-2^{l+ε}, 2^{l+ε}] - assert!( - proof.z1.bit_length() <= L_PLUS_EPSILON, - "z1 is too large {:?}", - proof.z1.bit_length() - ); - // z2 ∈ [-2^{l'+ε}, 2^{l'+ε}] - assert!( - proof.z2.bit_length() <= L_PRIME_PLUS_EPSILON, - "z2 is too large {:?}", - proof.z2.bit_length() - ); + /* + RANGE CHECKS + */ + // z1 ∈ [-2^{l+ε}, 2^{l+ε}] + assert!( + proof.z1.bit_length() <= L_PLUS_EPSILON, + "z1 is too large {:?}", + proof.z1.bit_length() + ); + // z2 ∈ [-2^{l'+ε}, 2^{l'+ε}] + assert!( + proof.z2.bit_length() <= L_PRIME_PLUS_EPSILON, + "z2 is too large {:?}", + proof.z2.bit_length() + ); - /* - FIRST EQUALITY CHECK - */ - // C^{z1} · (1 + N0)^{z2} · w^{N0} =A · D^e mod N0^2 - let left_1 = { - let temp_left_1_1 = Paillier::encrypt_with_chosen_randomness( - &statement.ek_verifier, - RawPlaintext::from(&proof.z2), - &Randomness::from(&proof.w), - ); - BigInt::mod_mul( - &mod_pow_with_negative(&statement.C, &proof.z1, &statement.NN0), - &temp_left_1_1.into(), - &statement.NN0, - ) - }; - // A · D^e mod N0^2 - let right_1 = BigInt::mod_mul( - &proof.commitment.A, - &mod_pow_with_negative(&statement.D, &e, &statement.NN0), - &statement.NN0, - ); - // Assert left == right - assert!(left_1 == right_1); - /* - SECOND EQUALITY CHECK - */ - // g^{z1} = B_x ·X^e ∈ G - let left_2 = Point::::generator().as_point() * Scalar::from_bigint(&proof.z1); - let right_2 = - proof.commitment.B_x.clone() + (statement.X.clone() * Scalar::from_bigint(&e)); - // Assert left == right - assert!(left_2 == right_2); - /* - THIRD EQUALITY CHECK - */ - // (1 + N1)^{z2} · wy^{N1} = B_y · Y^e mod N1^2 - let left_3_ciphertext = Paillier::encrypt_with_chosen_randomness( - &statement.ek_prover, - RawPlaintext::from(&proof.z2), - &Randomness::from(&proof.wy), - ); - let left_3: BigInt = left_3_ciphertext.into(); - // B_y · Y^e mod N1^2 - let right_3 = BigInt::mod_mul( - &proof.commitment.B_y, - &mod_pow_with_negative(&statement.Y, &e, &statement.NN1), - &statement.NN1, - ); - // Assert left == right - assert!(left_3.mod_floor(&statement.NN1) == right_3); - /* - FOURTH EQUALITY CHECK - */ - // s^{z1} · t^{z3} = E · big_S^e mod N_hat - let left_4 = { - // s^{z1} mod N_hat^2 - let temp_left_4_1 = mod_pow_with_negative(&statement.S, &proof.z1, &statement.N_hat); - // t^{z3} mod N_hat^2 - let temp_left_4_2 = mod_pow_with_negative(&statement.T, &proof.z3, &statement.N_hat); - // s^{z1} · t^{z3} mod N_hat^2 - BigInt::mod_mul(&temp_left_4_1, &temp_left_4_2, &statement.N_hat) - }; - // E · big_S^e mod N_hat^2 - let right_4 = BigInt::mod_mul( - &proof.commitment.E, - &mod_pow_with_negative(&proof.commitment.big_S, &e, &statement.N_hat), - &statement.N_hat, - ); - // Assert left == right - assert!(left_4 == right_4); - /* - FIFTH EQUALITY CHECK - */ - // s^{z2} · t^{z4} = F · big_T^e mod N_hat - let left_5 = { - // s^{z2} mod N_hat^2 - let temp_left_5_1 = mod_pow_with_negative(&statement.S, &proof.z2, &statement.N_hat); - // t^{z4} mod N_hat^2 - let temp_left_5_2 = mod_pow_with_negative(&statement.T, &proof.z4, &statement.N_hat); - // s^{z2} · t^{z4} mod N_hat^2 - BigInt::mod_mul(&temp_left_5_1, &temp_left_5_2, &statement.N_hat) - }; - // F · big_T^e mod N_hat^2 - let right_5 = BigInt::mod_mul( - &proof.commitment.F, - &mod_pow_with_negative(&proof.commitment.big_T, &e, &statement.N_hat), - &statement.N_hat, - ); - // Assert left == right - assert!(left_5 == right_5); - Ok(()) - } + /* + FIRST EQUALITY CHECK + */ + // C^{z1} · (1 + N0)^{z2} · w^{N0} =A · D^e mod N0^2 + let left_1 = { + let temp_left_1_1 = Paillier::encrypt_with_chosen_randomness( + &statement.ek_verifier, + RawPlaintext::from(&proof.z2), + &Randomness::from(&proof.w), + ); + BigInt::mod_mul( + &mod_pow_with_negative(&statement.C, &proof.z1, &statement.NN0), + &temp_left_1_1.into(), + &statement.NN0, + ) + }; + // A · D^e mod N0^2 + let right_1 = BigInt::mod_mul( + &proof.commitment.A, + &mod_pow_with_negative(&statement.D, &e, &statement.NN0), + &statement.NN0, + ); + // Assert left == right + assert!(left_1 == right_1); + /* + SECOND EQUALITY CHECK + */ + // g^{z1} = B_x ·X^e ∈ G + let left_2 = + Point::::generator().as_point() * Scalar::from_bigint(&proof.z1); + let right_2 = proof.commitment.B_x.clone() + + (statement.X.clone() * Scalar::from_bigint(&e)); + // Assert left == right + assert!(left_2 == right_2); + /* + THIRD EQUALITY CHECK + */ + // (1 + N1)^{z2} · wy^{N1} = B_y · Y^e mod N1^2 + let left_3_ciphertext = Paillier::encrypt_with_chosen_randomness( + &statement.ek_prover, + RawPlaintext::from(&proof.z2), + &Randomness::from(&proof.wy), + ); + let left_3: BigInt = left_3_ciphertext.into(); + // B_y · Y^e mod N1^2 + let right_3 = BigInt::mod_mul( + &proof.commitment.B_y, + &mod_pow_with_negative(&statement.Y, &e, &statement.NN1), + &statement.NN1, + ); + // Assert left == right + assert!(left_3.mod_floor(&statement.NN1) == right_3); + /* + FOURTH EQUALITY CHECK + */ + // s^{z1} · t^{z3} = E · big_S^e mod N_hat + let left_4 = { + // s^{z1} mod N_hat^2 + let temp_left_4_1 = mod_pow_with_negative( + &statement.S, + &proof.z1, + &statement.N_hat, + ); + // t^{z3} mod N_hat^2 + let temp_left_4_2 = mod_pow_with_negative( + &statement.T, + &proof.z3, + &statement.N_hat, + ); + // s^{z1} · t^{z3} mod N_hat^2 + BigInt::mod_mul(&temp_left_4_1, &temp_left_4_2, &statement.N_hat) + }; + // E · big_S^e mod N_hat^2 + let right_4 = BigInt::mod_mul( + &proof.commitment.E, + &mod_pow_with_negative( + &proof.commitment.big_S, + &e, + &statement.N_hat, + ), + &statement.N_hat, + ); + // Assert left == right + assert!(left_4 == right_4); + /* + FIFTH EQUALITY CHECK + */ + // s^{z2} · t^{z4} = F · big_T^e mod N_hat + let left_5 = { + // s^{z2} mod N_hat^2 + let temp_left_5_1 = mod_pow_with_negative( + &statement.S, + &proof.z2, + &statement.N_hat, + ); + // t^{z4} mod N_hat^2 + let temp_left_5_2 = mod_pow_with_negative( + &statement.T, + &proof.z4, + &statement.N_hat, + ); + // s^{z2} · t^{z4} mod N_hat^2 + BigInt::mod_mul(&temp_left_5_1, &temp_left_5_2, &statement.N_hat) + }; + // F · big_T^e mod N_hat^2 + let right_5 = BigInt::mod_mul( + &proof.commitment.F, + &mod_pow_with_negative( + &proof.commitment.big_T, + &e, + &statement.N_hat, + ), + &statement.N_hat, + ); + // Assert left == right + assert!(left_5 == right_5); + Ok(()) + } } #[cfg(test)] mod tests { - use super::*; - use crate::utilities::{mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER}; - use curv::elliptic::curves::secp256_k1::Secp256k1; - use fs_dkr::ring_pedersen_proof::RingPedersenStatement; - use paillier::{Encrypt, KeyGeneration, Paillier, RawPlaintext}; - use sha2::Sha256; + use super::*; + use crate::utilities::{ + mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER, + }; + use curv::elliptic::curves::secp256_k1::Secp256k1; + use fs_dkr::ring_pedersen_proof::RingPedersenStatement; + use paillier::{Encrypt, KeyGeneration, Paillier, RawPlaintext}; + use sha2::Sha256; - #[test] - fn test_affine_g_proof() { - let (ring_pedersen_statement, _witness) = - RingPedersenStatement::::generate(); - let (ek_prover, _) = Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); - let (ek_verifier, _) = Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); + #[test] + fn test_affine_g_proof() { + let (ring_pedersen_statement, _witness) = + RingPedersenStatement::::generate(); + let (ek_prover, _) = + Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); + let (ek_verifier, _) = + Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); - let rho: BigInt = BigInt::from_paillier_key(&ek_verifier); - let rho_y: BigInt = BigInt::from_paillier_key(&ek_prover); - let C = Paillier::encrypt(&ek_verifier, RawPlaintext::from(BigInt::from(12))); - let (statement, witness) = - PaillierAffineOpWithGroupComInRangeStatement::::generate( - ring_pedersen_statement.S, - ring_pedersen_statement.T, - ring_pedersen_statement.N, - rho, - rho_y, - ek_prover, - ek_verifier, - C.0.into_owned(), - ); - let proof = PaillierAffineOpWithGroupComInRangeProof::::prove( + let rho: BigInt = BigInt::from_paillier_key(&ek_verifier); + let rho_y: BigInt = BigInt::from_paillier_key(&ek_prover); + let C = Paillier::encrypt( + &ek_verifier, + RawPlaintext::from(BigInt::from(12)), + ); + let (statement, witness) = PaillierAffineOpWithGroupComInRangeStatement::< + Secp256k1, + Sha256, + >::generate( + ring_pedersen_statement.S, + ring_pedersen_statement.T, + ring_pedersen_statement.N, + rho, + rho_y, + ek_prover, + ek_verifier, + C.0.into_owned(), + ); + let proof = PaillierAffineOpWithGroupComInRangeProof::::prove( &witness, &statement, ); - assert!(PaillierAffineOpWithGroupComInRangeProof::::verify( + assert!(PaillierAffineOpWithGroupComInRangeProof::::verify( &proof, &statement ) .is_ok()); - } + } } diff --git a/src/utilities/dec_q/mod.rs b/src/utilities/dec_q/mod.rs index 61a886cf..39bd27bf 100644 --- a/src/utilities/dec_q/mod.rs +++ b/src/utilities/dec_q/mod.rs @@ -1,29 +1,32 @@ #![allow(non_snake_case)] /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies + Copyright 2022 by Webb Technologies - This file is part of CGGMP Threshold ECDSA library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of CGGMP Threshold ECDSA library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - CGGMP Threshold ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + CGGMP Threshold ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ use super::{sample_relatively_prime_integer, L, L_PLUS_EPSILON}; use crate::{utilities::mod_pow_with_negative, Error}; use curv::{ - arithmetic::{traits::*, Modulo}, - cryptographic_primitives::hashing::{Digest, DigestExt}, - elliptic::curves::{Curve, Scalar}, - BigInt, + arithmetic::{traits::*, Modulo}, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{Curve, Scalar}, + BigInt, +}; +use paillier::{ + EncryptWithChosenRandomness, EncryptionKey, Paillier, Randomness, + RawPlaintext, }; -use paillier::{EncryptWithChosenRandomness, EncryptionKey, Paillier, Randomness, RawPlaintext}; use rand::Rng; use rand_chacha::{rand_core::SeedableRng, ChaChaRng}; use serde::{Deserialize, Serialize}; @@ -31,253 +34,288 @@ use std::marker::PhantomData; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierDecryptionModQStatement { - pub S: BigInt, - pub T: BigInt, - pub N_hat: BigInt, - pub N0: BigInt, - pub NN0: BigInt, - pub C: BigInt, - pub x: BigInt, - pub ek_prover: EncryptionKey, - pub phantom: PhantomData<(E, H)>, + pub S: BigInt, + pub T: BigInt, + pub N_hat: BigInt, + pub N0: BigInt, + pub NN0: BigInt, + pub C: BigInt, + pub x: BigInt, + pub ek_prover: EncryptionKey, + pub phantom: PhantomData<(E, H)>, } pub struct PaillierDecryptionModQWitness { - y: BigInt, - rho: BigInt, - phantom: PhantomData<(E, H)>, + y: BigInt, + rho: BigInt, + phantom: PhantomData<(E, H)>, } impl PaillierDecryptionModQWitness { - pub fn new(y: BigInt, rho: BigInt) -> Self { - PaillierDecryptionModQWitness { y, rho, phantom: PhantomData } - } + pub fn new(y: BigInt, rho: BigInt) -> Self { + PaillierDecryptionModQWitness { y, rho, phantom: PhantomData } + } } impl PaillierDecryptionModQStatement { - #[allow(clippy::too_many_arguments)] - pub fn generate( - S: BigInt, - T: BigInt, - N_hat: BigInt, - rho: BigInt, - prover: EncryptionKey, - ) -> (Self, PaillierDecryptionModQWitness) { - let ek_prover = prover.clone(); - // y <- Z_N - let y = BigInt::sample_below(&prover.n); - // C = (1 + N0)^y * rho^N0 mod N0^2 - let C = { - let C_ciphertext = Paillier::encrypt_with_chosen_randomness( - &ek_prover, - RawPlaintext::from(y.clone()), - &Randomness::from(rho.clone()), - ); - let C: BigInt = C_ciphertext.into(); - C - }; - let x = BigInt::mod_floor(&y, Scalar::::group_order()); - ( - Self { - S, - T, - N_hat, - N0: prover.n, - NN0: prover.nn, - C, - x, - ek_prover, - phantom: PhantomData, - }, - PaillierDecryptionModQWitness { y, rho, phantom: PhantomData }, - ) - } + #[allow(clippy::too_many_arguments)] + pub fn generate( + S: BigInt, + T: BigInt, + N_hat: BigInt, + rho: BigInt, + prover: EncryptionKey, + ) -> (Self, PaillierDecryptionModQWitness) { + let ek_prover = prover.clone(); + // y <- Z_N + let y = BigInt::sample_below(&prover.n); + // C = (1 + N0)^y * rho^N0 mod N0^2 + let C = { + let C_ciphertext = Paillier::encrypt_with_chosen_randomness( + &ek_prover, + RawPlaintext::from(y.clone()), + &Randomness::from(rho.clone()), + ); + let C: BigInt = C_ciphertext.into(); + C + }; + let x = BigInt::mod_floor(&y, Scalar::::group_order()); + ( + Self { + S, + T, + N_hat, + N0: prover.n, + NN0: prover.nn, + C, + x, + ek_prover, + phantom: PhantomData, + }, + PaillierDecryptionModQWitness { y, rho, phantom: PhantomData }, + ) + } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierDecryptionModQCommitment { - A: BigInt, - gamma: BigInt, - big_S: BigInt, - big_T: BigInt, + A: BigInt, + gamma: BigInt, + big_S: BigInt, + big_T: BigInt, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierDecryptionModQProof { - z1: BigInt, - z2: BigInt, - w: BigInt, - commitment: PaillierDecryptionModQCommitment, - phantom: PhantomData<(E, H)>, + z1: BigInt, + z2: BigInt, + w: BigInt, + commitment: PaillierDecryptionModQCommitment, + phantom: PhantomData<(E, H)>, } // Link to the UC non-interactive threshold ECDSA paper impl PaillierDecryptionModQProof { - pub fn prove( - witness: &PaillierDecryptionModQWitness, - statement: &PaillierDecryptionModQStatement, - ) -> PaillierDecryptionModQProof { - // Set up exponents - let l_exp = BigInt::pow(&BigInt::from(2), L as u32); - let lplus_exp = BigInt::pow(&BigInt::from(2), L_PLUS_EPSILON as u32); - // α ← ± 2^{l+ε} - let alpha = BigInt::sample_range(&BigInt::from(-1).mul(&lplus_exp), &lplus_exp); - // mu ← ± 2l · Nˆ - let mu = BigInt::sample_range( - &BigInt::from(-1).mul(&l_exp).mul(&statement.N_hat), - &l_exp.mul(&statement.N_hat), - ); - // nu ← 2^{l+ε} · Nˆ - let nu = BigInt::sample_range( - &BigInt::from(-1).mul(&lplus_exp).mul(&statement.N_hat), - &lplus_exp.mul(&statement.N_hat), - ); - // r <- Z*_N - let r = sample_relatively_prime_integer(&statement.N0); - // big_S = s^y * t^μ mod Nˆ - let big_S = { - let s = BigInt::mod_pow(&statement.S, &witness.y, &statement.N_hat); - let t = mod_pow_with_negative(&statement.T, &mu, &statement.N_hat); - BigInt::mod_mul(&s, &t, &statement.N_hat) - }; - // big_T = s^α & t^ν mod Nˆ - let big_T = { - let s = mod_pow_with_negative(&statement.S, &alpha, &statement.N_hat); - let t = mod_pow_with_negative(&statement.T, &nu, &statement.N_hat); - BigInt::mod_mul(&s, &t, &statement.N_hat) - }; - // A = (1 + N0)^α * r^N0 mod N0^2 - let A = { - let A_ciphertext = Paillier::encrypt_with_chosen_randomness( - &statement.ek_prover, - RawPlaintext::from(alpha.clone()), - &Randomness::from(r.clone()), - ); - let A: BigInt = A_ciphertext.into(); - A.mod_floor(&statement.NN0) - }; - // gamma = alpha mod q - let gamma = BigInt::mod_floor(&alpha, Scalar::::group_order()); - // Generate NIZK challenge - let mut e = H::new() - .chain_bigint(&A) - .chain_bigint(&gamma) - .chain_bigint(&big_S) - .chain_bigint(&big_T) - .result_bigint(); - let mut rng: ChaChaRng = ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); - let val = rng.gen_range(0..2); - e = BigInt::from(val).mul(&BigInt::from(-2)).add(&BigInt::one()).mul(&e); - let commitment: PaillierDecryptionModQCommitment = - PaillierDecryptionModQCommitment { A, gamma, big_S, big_T }; - // z1 = α + e · y - let z1 = BigInt::add(&alpha, &BigInt::mul(&e, &witness.y)); - // z2 = ν + e · μ - let z2 = BigInt::add(&nu, &BigInt::mul(&e, &mu)); - // w = r · rho^e mod N0 - let w = { - let rho = mod_pow_with_negative(&witness.rho, &e, &statement.N0); - BigInt::mod_mul(&r, &rho, &statement.N0) - }; - // Return the proof - PaillierDecryptionModQProof { z1, z2, w, commitment, phantom: PhantomData } - } + pub fn prove( + witness: &PaillierDecryptionModQWitness, + statement: &PaillierDecryptionModQStatement, + ) -> PaillierDecryptionModQProof { + // Set up exponents + let l_exp = BigInt::pow(&BigInt::from(2), L as u32); + let lplus_exp = BigInt::pow(&BigInt::from(2), L_PLUS_EPSILON as u32); + // α ← ± 2^{l+ε} + let alpha = + BigInt::sample_range(&BigInt::from(-1).mul(&lplus_exp), &lplus_exp); + // mu ← ± 2l · Nˆ + let mu = BigInt::sample_range( + &BigInt::from(-1).mul(&l_exp).mul(&statement.N_hat), + &l_exp.mul(&statement.N_hat), + ); + // nu ← 2^{l+ε} · Nˆ + let nu = BigInt::sample_range( + &BigInt::from(-1).mul(&lplus_exp).mul(&statement.N_hat), + &lplus_exp.mul(&statement.N_hat), + ); + // r <- Z*_N + let r = sample_relatively_prime_integer(&statement.N0); + // big_S = s^y * t^μ mod Nˆ + let big_S = { + let s = BigInt::mod_pow(&statement.S, &witness.y, &statement.N_hat); + let t = mod_pow_with_negative(&statement.T, &mu, &statement.N_hat); + BigInt::mod_mul(&s, &t, &statement.N_hat) + }; + // big_T = s^α & t^ν mod Nˆ + let big_T = { + let s = + mod_pow_with_negative(&statement.S, &alpha, &statement.N_hat); + let t = mod_pow_with_negative(&statement.T, &nu, &statement.N_hat); + BigInt::mod_mul(&s, &t, &statement.N_hat) + }; + // A = (1 + N0)^α * r^N0 mod N0^2 + let A = { + let A_ciphertext = Paillier::encrypt_with_chosen_randomness( + &statement.ek_prover, + RawPlaintext::from(alpha.clone()), + &Randomness::from(r.clone()), + ); + let A: BigInt = A_ciphertext.into(); + A.mod_floor(&statement.NN0) + }; + // gamma = alpha mod q + let gamma = BigInt::mod_floor(&alpha, Scalar::::group_order()); + // Generate NIZK challenge + let mut e = H::new() + .chain_bigint(&A) + .chain_bigint(&gamma) + .chain_bigint(&big_S) + .chain_bigint(&big_T) + .result_bigint(); + let mut rng: ChaChaRng = + ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); + let val = rng.gen_range(0..2); + e = BigInt::from(val) + .mul(&BigInt::from(-2)) + .add(&BigInt::one()) + .mul(&e); + let commitment: PaillierDecryptionModQCommitment = + PaillierDecryptionModQCommitment { A, gamma, big_S, big_T }; + // z1 = α + e · y + let z1 = BigInt::add(&alpha, &BigInt::mul(&e, &witness.y)); + // z2 = ν + e · μ + let z2 = BigInt::add(&nu, &BigInt::mul(&e, &mu)); + // w = r · rho^e mod N0 + let w = { + let rho = mod_pow_with_negative(&witness.rho, &e, &statement.N0); + BigInt::mod_mul(&r, &rho, &statement.N0) + }; + // Return the proof + PaillierDecryptionModQProof { + z1, + z2, + w, + commitment, + phantom: PhantomData, + } + } - pub fn verify( - proof: &PaillierDecryptionModQProof, - statement: &PaillierDecryptionModQStatement, - ) -> Result<(), Error> { - // Compute the challenge - let mut e = H::new() - .chain_bigint(&proof.commitment.A) - .chain_bigint(&proof.commitment.gamma) - .chain_bigint(&proof.commitment.big_S) - .chain_bigint(&proof.commitment.big_T) - .result_bigint(); - let mut rng: ChaChaRng = ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); - let val = rng.gen_range(0..2); - e = BigInt::from(val).mul(&BigInt::from(-2)).add(&BigInt::one()).mul(&e); - /* - FIRST EQUALITY CHECK - (1 + N0)^z1 · w^N0 = A · C^e mod N0^2 === Enc(z1,w) = A · C^e mod N0^2 - */ - // Compute the left hand side - let left_1 = { - let left_1_ciphertext = Paillier::encrypt_with_chosen_randomness( - &statement.ek_prover, - RawPlaintext::from(proof.z1.clone()), - &Randomness::from(proof.w.clone()), - ); - let left_1: BigInt = left_1_ciphertext.into(); - left_1.mod_floor(&statement.NN0) - }; - // Compute the right hand side - let right_1 = { - let C = mod_pow_with_negative(&statement.C, &e, &statement.NN0); - BigInt::mod_mul(&proof.commitment.A, &C, &statement.NN0) - }; - // Check the equality - assert!(left_1 == right_1); - /* - SECOND EQUALITY CHECK - z1 = γ + e * x mod q - */ - // Compute the left hand side - let left_2 = proof.z1.clone().mod_floor(Scalar::::group_order()); - // Compute the right hand side - let right_2 = BigInt::mod_add( - &proof.commitment.gamma, - &BigInt::mod_mul(&e, &statement.x, Scalar::::group_order()), - Scalar::::group_order(), - ); - // Check the equality - assert!(left_2 == right_2); - /* - THIRD EQUALITY CHECK - s^z1 · t^z2 = T · S^e mod Nˆ - */ - // Compute the left hand side - let left_3 = { - let s = mod_pow_with_negative(&statement.S, &proof.z1, &statement.N_hat); - let t = mod_pow_with_negative(&statement.T, &proof.z2, &statement.N_hat); - BigInt::mod_mul(&s, &t, &statement.N_hat) - }; - // Compute the right hand side - let right_3 = { - let temp = mod_pow_with_negative(&proof.commitment.big_S, &e, &statement.N_hat); - BigInt::mod_mul(&proof.commitment.big_T, &temp, &statement.N_hat) - }; - // Check the equality - assert!(left_3 == right_3); - Ok(()) - } + pub fn verify( + proof: &PaillierDecryptionModQProof, + statement: &PaillierDecryptionModQStatement, + ) -> Result<(), Error> { + // Compute the challenge + let mut e = H::new() + .chain_bigint(&proof.commitment.A) + .chain_bigint(&proof.commitment.gamma) + .chain_bigint(&proof.commitment.big_S) + .chain_bigint(&proof.commitment.big_T) + .result_bigint(); + let mut rng: ChaChaRng = + ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); + let val = rng.gen_range(0..2); + e = BigInt::from(val) + .mul(&BigInt::from(-2)) + .add(&BigInt::one()) + .mul(&e); + /* + FIRST EQUALITY CHECK + (1 + N0)^z1 · w^N0 = A · C^e mod N0^2 === Enc(z1,w) = A · C^e mod N0^2 + */ + // Compute the left hand side + let left_1 = { + let left_1_ciphertext = Paillier::encrypt_with_chosen_randomness( + &statement.ek_prover, + RawPlaintext::from(proof.z1.clone()), + &Randomness::from(proof.w.clone()), + ); + let left_1: BigInt = left_1_ciphertext.into(); + left_1.mod_floor(&statement.NN0) + }; + // Compute the right hand side + let right_1 = { + let C = mod_pow_with_negative(&statement.C, &e, &statement.NN0); + BigInt::mod_mul(&proof.commitment.A, &C, &statement.NN0) + }; + // Check the equality + assert!(left_1 == right_1); + /* + SECOND EQUALITY CHECK + z1 = γ + e * x mod q + */ + // Compute the left hand side + let left_2 = proof.z1.clone().mod_floor(Scalar::::group_order()); + // Compute the right hand side + let right_2 = BigInt::mod_add( + &proof.commitment.gamma, + &BigInt::mod_mul(&e, &statement.x, Scalar::::group_order()), + Scalar::::group_order(), + ); + // Check the equality + assert!(left_2 == right_2); + /* + THIRD EQUALITY CHECK + s^z1 · t^z2 = T · S^e mod Nˆ + */ + // Compute the left hand side + let left_3 = { + let s = mod_pow_with_negative( + &statement.S, + &proof.z1, + &statement.N_hat, + ); + let t = mod_pow_with_negative( + &statement.T, + &proof.z2, + &statement.N_hat, + ); + BigInt::mod_mul(&s, &t, &statement.N_hat) + }; + // Compute the right hand side + let right_3 = { + let temp = mod_pow_with_negative( + &proof.commitment.big_S, + &e, + &statement.N_hat, + ); + BigInt::mod_mul(&proof.commitment.big_T, &temp, &statement.N_hat) + }; + // Check the equality + assert!(left_3 == right_3); + Ok(()) + } } #[cfg(test)] mod tests { - use super::*; - use crate::utilities::{mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER}; - use curv::elliptic::curves::secp256_k1::Secp256k1; - use fs_dkr::ring_pedersen_proof::RingPedersenStatement; - use paillier::{KeyGeneration, Paillier}; - use sha2::Sha256; + use super::*; + use crate::utilities::{ + mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER, + }; + use curv::elliptic::curves::secp256_k1::Secp256k1; + use fs_dkr::ring_pedersen_proof::RingPedersenStatement; + use paillier::{KeyGeneration, Paillier}; + use sha2::Sha256; - #[test] - fn test_paillier_decryption_modulo_q() { - let (ring_pedersen_statement, _witness) = - RingPedersenStatement::::generate(); - let (ek_prover, _) = Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); - let rho: BigInt = BigInt::from_paillier_key(&ek_prover); - let (statement, witness) = PaillierDecryptionModQStatement::::generate( - ring_pedersen_statement.S, - ring_pedersen_statement.T, - ring_pedersen_statement.N, - rho, - ek_prover, - ); - let proof = PaillierDecryptionModQProof::::prove(&witness, &statement); - assert!( - PaillierDecryptionModQProof::::verify(&proof, &statement).is_ok() - ); - } + #[test] + fn test_paillier_decryption_modulo_q() { + let (ring_pedersen_statement, _witness) = + RingPedersenStatement::::generate(); + let (ek_prover, _) = + Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); + let rho: BigInt = BigInt::from_paillier_key(&ek_prover); + let (statement, witness) = + PaillierDecryptionModQStatement::::generate( + ring_pedersen_statement.S, + ring_pedersen_statement.T, + ring_pedersen_statement.N, + rho, + ek_prover, + ); + let proof = PaillierDecryptionModQProof::::prove( + &witness, &statement, + ); + assert!(PaillierDecryptionModQProof::::verify( + &proof, &statement + ) + .is_ok()); + } } diff --git a/src/utilities/enc/mod.rs b/src/utilities/enc/mod.rs index c03308f4..10558057 100644 --- a/src/utilities/enc/mod.rs +++ b/src/utilities/enc/mod.rs @@ -1,18 +1,18 @@ #![allow(non_snake_case)] /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies + Copyright 2022 by Webb Technologies - This file is part of CGGMP Threshold ECDSA library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of CGGMP Threshold ECDSA library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - CGGMP Threshold ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + CGGMP Threshold ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ //! Paillier Encryption in Range ZK – Π^enc @@ -23,246 +23,276 @@ use super::sample_relatively_prime_integer; use crate::utilities::{mod_pow_with_negative, L}; use curv::{ - arithmetic::{traits::*, Modulo}, - cryptographic_primitives::hashing::{Digest, DigestExt}, - elliptic::curves::{Curve, Scalar}, - BigInt, + arithmetic::{traits::*, Modulo}, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{Curve, Scalar}, + BigInt, +}; +use paillier::{ + EncryptWithChosenRandomness, EncryptionKey, Paillier, Randomness, + RawPlaintext, }; -use paillier::{EncryptWithChosenRandomness, EncryptionKey, Paillier, Randomness, RawPlaintext}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; use zk_paillier::zkproofs::IncorrectProof; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierEncryptionInRangeStatement { - pub N0: BigInt, - pub NN0: BigInt, - pub K: BigInt, - pub s: BigInt, - pub t: BigInt, - pub N_hat: BigInt, - pub phantom: PhantomData<(E, H)>, + pub N0: BigInt, + pub NN0: BigInt, + pub K: BigInt, + pub s: BigInt, + pub t: BigInt, + pub N_hat: BigInt, + pub phantom: PhantomData<(E, H)>, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierEncryptionInRangeWitness { - k: BigInt, - rho: BigInt, - phantom: PhantomData<(E, H)>, + k: BigInt, + rho: BigInt, + phantom: PhantomData<(E, H)>, } impl PaillierEncryptionInRangeWitness { - pub fn new(k: BigInt, rho: BigInt) -> Self { - PaillierEncryptionInRangeWitness { k, rho, phantom: PhantomData } - } + pub fn new(k: BigInt, rho: BigInt) -> Self { + PaillierEncryptionInRangeWitness { k, rho, phantom: PhantomData } + } } impl PaillierEncryptionInRangeStatement { - #[allow(clippy::too_many_arguments)] - pub fn generate( - rho: BigInt, - s: BigInt, - t: BigInt, - N_hat: BigInt, - paillier_key: EncryptionKey, - ) -> (Self, PaillierEncryptionInRangeWitness) { - // Set up exponents - let _l_exp = BigInt::pow(&BigInt::from(2), L as u32); - // Set up moduli - let N0 = paillier_key.clone().n; - let NN0 = paillier_key.clone().nn; - let k = BigInt::sample_below(Scalar::::group_order()); - let K: BigInt = Paillier::encrypt_with_chosen_randomness( - &paillier_key, - RawPlaintext::from(&k), - &Randomness::from(&rho), - ) - .into(); - - ( - Self { N0, NN0, K, s, t, N_hat, phantom: PhantomData }, - PaillierEncryptionInRangeWitness { k, rho, phantom: PhantomData }, - ) - } + #[allow(clippy::too_many_arguments)] + pub fn generate( + rho: BigInt, + s: BigInt, + t: BigInt, + N_hat: BigInt, + paillier_key: EncryptionKey, + ) -> (Self, PaillierEncryptionInRangeWitness) { + // Set up exponents + let _l_exp = BigInt::pow(&BigInt::from(2), L as u32); + // Set up moduli + let N0 = paillier_key.clone().n; + let NN0 = paillier_key.clone().nn; + let k = BigInt::sample_below(Scalar::::group_order()); + let K: BigInt = Paillier::encrypt_with_chosen_randomness( + &paillier_key, + RawPlaintext::from(&k), + &Randomness::from(&rho), + ) + .into(); + + ( + Self { N0, NN0, K, s, t, N_hat, phantom: PhantomData }, + PaillierEncryptionInRangeWitness { k, rho, phantom: PhantomData }, + ) + } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierEncryptionInRangeCommitment { - S: BigInt, - A: BigInt, - C: BigInt, + S: BigInt, + A: BigInt, + C: BigInt, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierEncryptionInRangeProof { - z_1: BigInt, - z_2: BigInt, - z_3: BigInt, - commitment: PaillierEncryptionInRangeCommitment, - phantom: PhantomData<(E, H)>, + z_1: BigInt, + z_2: BigInt, + z_3: BigInt, + commitment: PaillierEncryptionInRangeCommitment, + phantom: PhantomData<(E, H)>, } impl PaillierEncryptionInRangeProof { - #[allow(dead_code)] - pub fn prove( - witness: &PaillierEncryptionInRangeWitness, - statement: &PaillierEncryptionInRangeStatement, - ) -> Self { - // Step 1: Sample alpha between -2^{L+eps} and 2^{L+eps} - let alpha_upper = BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32); - let alpha_lower = BigInt::from(-1).mul(&alpha_upper); - let alpha = BigInt::sample_range(&alpha_lower, &alpha_upper); - - // Step 2: mu, r, gamma - // Sample mu between -2^L * N_hat and 2^L * N_hat - let mu_upper = BigInt::mul( - &statement.N_hat, - &BigInt::pow(&BigInt::from(2), crate::utilities::L as u32), - ); - let mu_lower = BigInt::from(-1).mul(&mu_upper); - let mu = BigInt::sample_range(&mu_lower, &mu_upper); - - // γ ← ± 2^{l+ε} · Nˆ - let gamma_upper = BigInt::mul( - &statement.N_hat, - &BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32), - ); - let gamma_lower = BigInt::from(-1).mul(&gamma_upper); - let gamma = BigInt::sample_range(&gamma_lower, &gamma_upper); - // Sample r from Z*_{N_0} - let r = sample_relatively_prime_integer(&statement.N0.clone()); - - // Step 3: S, A, C - // S = s^k t^mu mod N_hat - let S = BigInt::mod_mul( - &mod_pow_with_negative(&statement.s, &witness.k, &statement.N_hat), - &mod_pow_with_negative(&statement.t, &mu, &statement.N_hat), - &statement.N_hat, - ); - - // A = (1+N_0)^{alpha}r^{N_0} mod N_0^2 - let A: BigInt = Paillier::encrypt_with_chosen_randomness( - &EncryptionKey { n: statement.N0.clone(), nn: statement.NN0.clone() }, - RawPlaintext::from(&alpha), - &Randomness::from(&r), - ) - .into(); - - // C = s^alpha * t^gamma mod N_hat - let C = BigInt::mod_mul( - &mod_pow_with_negative(&statement.s, &alpha, &statement.N_hat), - &mod_pow_with_negative(&statement.t, &gamma, &statement.N_hat), - &statement.N_hat, - ); - - let commitment = - PaillierEncryptionInRangeCommitment { S: S.clone(), A: A.clone(), C: C.clone() }; - - // Step 4: Hash S, A, C - let e = H::new().chain_bigint(&S).chain_bigint(&A).chain_bigint(&C).result_bigint(); - - // Step 5: Compute z_1, z_2, z_3 - // z_1 = alpha + ek - let z_1 = BigInt::add(&alpha, &BigInt::mul(&e, &witness.k)); - // z_2 = r * rho^e mod N_0 - let z_2 = BigInt::mod_mul( - &r, - &mod_pow_with_negative(&witness.rho, &e, &statement.N0), - &statement.N0, - ); - // z_3 = gamma + e*mu - let z_3 = BigInt::add(&gamma, &BigInt::mul(&e, &mu)); - - Self { z_1, z_2, z_3, commitment, phantom: PhantomData } - } - - #[allow(dead_code)] - pub fn verify( - proof: &PaillierEncryptionInRangeProof, - statement: &PaillierEncryptionInRangeStatement, - ) -> Result<(), IncorrectProof> { - let e = H::new() - .chain_bigint(&proof.commitment.S) - .chain_bigint(&proof.commitment.A) - .chain_bigint(&proof.commitment.C) - .result_bigint(); - - // Equality Checks - let NN0 = statement.NN0.clone(); - // left_1 = (1+N_0)^{z_1}z_2^{N_0} mod N_0^2 - let left_1: BigInt = Paillier::encrypt_with_chosen_randomness( - &EncryptionKey { n: statement.N0.clone(), nn: NN0.clone() }, - RawPlaintext::from(&proof.z_1), - &Randomness::from(&proof.z_2), - ) - .into(); - // right_1 = A * K^e mod N_0^2 - let right_1 = BigInt::mod_mul( - &proof.commitment.A, - &mod_pow_with_negative(&statement.K, &e, &NN0), - &NN0, - ); - - // left_2 = s^z_1 t^z_3 mod N_hat - let left_2 = BigInt::mod_mul( - &mod_pow_with_negative(&statement.s, &proof.z_1, &statement.N_hat), - &mod_pow_with_negative(&statement.t, &proof.z_3, &statement.N_hat), - &statement.N_hat, - ); - // right_2 = C * S^e mod N_hat - let right_2 = BigInt::mod_mul( - &proof.commitment.C, - &mod_pow_with_negative(&proof.commitment.S, &e, &statement.N_hat), - &statement.N_hat, - ); - - if left_1.mod_floor(&NN0) != right_1 || left_2 != right_2 { - return Err(IncorrectProof) - } - - // Range Check -2^{L + eps} <= z_1 <= 2^{L+eps} - let lower_bound_check: bool = proof.z_1 >= - BigInt::from(-1) - .mul(&BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32)); - - let upper_bound_check = - proof.z_1 <= BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32); - - if !(lower_bound_check && upper_bound_check) { - return Err(IncorrectProof) - } - - Ok(()) - } + #[allow(dead_code)] + pub fn prove( + witness: &PaillierEncryptionInRangeWitness, + statement: &PaillierEncryptionInRangeStatement, + ) -> Self { + // Step 1: Sample alpha between -2^{L+eps} and 2^{L+eps} + let alpha_upper = BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + ); + let alpha_lower = BigInt::from(-1).mul(&alpha_upper); + let alpha = BigInt::sample_range(&alpha_lower, &alpha_upper); + + // Step 2: mu, r, gamma + // Sample mu between -2^L * N_hat and 2^L * N_hat + let mu_upper = BigInt::mul( + &statement.N_hat, + &BigInt::pow(&BigInt::from(2), crate::utilities::L as u32), + ); + let mu_lower = BigInt::from(-1).mul(&mu_upper); + let mu = BigInt::sample_range(&mu_lower, &mu_upper); + + // γ ← ± 2^{l+ε} · Nˆ + let gamma_upper = BigInt::mul( + &statement.N_hat, + &BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + ), + ); + let gamma_lower = BigInt::from(-1).mul(&gamma_upper); + let gamma = BigInt::sample_range(&gamma_lower, &gamma_upper); + // Sample r from Z*_{N_0} + let r = sample_relatively_prime_integer(&statement.N0.clone()); + + // Step 3: S, A, C + // S = s^k t^mu mod N_hat + let S = BigInt::mod_mul( + &mod_pow_with_negative(&statement.s, &witness.k, &statement.N_hat), + &mod_pow_with_negative(&statement.t, &mu, &statement.N_hat), + &statement.N_hat, + ); + + // A = (1+N_0)^{alpha}r^{N_0} mod N_0^2 + let A: BigInt = Paillier::encrypt_with_chosen_randomness( + &EncryptionKey { + n: statement.N0.clone(), + nn: statement.NN0.clone(), + }, + RawPlaintext::from(&alpha), + &Randomness::from(&r), + ) + .into(); + + // C = s^alpha * t^gamma mod N_hat + let C = BigInt::mod_mul( + &mod_pow_with_negative(&statement.s, &alpha, &statement.N_hat), + &mod_pow_with_negative(&statement.t, &gamma, &statement.N_hat), + &statement.N_hat, + ); + + let commitment = PaillierEncryptionInRangeCommitment { + S: S.clone(), + A: A.clone(), + C: C.clone(), + }; + + // Step 4: Hash S, A, C + let e = H::new() + .chain_bigint(&S) + .chain_bigint(&A) + .chain_bigint(&C) + .result_bigint(); + + // Step 5: Compute z_1, z_2, z_3 + // z_1 = alpha + ek + let z_1 = BigInt::add(&alpha, &BigInt::mul(&e, &witness.k)); + // z_2 = r * rho^e mod N_0 + let z_2 = BigInt::mod_mul( + &r, + &mod_pow_with_negative(&witness.rho, &e, &statement.N0), + &statement.N0, + ); + // z_3 = gamma + e*mu + let z_3 = BigInt::add(&gamma, &BigInt::mul(&e, &mu)); + + Self { z_1, z_2, z_3, commitment, phantom: PhantomData } + } + + #[allow(dead_code)] + pub fn verify( + proof: &PaillierEncryptionInRangeProof, + statement: &PaillierEncryptionInRangeStatement, + ) -> Result<(), IncorrectProof> { + let e = H::new() + .chain_bigint(&proof.commitment.S) + .chain_bigint(&proof.commitment.A) + .chain_bigint(&proof.commitment.C) + .result_bigint(); + + // Equality Checks + let NN0 = statement.NN0.clone(); + // left_1 = (1+N_0)^{z_1}z_2^{N_0} mod N_0^2 + let left_1: BigInt = Paillier::encrypt_with_chosen_randomness( + &EncryptionKey { n: statement.N0.clone(), nn: NN0.clone() }, + RawPlaintext::from(&proof.z_1), + &Randomness::from(&proof.z_2), + ) + .into(); + // right_1 = A * K^e mod N_0^2 + let right_1 = BigInt::mod_mul( + &proof.commitment.A, + &mod_pow_with_negative(&statement.K, &e, &NN0), + &NN0, + ); + + // left_2 = s^z_1 t^z_3 mod N_hat + let left_2 = BigInt::mod_mul( + &mod_pow_with_negative(&statement.s, &proof.z_1, &statement.N_hat), + &mod_pow_with_negative(&statement.t, &proof.z_3, &statement.N_hat), + &statement.N_hat, + ); + // right_2 = C * S^e mod N_hat + let right_2 = BigInt::mod_mul( + &proof.commitment.C, + &mod_pow_with_negative(&proof.commitment.S, &e, &statement.N_hat), + &statement.N_hat, + ); + + if left_1.mod_floor(&NN0) != right_1 || left_2 != right_2 { + return Err(IncorrectProof) + } + + // Range Check -2^{L + eps} <= z_1 <= 2^{L+eps} + let lower_bound_check: bool = proof.z_1 >= + BigInt::from(-1).mul(&BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + )); + + let upper_bound_check = proof.z_1 <= + BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + ); + + if !(lower_bound_check && upper_bound_check) { + return Err(IncorrectProof) + } + + Ok(()) + } } #[cfg(test)] mod tests { - use super::*; - use crate::utilities::{mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER}; - use curv::elliptic::curves::secp256_k1::Secp256k1; - use fs_dkr::ring_pedersen_proof::RingPedersenStatement; - use paillier::{KeyGeneration, Paillier}; - use sha2::Sha256; - - #[test] - fn test_paillier_encryption_in_range_proof() { - let (ring_pedersen_statement, _witness) = - RingPedersenStatement::::generate(); - let (paillier_key, _) = Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); - - let rho: BigInt = BigInt::from_paillier_key(&paillier_key); - let (statement, witness) = - PaillierEncryptionInRangeStatement::::generate( - rho, - ring_pedersen_statement.S, - ring_pedersen_statement.T, - ring_pedersen_statement.N, - paillier_key, - ); - let proof = - PaillierEncryptionInRangeProof::::prove(&witness, &statement); - assert!(PaillierEncryptionInRangeProof::::verify(&proof, &statement,) - .is_ok()); - } + use super::*; + use crate::utilities::{ + mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER, + }; + use curv::elliptic::curves::secp256_k1::Secp256k1; + use fs_dkr::ring_pedersen_proof::RingPedersenStatement; + use paillier::{KeyGeneration, Paillier}; + use sha2::Sha256; + + #[test] + fn test_paillier_encryption_in_range_proof() { + let (ring_pedersen_statement, _witness) = + RingPedersenStatement::::generate(); + let (paillier_key, _) = + Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); + + let rho: BigInt = BigInt::from_paillier_key(&paillier_key); + let (statement, witness) = + PaillierEncryptionInRangeStatement::::generate( + rho, + ring_pedersen_statement.S, + ring_pedersen_statement.T, + ring_pedersen_statement.N, + paillier_key, + ); + let proof = PaillierEncryptionInRangeProof::::prove( + &witness, &statement, + ); + assert!(PaillierEncryptionInRangeProof::::verify( + &proof, &statement, + ) + .is_ok()); + } } diff --git a/src/utilities/log_star/mod.rs b/src/utilities/log_star/mod.rs index 29ac2f12..27b5d77a 100644 --- a/src/utilities/log_star/mod.rs +++ b/src/utilities/log_star/mod.rs @@ -1,18 +1,18 @@ #![allow(non_snake_case)] /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies + Copyright 2022 by Webb Technologies - This file is part of CGGMP Threshold ECDSA library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of CGGMP Threshold ECDSA library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - CGGMP Threshold ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + CGGMP Threshold ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ //! Knowledge of Exponent vs Paillier Encryption – Π^{log∗} @@ -24,259 +24,310 @@ use super::sample_relatively_prime_integer; use crate::utilities::{mod_pow_with_negative, L}; use curv::{ - arithmetic::{traits::*, Modulo}, - cryptographic_primitives::hashing::{Digest, DigestExt}, - elliptic::curves::{Curve, Point, Scalar}, - BigInt, + arithmetic::{traits::*, Modulo}, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{Curve, Point, Scalar}, + BigInt, +}; +use paillier::{ + EncryptWithChosenRandomness, EncryptionKey, Paillier, Randomness, + RawPlaintext, }; -use paillier::{EncryptWithChosenRandomness, EncryptionKey, Paillier, Randomness, RawPlaintext}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; use zk_paillier::zkproofs::IncorrectProof; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KnowledgeOfExponentPaillierEncryptionStatement { - pub N0: BigInt, - pub NN0: BigInt, - pub C: BigInt, - pub X: Point, - // Πlog∗ states X = g^x. - // This g is not allows the generator of secp256k1. - // An example is 3-Round Presigning (see Figure 7, Round 3 in the paper) where: - // - x = k_i - // - X = Delta_i - // - Delta_i = Gamma^{k_i} - // :- g = Gamma - pub g: Point, - pub N_hat: BigInt, - pub s: BigInt, - pub t: BigInt, - pub phantom: PhantomData<(E, H)>, +pub struct KnowledgeOfExponentPaillierEncryptionStatement< + E: Curve, + H: Digest + Clone, +> { + pub N0: BigInt, + pub NN0: BigInt, + pub C: BigInt, + pub X: Point, + // Πlog∗ states X = g^x. + // This g is not allows the generator of secp256k1. + // An example is 3-Round Presigning (see Figure 7, Round 3 in the paper) + // where: + // - x = k_i + // - X = Delta_i + // - Delta_i = Gamma^{k_i} + // :- g = Gamma + pub g: Point, + pub N_hat: BigInt, + pub s: BigInt, + pub t: BigInt, + pub phantom: PhantomData<(E, H)>, } -pub struct KnowledgeOfExponentPaillierEncryptionWitness { - x: BigInt, - rho: BigInt, - phantom: PhantomData<(E, H)>, +pub struct KnowledgeOfExponentPaillierEncryptionWitness< + E: Curve, + H: Digest + Clone, +> { + x: BigInt, + rho: BigInt, + phantom: PhantomData<(E, H)>, } -impl KnowledgeOfExponentPaillierEncryptionWitness { - pub fn new(x: BigInt, rho: BigInt) -> Self { - KnowledgeOfExponentPaillierEncryptionWitness { x, rho, phantom: PhantomData } - } +impl + KnowledgeOfExponentPaillierEncryptionWitness +{ + pub fn new(x: BigInt, rho: BigInt) -> Self { + KnowledgeOfExponentPaillierEncryptionWitness { + x, + rho, + phantom: PhantomData, + } + } } -impl KnowledgeOfExponentPaillierEncryptionStatement { - #[allow(clippy::too_many_arguments)] - pub fn generate( - rho: BigInt, - g: Option>, - s: BigInt, - t: BigInt, - N_hat: BigInt, - paillier_key: EncryptionKey, - ) -> (Self, KnowledgeOfExponentPaillierEncryptionWitness) { - // Set up exponents - let l_exp = BigInt::pow(&BigInt::from(2), L as u32); - // Set up moduli - let N0 = paillier_key.clone().n; - let NN0 = paillier_key.nn; - let x = BigInt::sample_range(&BigInt::from(-1).mul(&l_exp), &l_exp); - let g = g.unwrap_or(Point::::generator().to_point()); - let X = &g * Scalar::from(&x); - let C: BigInt = Paillier::encrypt_with_chosen_randomness( - &EncryptionKey { n: N0.clone(), nn: NN0.clone() }, - RawPlaintext::from(&x), - &Randomness::from(&rho), - ) - .into(); - ( - Self { N0, NN0, C, X, g, N_hat, s, t, phantom: PhantomData }, - KnowledgeOfExponentPaillierEncryptionWitness { x, rho, phantom: PhantomData }, - ) - } +impl + KnowledgeOfExponentPaillierEncryptionStatement +{ + #[allow(clippy::too_many_arguments)] + pub fn generate( + rho: BigInt, + g: Option>, + s: BigInt, + t: BigInt, + N_hat: BigInt, + paillier_key: EncryptionKey, + ) -> (Self, KnowledgeOfExponentPaillierEncryptionWitness) { + // Set up exponents + let l_exp = BigInt::pow(&BigInt::from(2), L as u32); + // Set up moduli + let N0 = paillier_key.clone().n; + let NN0 = paillier_key.nn; + let x = BigInt::sample_range(&BigInt::from(-1).mul(&l_exp), &l_exp); + let g = g.unwrap_or(Point::::generator().to_point()); + let X = &g * Scalar::from(&x); + let C: BigInt = Paillier::encrypt_with_chosen_randomness( + &EncryptionKey { n: N0.clone(), nn: NN0.clone() }, + RawPlaintext::from(&x), + &Randomness::from(&rho), + ) + .into(); + ( + Self { N0, NN0, C, X, g, N_hat, s, t, phantom: PhantomData }, + KnowledgeOfExponentPaillierEncryptionWitness { + x, + rho, + phantom: PhantomData, + }, + ) + } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct KnowledgeOfExponentPaillierEncryptionCommitment { - S: BigInt, - A: BigInt, - Y: Point, - D: BigInt, + S: BigInt, + A: BigInt, + Y: Point, + D: BigInt, } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KnowledgeOfExponentPaillierEncryptionProof { - z_1: BigInt, - z_2: BigInt, - z_3: BigInt, - commitment: KnowledgeOfExponentPaillierEncryptionCommitment, - phantom: PhantomData<(E, H)>, +pub struct KnowledgeOfExponentPaillierEncryptionProof< + E: Curve, + H: Digest + Clone, +> { + z_1: BigInt, + z_2: BigInt, + z_3: BigInt, + commitment: KnowledgeOfExponentPaillierEncryptionCommitment, + phantom: PhantomData<(E, H)>, } // Link to the UC non-interactive threshold ECDSA paper -impl KnowledgeOfExponentPaillierEncryptionProof { - pub fn prove( - witness: &KnowledgeOfExponentPaillierEncryptionWitness, - statement: &KnowledgeOfExponentPaillierEncryptionStatement, - ) -> KnowledgeOfExponentPaillierEncryptionProof { - // Step 1: Sample alpha between -2^{l+ε} and 2^{l+ε} - let alpha_upper = BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32); - let alpha_lower = BigInt::from(-1).mul(&alpha_upper); - let alpha = BigInt::sample_range(&alpha_lower, &alpha_upper); - - // Step 2: mu, r, gamma - // Sample mu between -2^L * N_hat and 2^L * N_hat - let mu_upper = BigInt::mul( - &statement.N_hat, - &BigInt::pow(&BigInt::from(2), crate::utilities::L as u32), - ); - let mu_lower = BigInt::from(-1).mul(&mu_upper); - let mu = BigInt::sample_range(&mu_lower, &mu_upper); - - // γ ← ± 2^{l+ε} · Nˆ - let gamma_upper = BigInt::mul( - &statement.N_hat, - &BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32), - ); - let gamma_lower = BigInt::from(-1).mul(&gamma_upper); - let gamma = BigInt::sample_range(&gamma_lower, &gamma_upper); - // Sample r from Z*_{N_0} - let r = sample_relatively_prime_integer(&statement.N0.clone()); - - // S = s^x t^mu mod N_hat - let S = BigInt::mod_mul( - &mod_pow_with_negative(&statement.s, &witness.x, &statement.N_hat), - &mod_pow_with_negative(&statement.t, &mu, &statement.N_hat), - &statement.N_hat, - ); - - // A = (1+N_0)^{alpha}r^{N_0} mod N_0^2 - let A: BigInt = Paillier::encrypt_with_chosen_randomness( - &EncryptionKey { n: statement.N0.clone(), nn: statement.NN0.clone() }, - RawPlaintext::from(&alpha), - &Randomness::from(&r), - ) - .into(); - - // Y = g^alpha - let Y = &statement.g * Scalar::from_bigint(&alpha); - // D = s^alpha t^gamma mod N_hat - let D = BigInt::mod_mul( - &mod_pow_with_negative(&statement.s, &alpha, &statement.N_hat), - &mod_pow_with_negative(&statement.t, &gamma, &statement.N_hat), - &statement.N_hat, - ); - - let commitment = KnowledgeOfExponentPaillierEncryptionCommitment { - S: S.clone(), - A: A.clone(), - Y: Y.clone(), - D: D.clone(), - }; - - let e = H::new() - .chain_bigint(&S) - .chain_bigint(&A) - .chain_point(&Y) - .chain_bigint(&D) - .result_bigint(); - - // Step 5: Compute z_1, z_2, z_3 - // z_1 = alpha + ex - let z_1 = BigInt::add(&alpha, &BigInt::mul(&e, &witness.x)); - // z_2 = r * rho^e mod N_0 - let z_2 = BigInt::mod_mul( - &r, - &mod_pow_with_negative(&witness.rho, &e, &statement.N0), - &statement.N0, - ); - // z_3 = gamma + e*mu - let z_3 = BigInt::add(&gamma, &BigInt::mul(&e, &mu)); - - Self { z_1, z_2, z_3, commitment, phantom: PhantomData } - } - - pub fn verify( - proof: &KnowledgeOfExponentPaillierEncryptionProof, - statement: &KnowledgeOfExponentPaillierEncryptionStatement, - ) -> Result<(), IncorrectProof> { - let e = H::new() - .chain_bigint(&proof.commitment.S) - .chain_bigint(&proof.commitment.A) - .chain_point(&proof.commitment.Y) - .chain_bigint(&proof.commitment.D) - .result_bigint(); - - // left_1 = (1+N_0)^{z_1}z_2^{N_0} mod N_0^2 - let left_1: BigInt = Paillier::encrypt_with_chosen_randomness( - &EncryptionKey { n: statement.N0.clone(), nn: statement.NN0.clone() }, - RawPlaintext::from(&proof.z_1), - &Randomness::from(&proof.z_2), - ) - .into(); - - // right_1 = A * C^e - let right_1 = BigInt::mod_mul( - &proof.commitment.A, - &mod_pow_with_negative(&statement.C, &e, &statement.NN0), - &statement.NN0, - ); - - // left_2 = g^z_1 - let left_2 = &statement.g * Scalar::from_bigint(&proof.z_1); - // right_2 = Y * X^e - let right_2 = proof.commitment.Y.clone() + (statement.X.clone() * Scalar::from_bigint(&e)); - - // left_3 = s^z_1 t^z_3 mod N_hat - let left_3 = BigInt::mod_mul( - &mod_pow_with_negative(&statement.s, &proof.z_1, &statement.N_hat), - &mod_pow_with_negative(&statement.t, &proof.z_3, &statement.N_hat), - &statement.N_hat, - ); - - // right_3 = D * S^e mod N_hat - let right_3 = BigInt::mod_mul( - &proof.commitment.D, - &mod_pow_with_negative(&proof.commitment.S, &e, &statement.N_hat), - &statement.N_hat, - ); - - if left_1.mod_floor(&statement.NN0) != right_1 || left_2 != right_2 || left_3 != right_3 { - return Err(IncorrectProof) - } - - // Range Check -2^{L + eps} <= z_1 <= 2^{L+eps} - let lower_bound_check: bool = proof.z_1 >= - BigInt::from(-1) - .mul(&BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32)); - - let upper_bound_check = - proof.z_1 <= BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32); - - if !(lower_bound_check && upper_bound_check) { - return Err(IncorrectProof) - } - Ok(()) - } +impl + KnowledgeOfExponentPaillierEncryptionProof +{ + pub fn prove( + witness: &KnowledgeOfExponentPaillierEncryptionWitness, + statement: &KnowledgeOfExponentPaillierEncryptionStatement, + ) -> KnowledgeOfExponentPaillierEncryptionProof { + // Step 1: Sample alpha between -2^{l+ε} and 2^{l+ε} + let alpha_upper = BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + ); + let alpha_lower = BigInt::from(-1).mul(&alpha_upper); + let alpha = BigInt::sample_range(&alpha_lower, &alpha_upper); + + // Step 2: mu, r, gamma + // Sample mu between -2^L * N_hat and 2^L * N_hat + let mu_upper = BigInt::mul( + &statement.N_hat, + &BigInt::pow(&BigInt::from(2), crate::utilities::L as u32), + ); + let mu_lower = BigInt::from(-1).mul(&mu_upper); + let mu = BigInt::sample_range(&mu_lower, &mu_upper); + + // γ ← ± 2^{l+ε} · Nˆ + let gamma_upper = BigInt::mul( + &statement.N_hat, + &BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + ), + ); + let gamma_lower = BigInt::from(-1).mul(&gamma_upper); + let gamma = BigInt::sample_range(&gamma_lower, &gamma_upper); + // Sample r from Z*_{N_0} + let r = sample_relatively_prime_integer(&statement.N0.clone()); + + // S = s^x t^mu mod N_hat + let S = BigInt::mod_mul( + &mod_pow_with_negative(&statement.s, &witness.x, &statement.N_hat), + &mod_pow_with_negative(&statement.t, &mu, &statement.N_hat), + &statement.N_hat, + ); + + // A = (1+N_0)^{alpha}r^{N_0} mod N_0^2 + let A: BigInt = Paillier::encrypt_with_chosen_randomness( + &EncryptionKey { + n: statement.N0.clone(), + nn: statement.NN0.clone(), + }, + RawPlaintext::from(&alpha), + &Randomness::from(&r), + ) + .into(); + + // Y = g^alpha + let Y = &statement.g * Scalar::from_bigint(&alpha); + // D = s^alpha t^gamma mod N_hat + let D = BigInt::mod_mul( + &mod_pow_with_negative(&statement.s, &alpha, &statement.N_hat), + &mod_pow_with_negative(&statement.t, &gamma, &statement.N_hat), + &statement.N_hat, + ); + + let commitment = KnowledgeOfExponentPaillierEncryptionCommitment { + S: S.clone(), + A: A.clone(), + Y: Y.clone(), + D: D.clone(), + }; + + let e = H::new() + .chain_bigint(&S) + .chain_bigint(&A) + .chain_point(&Y) + .chain_bigint(&D) + .result_bigint(); + + // Step 5: Compute z_1, z_2, z_3 + // z_1 = alpha + ex + let z_1 = BigInt::add(&alpha, &BigInt::mul(&e, &witness.x)); + // z_2 = r * rho^e mod N_0 + let z_2 = BigInt::mod_mul( + &r, + &mod_pow_with_negative(&witness.rho, &e, &statement.N0), + &statement.N0, + ); + // z_3 = gamma + e*mu + let z_3 = BigInt::add(&gamma, &BigInt::mul(&e, &mu)); + + Self { z_1, z_2, z_3, commitment, phantom: PhantomData } + } + + pub fn verify( + proof: &KnowledgeOfExponentPaillierEncryptionProof, + statement: &KnowledgeOfExponentPaillierEncryptionStatement, + ) -> Result<(), IncorrectProof> { + let e = H::new() + .chain_bigint(&proof.commitment.S) + .chain_bigint(&proof.commitment.A) + .chain_point(&proof.commitment.Y) + .chain_bigint(&proof.commitment.D) + .result_bigint(); + + // left_1 = (1+N_0)^{z_1}z_2^{N_0} mod N_0^2 + let left_1: BigInt = Paillier::encrypt_with_chosen_randomness( + &EncryptionKey { + n: statement.N0.clone(), + nn: statement.NN0.clone(), + }, + RawPlaintext::from(&proof.z_1), + &Randomness::from(&proof.z_2), + ) + .into(); + + // right_1 = A * C^e + let right_1 = BigInt::mod_mul( + &proof.commitment.A, + &mod_pow_with_negative(&statement.C, &e, &statement.NN0), + &statement.NN0, + ); + + // left_2 = g^z_1 + let left_2 = &statement.g * Scalar::from_bigint(&proof.z_1); + // right_2 = Y * X^e + let right_2 = proof.commitment.Y.clone() + + (statement.X.clone() * Scalar::from_bigint(&e)); + + // left_3 = s^z_1 t^z_3 mod N_hat + let left_3 = BigInt::mod_mul( + &mod_pow_with_negative(&statement.s, &proof.z_1, &statement.N_hat), + &mod_pow_with_negative(&statement.t, &proof.z_3, &statement.N_hat), + &statement.N_hat, + ); + + // right_3 = D * S^e mod N_hat + let right_3 = BigInt::mod_mul( + &proof.commitment.D, + &mod_pow_with_negative(&proof.commitment.S, &e, &statement.N_hat), + &statement.N_hat, + ); + + if left_1.mod_floor(&statement.NN0) != right_1 || + left_2 != right_2 || + left_3 != right_3 + { + return Err(IncorrectProof) + } + + // Range Check -2^{L + eps} <= z_1 <= 2^{L+eps} + let lower_bound_check: bool = proof.z_1 >= + BigInt::from(-1).mul(&BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + )); + + let upper_bound_check = proof.z_1 <= + BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + ); + + if !(lower_bound_check && upper_bound_check) { + return Err(IncorrectProof) + } + Ok(()) + } } #[cfg(test)] mod tests { - use super::*; - use crate::utilities::{mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER}; - use curv::elliptic::curves::secp256_k1::Secp256k1; - use fs_dkr::ring_pedersen_proof::RingPedersenStatement; - use paillier::{KeyGeneration, Paillier}; - use sha2::Sha256; - - #[test] - fn test_log_star_proof() { - let (ring_pedersen_statement, _witness) = - RingPedersenStatement::::generate(); - let (paillier_key, _) = Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); - - let rho: BigInt = BigInt::from_paillier_key(&paillier_key); - let (statement, witness) = + use super::*; + use crate::utilities::{ + mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER, + }; + use curv::elliptic::curves::secp256_k1::Secp256k1; + use fs_dkr::ring_pedersen_proof::RingPedersenStatement; + use paillier::{KeyGeneration, Paillier}; + use sha2::Sha256; + + #[test] + fn test_log_star_proof() { + let (ring_pedersen_statement, _witness) = + RingPedersenStatement::::generate(); + let (paillier_key, _) = + Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); + + let rho: BigInt = BigInt::from_paillier_key(&paillier_key); + let (statement, witness) = KnowledgeOfExponentPaillierEncryptionStatement::::generate( rho, Some(Point::::generator().to_point()), @@ -285,12 +336,13 @@ mod tests { ring_pedersen_statement.N, paillier_key, ); - let proof = KnowledgeOfExponentPaillierEncryptionProof::::prove( - &witness, &statement, - ); - assert!(KnowledgeOfExponentPaillierEncryptionProof::::verify( + let proof = KnowledgeOfExponentPaillierEncryptionProof::< + Secp256k1, + Sha256, + >::prove(&witness, &statement); + assert!(KnowledgeOfExponentPaillierEncryptionProof::::verify( &proof, &statement ) .is_ok()); - } + } } diff --git a/src/utilities/mod.rs b/src/utilities/mod.rs index 2bf95595..f5627013 100644 --- a/src/utilities/mod.rs +++ b/src/utilities/mod.rs @@ -1,6 +1,6 @@ use curv::{ - arithmetic::{BasicOps, Integer, Modulo, NumberTests, Samplable, Zero}, - BigInt, + arithmetic::{BasicOps, Integer, Modulo, NumberTests, Samplable, Zero}, + BigInt, }; pub mod aff_g; @@ -15,20 +15,24 @@ pub mod zk_pdl; pub mod zk_pdl_with_slack; pub fn sample_relatively_prime_integer(n: &BigInt) -> BigInt { - let mut sample = BigInt::sample_below(n); - while BigInt::gcd(&sample, n) != BigInt::from(1) { - sample = BigInt::sample_below(n); - } - sample + let mut sample = BigInt::sample_below(n); + while BigInt::gcd(&sample, n) != BigInt::from(1) { + sample = BigInt::sample_below(n); + } + sample } -pub fn mod_pow_with_negative(v: &BigInt, pow: &BigInt, modulus: &BigInt) -> BigInt { - if BigInt::is_negative(pow) { - let temp = BigInt::mod_pow(v, &pow.abs(), modulus); - BigInt::mod_inv(&temp, modulus).unwrap_or_else(BigInt::zero) - } else { - BigInt::mod_pow(v, pow, modulus) - } +pub fn mod_pow_with_negative( + v: &BigInt, + pow: &BigInt, + modulus: &BigInt, +) -> BigInt { + if BigInt::is_negative(pow) { + let temp = BigInt::mod_pow(v, &pow.abs(), modulus); + BigInt::mod_inv(&temp, modulus).unwrap_or_else(BigInt::zero) + } else { + BigInt::mod_pow(v, pow, modulus) + } } pub const SEC_PARAM: usize = 256; @@ -37,13 +41,14 @@ pub const OT_PARAM: usize = 128; pub const OT_BYTES: usize = OT_PARAM / 8; pub const STAT_PARAM: usize = 80; -// ZK_MOD_ITERATIONS is the number of iterations that are performed to prove the validity of -// a Paillier-Blum modulus N. -// Theoretically, the number of iterations corresponds to the statistical security parameter, -// and would be 80. -// The way it is used in the refresh protocol ensures that the prover cannot guess in advance the -// secret ρ used to instantiate the hash function. -// Since sampling primes is expensive, we argue that the security can be reduced. +// ZK_MOD_ITERATIONS is the number of iterations that are performed to prove the +// validity of a Paillier-Blum modulus N. +// Theoretically, the number of iterations corresponds to the statistical +// security parameter, and would be 80. +// The way it is used in the refresh protocol ensures that the prover cannot +// guess in advance the secret ρ used to instantiate the hash function. +// Since sampling primes is expensive, we argue that the security can be +// reduced. pub const ZK_MOD_ITERATIONS: usize = 12; #[allow(clippy::identity_op)] diff --git a/src/utilities/mta/mod.rs b/src/utilities/mta/mod.rs index 787992fa..ecff7aac 100644 --- a/src/utilities/mta/mod.rs +++ b/src/utilities/mta/mod.rs @@ -1,29 +1,29 @@ /* - Multi-party ECDSA + Multi-party ECDSA - Copyright 2018 by Kzen Networks + Copyright 2018 by Kzen Networks - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ /// MtA is described in https://eprint.iacr.org/2019/114.pdf section 3 use curv::arithmetic::traits::Samplable; use curv::{ - cryptographic_primitives::proofs::sigma_dlog::DLogProof, - elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, - BigInt, + cryptographic_primitives::proofs::sigma_dlog::DLogProof, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, }; use paillier::{ - traits::EncryptWithChosenRandomness, Add, Decrypt, DecryptionKey, EncryptionKey, Mul, Paillier, - Randomness, RawCiphertext, RawPlaintext, + traits::EncryptWithChosenRandomness, Add, Decrypt, DecryptionKey, + EncryptionKey, Mul, Paillier, Randomness, RawCiphertext, RawPlaintext, }; use zk_paillier::zkproofs::DLogStatement; @@ -31,9 +31,9 @@ use serde::{Deserialize, Serialize}; use sha2::Sha256; use crate::{ - party_i::PartyPrivate, - utilities::mta::range_proofs::AliceProof, - Error::{self, InvalidKey}, + party_i::PartyPrivate, + utilities::mta::range_proofs::AliceProof, + Error::{self, InvalidKey}, }; pub mod range_proofs; @@ -42,172 +42,193 @@ mod test; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct MessageA { - pub c: BigInt, // paillier encryption - pub range_proofs: Vec, /* proofs (using other parties' h1,h2,N_tilde) that the - * plaintext is small */ + pub c: BigInt, // paillier encryption + pub range_proofs: Vec, /* proofs (using other parties' + * h1,h2,N_tilde) that the + * plaintext is small */ } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct MessageB { - pub c: BigInt, // paillier encryption - pub b_proof: DLogProof, - pub beta_tag_proof: DLogProof, + pub c: BigInt, // paillier encryption + pub b_proof: DLogProof, + pub beta_tag_proof: DLogProof, } impl MessageA { - /// Creates a new `messageA` using Alice's Paillier encryption key and `dlog_statements` - /// - other parties' `h1,h2,N_tilde`s for range proofs. - /// If range proofs are not needed (one example is identification of aborts where we - /// only want to reconstruct a ciphertext), `dlog_statements` can be an empty slice. - pub fn a( - a: &Scalar, - alice_ek: &EncryptionKey, - dlog_statements: &[DLogStatement], - ) -> (Self, BigInt) { - let randomness = BigInt::sample_below(&alice_ek.n); - let m_a = MessageA::a_with_predefined_randomness(a, alice_ek, &randomness, dlog_statements); - (m_a, randomness) - } - - pub fn a_with_predefined_randomness( - a: &Scalar, - alice_ek: &EncryptionKey, - randomness: &BigInt, - dlog_statements: &[DLogStatement], - ) -> Self { - let c_a = Paillier::encrypt_with_chosen_randomness( - alice_ek, - RawPlaintext::from(a.to_bigint()), - &Randomness::from(randomness.clone()), - ) - .0 - .clone() - .into_owned(); - let alice_range_proofs = dlog_statements - .iter() - .map(|dlog_statement| { - AliceProof::generate(&a.to_bigint(), &c_a, alice_ek, dlog_statement, randomness) - }) - .collect::>(); - - Self { c: c_a, range_proofs: alice_range_proofs } - } + /// Creates a new `messageA` using Alice's Paillier encryption key and + /// `dlog_statements` + /// - other parties' `h1,h2,N_tilde`s for range proofs. + /// If range proofs are not needed (one example is identification of aborts + /// where we only want to reconstruct a ciphertext), `dlog_statements` + /// can be an empty slice. + pub fn a( + a: &Scalar, + alice_ek: &EncryptionKey, + dlog_statements: &[DLogStatement], + ) -> (Self, BigInt) { + let randomness = BigInt::sample_below(&alice_ek.n); + let m_a = MessageA::a_with_predefined_randomness( + a, + alice_ek, + &randomness, + dlog_statements, + ); + (m_a, randomness) + } + + pub fn a_with_predefined_randomness( + a: &Scalar, + alice_ek: &EncryptionKey, + randomness: &BigInt, + dlog_statements: &[DLogStatement], + ) -> Self { + let c_a = Paillier::encrypt_with_chosen_randomness( + alice_ek, + RawPlaintext::from(a.to_bigint()), + &Randomness::from(randomness.clone()), + ) + .0 + .clone() + .into_owned(); + let alice_range_proofs = dlog_statements + .iter() + .map(|dlog_statement| { + AliceProof::generate( + &a.to_bigint(), + &c_a, + alice_ek, + dlog_statement, + randomness, + ) + }) + .collect::>(); + + Self { c: c_a, range_proofs: alice_range_proofs } + } } impl MessageB { - pub fn b( - b: &Scalar, - alice_ek: &EncryptionKey, - m_a: MessageA, - dlog_statements: &[DLogStatement], - ) -> Result<(Self, Scalar, BigInt, BigInt), Error> { - let beta_tag = BigInt::sample_below(&alice_ek.n); - let randomness = BigInt::sample_below(&alice_ek.n); - let (m_b, beta) = MessageB::b_with_predefined_randomness( - b, - alice_ek, - m_a, - &randomness, - &beta_tag, - dlog_statements, - )?; - - Ok((m_b, beta, randomness, beta_tag)) - } - - pub fn b_with_predefined_randomness( - b: &Scalar, - alice_ek: &EncryptionKey, - m_a: MessageA, - randomness: &BigInt, - beta_tag: &BigInt, - dlog_statements: &[DLogStatement], - ) -> Result<(Self, Scalar), Error> { - if m_a.range_proofs.len() != dlog_statements.len() { - return Err(InvalidKey) - } - // verify proofs - if !m_a - .range_proofs - .iter() - .zip(dlog_statements) - .map(|(proof, dlog_statement)| proof.verify(&m_a.c, alice_ek, dlog_statement)) - .all(|x| x) - { - return Err(InvalidKey) - }; - let beta_tag_fe = Scalar::::from(beta_tag); - let c_beta_tag = Paillier::encrypt_with_chosen_randomness( - alice_ek, - RawPlaintext::from(beta_tag), - &Randomness::from(randomness.clone()), - ); - - let b_bn = b.to_bigint(); - let b_c_a = Paillier::mul(alice_ek, RawCiphertext::from(m_a.c), RawPlaintext::from(b_bn)); - let c_b = Paillier::add(alice_ek, b_c_a, c_beta_tag); - let beta = Scalar::::zero() - &beta_tag_fe; - let dlog_proof_b = DLogProof::prove(b); - let dlog_proof_beta_tag = DLogProof::prove(&beta_tag_fe); - - Ok(( - Self { - c: c_b.0.clone().into_owned(), - b_proof: dlog_proof_b, - beta_tag_proof: dlog_proof_beta_tag, - }, - beta, - )) - } - - pub fn verify_proofs_get_alpha( - &self, - dk: &DecryptionKey, - a: &Scalar, - ) -> Result<(Scalar, BigInt), Error> { - let alice_share = Paillier::decrypt(dk, &RawCiphertext::from(self.c.clone())); - let g = Point::generator(); - let alpha = Scalar::::from(alice_share.0.as_ref()); - let g_alpha = g * α - let ba_btag = &self.b_proof.pk * a + &self.beta_tag_proof.pk; - if DLogProof::verify(&self.b_proof).is_ok() + pub fn b( + b: &Scalar, + alice_ek: &EncryptionKey, + m_a: MessageA, + dlog_statements: &[DLogStatement], + ) -> Result<(Self, Scalar, BigInt, BigInt), Error> { + let beta_tag = BigInt::sample_below(&alice_ek.n); + let randomness = BigInt::sample_below(&alice_ek.n); + let (m_b, beta) = MessageB::b_with_predefined_randomness( + b, + alice_ek, + m_a, + &randomness, + &beta_tag, + dlog_statements, + )?; + + Ok((m_b, beta, randomness, beta_tag)) + } + + pub fn b_with_predefined_randomness( + b: &Scalar, + alice_ek: &EncryptionKey, + m_a: MessageA, + randomness: &BigInt, + beta_tag: &BigInt, + dlog_statements: &[DLogStatement], + ) -> Result<(Self, Scalar), Error> { + if m_a.range_proofs.len() != dlog_statements.len() { + return Err(InvalidKey) + } + // verify proofs + if !m_a + .range_proofs + .iter() + .zip(dlog_statements) + .map(|(proof, dlog_statement)| { + proof.verify(&m_a.c, alice_ek, dlog_statement) + }) + .all(|x| x) + { + return Err(InvalidKey) + }; + let beta_tag_fe = Scalar::::from(beta_tag); + let c_beta_tag = Paillier::encrypt_with_chosen_randomness( + alice_ek, + RawPlaintext::from(beta_tag), + &Randomness::from(randomness.clone()), + ); + + let b_bn = b.to_bigint(); + let b_c_a = Paillier::mul( + alice_ek, + RawCiphertext::from(m_a.c), + RawPlaintext::from(b_bn), + ); + let c_b = Paillier::add(alice_ek, b_c_a, c_beta_tag); + let beta = Scalar::::zero() - &beta_tag_fe; + let dlog_proof_b = DLogProof::prove(b); + let dlog_proof_beta_tag = DLogProof::prove(&beta_tag_fe); + + Ok(( + Self { + c: c_b.0.clone().into_owned(), + b_proof: dlog_proof_b, + beta_tag_proof: dlog_proof_beta_tag, + }, + beta, + )) + } + + pub fn verify_proofs_get_alpha( + &self, + dk: &DecryptionKey, + a: &Scalar, + ) -> Result<(Scalar, BigInt), Error> { + let alice_share = + Paillier::decrypt(dk, &RawCiphertext::from(self.c.clone())); + let g = Point::generator(); + let alpha = Scalar::::from(alice_share.0.as_ref()); + let g_alpha = g * α + let ba_btag = &self.b_proof.pk * a + &self.beta_tag_proof.pk; + if DLogProof::verify(&self.b_proof).is_ok() && DLogProof::verify(&self.beta_tag_proof).is_ok() // we prove the correctness of the ciphertext using this check and the proof of knowledge of dlog of beta_tag && ba_btag == g_alpha - { - Ok((alpha, alice_share.0.into_owned())) - } else { - Err(InvalidKey) - } - } - - // another version, supporting PartyPrivate therefore binding mta to gg18. - // with the regular version mta can be used in general - pub fn verify_proofs_get_alpha_gg18( - &self, - private: &PartyPrivate, - a: &Scalar, - ) -> Result, Error> { - let alice_share = private.decrypt(self.c.clone()); - let g = Point::generator(); - let alpha = Scalar::::from(alice_share.0.as_ref()); - let g_alpha = g * α - let ba_btag = &self.b_proof.pk * a + &self.beta_tag_proof.pk; - - if DLogProof::verify(&self.b_proof).is_ok() && - DLogProof::verify(&self.beta_tag_proof).is_ok() && - ba_btag == g_alpha - { - Ok(alpha) - } else { - Err(InvalidKey) - } - } - - pub fn verify_b_against_public( - public_gb: &Point, - mta_gb: &Point, - ) -> bool { - public_gb == mta_gb - } + { + Ok((alpha, alice_share.0.into_owned())) + } else { + Err(InvalidKey) + } + } + + // another version, supporting PartyPrivate therefore binding mta to gg18. + // with the regular version mta can be used in general + pub fn verify_proofs_get_alpha_gg18( + &self, + private: &PartyPrivate, + a: &Scalar, + ) -> Result, Error> { + let alice_share = private.decrypt(self.c.clone()); + let g = Point::generator(); + let alpha = Scalar::::from(alice_share.0.as_ref()); + let g_alpha = g * α + let ba_btag = &self.b_proof.pk * a + &self.beta_tag_proof.pk; + + if DLogProof::verify(&self.b_proof).is_ok() && + DLogProof::verify(&self.beta_tag_proof).is_ok() && + ba_btag == g_alpha + { + Ok(alpha) + } else { + Err(InvalidKey) + } + } + + pub fn verify_b_against_public( + public_gb: &Point, + mta_gb: &Point, + ) -> bool { + public_gb == mta_gb + } } diff --git a/src/utilities/mta/range_proofs.rs b/src/utilities/mta/range_proofs.rs index fb894258..5401ac2c 100644 --- a/src/utilities/mta/range_proofs.rs +++ b/src/utilities/mta/range_proofs.rs @@ -6,14 +6,16 @@ //! Zero knowledge range proofs for MtA protocol are implemented here. //! Formal description can be found in Appendix A of https://eprint.iacr.org/2019/114.pdf //! There are some deviations from the original specification: -//! 1) In Bob's proofs `gamma` is sampled from `[0;q^2 * N]` and `tau` from `[0;q^3 * N_tilde]`. -//! 2) A non-interactive version is implemented, with challenge `e` computed via Fiat-Shamir. +//! 1) In Bob's proofs `gamma` is sampled from `[0;q^2 * N]` and `tau` from +//! `[0;q^3 * N_tilde]`. +//! 2) A non-interactive version is implemented, with challenge `e` computed via +//! Fiat-Shamir. use curv::{ - arithmetic::traits::*, - cryptographic_primitives::hashing::{Digest, DigestExt}, - elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, - BigInt, + arithmetic::traits::*, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, }; use sha2::Sha256; @@ -28,633 +30,685 @@ use zeroize::Zeroize; #[derive(Zeroize)] #[zeroize(drop)] struct AliceZkpRound1 { - alpha: BigInt, - beta: BigInt, - gamma: BigInt, - ro: BigInt, - z: BigInt, - u: BigInt, - w: BigInt, + alpha: BigInt, + beta: BigInt, + gamma: BigInt, + ro: BigInt, + z: BigInt, + u: BigInt, + w: BigInt, } impl AliceZkpRound1 { - fn from( - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - a: &BigInt, - q: &BigInt, - ) -> Self { - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - let N_tilde = &dlog_statement.N; - let alpha = BigInt::sample_below(&q.pow(3)); - let beta = BigInt::from_paillier_key(alice_ek); - let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let ro = BigInt::sample_below(&(q * N_tilde)); - let z = (BigInt::mod_pow(h1, a, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; - let u = ((alpha.borrow() * &alice_ek.n + 1) * - BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % - &alice_ek.nn; - let w = - (BigInt::mod_pow(h1, &alpha, N_tilde) * BigInt::mod_pow(h2, &gamma, N_tilde)) % N_tilde; - Self { alpha, beta, gamma, ro, z, u, w } - } + fn from( + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + a: &BigInt, + q: &BigInt, + ) -> Self { + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let N_tilde = &dlog_statement.N; + let alpha = BigInt::sample_below(&q.pow(3)); + let beta = BigInt::from_paillier_key(alice_ek); + let gamma = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let ro = BigInt::sample_below(&(q * N_tilde)); + let z = (BigInt::mod_pow(h1, a, N_tilde) * + BigInt::mod_pow(h2, &ro, N_tilde)) % + N_tilde; + let u = ((alpha.borrow() * &alice_ek.n + 1) * + BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % + &alice_ek.nn; + let w = (BigInt::mod_pow(h1, &alpha, N_tilde) * + BigInt::mod_pow(h2, &gamma, N_tilde)) % + N_tilde; + Self { alpha, beta, gamma, ro, z, u, w } + } } /// Represents the second round of the interactive version of the proof struct AliceZkpRound2 { - s: BigInt, - s1: BigInt, - s2: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, } impl AliceZkpRound2 { - fn from( - alice_ek: &EncryptionKey, - round1: &AliceZkpRound1, - e: &BigInt, - a: &BigInt, - r: &BigInt, - ) -> Self { - Self { - s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, - s1: (e * a) + round1.alpha.borrow(), - s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), - } - } + fn from( + alice_ek: &EncryptionKey, + round1: &AliceZkpRound1, + e: &BigInt, + a: &BigInt, + r: &BigInt, + ) -> Self { + Self { + s: (BigInt::mod_pow(r, e, &alice_ek.n) * round1.beta.borrow()) % + &alice_ek.n, + s1: (e * a) + round1.alpha.borrow(), + s2: (e * round1.ro.borrow()) + round1.gamma.borrow(), + } + } } /// Alice's proof #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AliceProof { - z: BigInt, - e: BigInt, - s: BigInt, - s1: BigInt, - s2: BigInt, + z: BigInt, + e: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, } impl AliceProof { - /// verify Alice's proof using the proof and public keys - pub fn verify( - &self, - cipher: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - ) -> bool { - let N = &alice_ek.n; - let NN = &alice_ek.nn; - let N_tilde = &dlog_statement.N; - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - let Gen = alice_ek.n.borrow() + 1; - - if self.s1 > Scalar::::group_order().pow(3) { - return false - } - - let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); - let z_e_inv = match z_e_inv { - // z must be invertible, yet the check is done here - None => return false, - Some(c) => c, - }; - - let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) * - BigInt::mod_pow(h2, &self.s2, N_tilde) * - z_e_inv) % N_tilde; - - let gs1 = (self.s1.borrow() * N + 1) % NN; - let cipher_e_inv = BigInt::mod_inv(&BigInt::mod_pow(cipher, &self.e, NN), NN); - let cipher_e_inv = match cipher_e_inv { - None => return false, - Some(c) => c, - }; - - let u = (gs1 * BigInt::mod_pow(&self.s, N, NN) * cipher_e_inv) % NN; - - let e = Sha256::new() - .chain_bigint(N) - .chain_bigint(&Gen) - .chain_bigint(cipher) - .chain_bigint(&self.z) - .chain_bigint(&u) - .chain_bigint(&w) - .result_bigint(); - if e != self.e { - return false - } - - true - } - /// Create the proof using Alice's Paillier private keys and public ZKP setup. - /// Requires randomness used for encrypting Alice's secret a. - /// It is assumed that secp256k1 curve is used. - pub fn generate( - a: &BigInt, - cipher: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - r: &BigInt, - ) -> Self { - let round1 = - AliceZkpRound1::from(alice_ek, dlog_statement, a, Scalar::::group_order()); - - let Gen = alice_ek.n.borrow() + 1; - let e = Sha256::new() - .chain_bigint(&alice_ek.n) - .chain_bigint(&Gen) - .chain_bigint(cipher) - .chain_bigint(&round1.z) - .chain_bigint(&round1.u) - .chain_bigint(&round1.w) - .result_bigint(); - - let round2 = AliceZkpRound2::from(alice_ek, &round1, &e, a, r); - - Self { z: round1.z.clone(), e, s: round2.s, s1: round2.s1, s2: round2.s2 } - } + /// verify Alice's proof using the proof and public keys + pub fn verify( + &self, + cipher: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + ) -> bool { + let N = &alice_ek.n; + let NN = &alice_ek.nn; + let N_tilde = &dlog_statement.N; + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let Gen = alice_ek.n.borrow() + 1; + + if self.s1 > Scalar::::group_order().pow(3) { + return false + } + + let z_e_inv = BigInt::mod_inv( + &BigInt::mod_pow(&self.z, &self.e, N_tilde), + N_tilde, + ); + let z_e_inv = match z_e_inv { + // z must be invertible, yet the check is done here + None => return false, + Some(c) => c, + }; + + let w = (BigInt::mod_pow(h1, &self.s1, N_tilde) * + BigInt::mod_pow(h2, &self.s2, N_tilde) * + z_e_inv) % + N_tilde; + + let gs1 = (self.s1.borrow() * N + 1) % NN; + let cipher_e_inv = + BigInt::mod_inv(&BigInt::mod_pow(cipher, &self.e, NN), NN); + let cipher_e_inv = match cipher_e_inv { + None => return false, + Some(c) => c, + }; + + let u = (gs1 * BigInt::mod_pow(&self.s, N, NN) * cipher_e_inv) % NN; + + let e = Sha256::new() + .chain_bigint(N) + .chain_bigint(&Gen) + .chain_bigint(cipher) + .chain_bigint(&self.z) + .chain_bigint(&u) + .chain_bigint(&w) + .result_bigint(); + if e != self.e { + return false + } + + true + } + /// Create the proof using Alice's Paillier private keys and public ZKP + /// setup. Requires randomness used for encrypting Alice's secret a. + /// It is assumed that secp256k1 curve is used. + pub fn generate( + a: &BigInt, + cipher: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &BigInt, + ) -> Self { + let round1 = AliceZkpRound1::from( + alice_ek, + dlog_statement, + a, + Scalar::::group_order(), + ); + + let Gen = alice_ek.n.borrow() + 1; + let e = Sha256::new() + .chain_bigint(&alice_ek.n) + .chain_bigint(&Gen) + .chain_bigint(cipher) + .chain_bigint(&round1.z) + .chain_bigint(&round1.u) + .chain_bigint(&round1.w) + .result_bigint(); + + let round2 = AliceZkpRound2::from(alice_ek, &round1, &e, a, r); + + Self { + z: round1.z.clone(), + e, + s: round2.s, + s1: round2.s1, + s2: round2.s2, + } + } } /// Represents first round of the interactive version of the proof #[derive(Zeroize)] #[zeroize(drop)] struct BobZkpRound1 { - pub alpha: BigInt, - pub beta: BigInt, - pub gamma: BigInt, - pub ro: BigInt, - pub ro_prim: BigInt, - pub sigma: BigInt, - pub tau: BigInt, - pub z: BigInt, - pub z_prim: BigInt, - pub t: BigInt, - pub w: BigInt, - pub v: BigInt, + pub alpha: BigInt, + pub beta: BigInt, + pub gamma: BigInt, + pub ro: BigInt, + pub ro_prim: BigInt, + pub sigma: BigInt, + pub tau: BigInt, + pub z: BigInt, + pub z_prim: BigInt, + pub t: BigInt, + pub w: BigInt, + pub v: BigInt, } impl BobZkpRound1 { - /// `b` - Bob's secret - /// `beta_prim` - randomly chosen in `MtA` by Bob - /// `a_encrypted` - Alice's secret encrypted by Alice - fn from( - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - b: &Scalar, - beta_prim: &BigInt, - a_encrypted: &BigInt, - q: &BigInt, - ) -> Self { - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - let N_tilde = &dlog_statement.N; - let b_bn = b.to_bigint(); - - let alpha = BigInt::sample_below(&q.pow(3)); - let beta = BigInt::from_paillier_key(alice_ek); - let gamma = BigInt::sample_below(&(q.pow(2) * &alice_ek.n)); - let ro = BigInt::sample_below(&(q * N_tilde)); - let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let sigma = BigInt::sample_below(&(q * N_tilde)); - let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); - let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * BigInt::mod_pow(h2, &ro, N_tilde)) % N_tilde; - let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) * - BigInt::mod_pow(h2, &ro_prim, N_tilde)) % - N_tilde; - let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * BigInt::mod_pow(h2, &sigma, N_tilde)) % - N_tilde; - let w = - (BigInt::mod_pow(h1, &gamma, N_tilde) * BigInt::mod_pow(h2, &tau, N_tilde)) % N_tilde; - let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) * - (gamma.borrow() * &alice_ek.n + 1) * - BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % - &alice_ek.nn; - Self { alpha, beta, gamma, ro, ro_prim, sigma, tau, z, z_prim, t, w, v } - } + /// `b` - Bob's secret + /// `beta_prim` - randomly chosen in `MtA` by Bob + /// `a_encrypted` - Alice's secret encrypted by Alice + fn from( + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + b: &Scalar, + beta_prim: &BigInt, + a_encrypted: &BigInt, + q: &BigInt, + ) -> Self { + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + let N_tilde = &dlog_statement.N; + let b_bn = b.to_bigint(); + + let alpha = BigInt::sample_below(&q.pow(3)); + let beta = BigInt::from_paillier_key(alice_ek); + let gamma = BigInt::sample_below(&(q.pow(2) * &alice_ek.n)); + let ro = BigInt::sample_below(&(q * N_tilde)); + let ro_prim = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let sigma = BigInt::sample_below(&(q * N_tilde)); + let tau = BigInt::sample_below(&(q.pow(3) * N_tilde)); + let z = (BigInt::mod_pow(h1, &b_bn, N_tilde) * + BigInt::mod_pow(h2, &ro, N_tilde)) % + N_tilde; + let z_prim = (BigInt::mod_pow(h1, &alpha, N_tilde) * + BigInt::mod_pow(h2, &ro_prim, N_tilde)) % + N_tilde; + let t = (BigInt::mod_pow(h1, beta_prim, N_tilde) * + BigInt::mod_pow(h2, &sigma, N_tilde)) % + N_tilde; + let w = (BigInt::mod_pow(h1, &gamma, N_tilde) * + BigInt::mod_pow(h2, &tau, N_tilde)) % + N_tilde; + let v = (BigInt::mod_pow(a_encrypted, &alpha, &alice_ek.nn) * + (gamma.borrow() * &alice_ek.n + 1) * + BigInt::mod_pow(&beta, &alice_ek.n, &alice_ek.nn)) % + &alice_ek.nn; + Self { alpha, beta, gamma, ro, ro_prim, sigma, tau, z, z_prim, t, w, v } + } } /// represents second round of the interactive version of the proof struct BobZkpRound2 { - pub s: BigInt, - pub s1: BigInt, - pub s2: BigInt, - pub t1: BigInt, - pub t2: BigInt, + pub s: BigInt, + pub s1: BigInt, + pub s2: BigInt, + pub t1: BigInt, + pub t2: BigInt, } impl BobZkpRound2 { - /// `e` - the challenge in interactive ZKP, the hash in non-interactive ZKP - /// `b` - Bob's secret - /// `beta_prim` - randomly chosen in `MtA` by Bob - /// `r` - randomness used by Bob on Alice's public Paillier key to encrypt `beta_prim` in `MtA` - fn from( - alice_ek: &EncryptionKey, - round1: &BobZkpRound1, - e: &BigInt, - b: &Scalar, - beta_prim: &BigInt, - r: &Randomness, - ) -> Self { - let b_bn = b.to_bigint(); - Self { - s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * round1.beta.borrow()) % &alice_ek.n, - s1: (e * b_bn) + round1.alpha.borrow(), - s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), - t1: (e * beta_prim) + round1.gamma.borrow(), - t2: (e * round1.sigma.borrow()) + round1.tau.borrow(), - } - } + /// `e` - the challenge in interactive ZKP, the hash in non-interactive ZKP + /// `b` - Bob's secret + /// `beta_prim` - randomly chosen in `MtA` by Bob + /// `r` - randomness used by Bob on Alice's public Paillier key to encrypt + /// `beta_prim` in `MtA` + fn from( + alice_ek: &EncryptionKey, + round1: &BobZkpRound1, + e: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + r: &Randomness, + ) -> Self { + let b_bn = b.to_bigint(); + Self { + s: (BigInt::mod_pow(r.0.borrow(), e, &alice_ek.n) * + round1.beta.borrow()) % + &alice_ek.n, + s1: (e * b_bn) + round1.alpha.borrow(), + s2: (e * round1.ro.borrow()) + round1.ro_prim.borrow(), + t1: (e * beta_prim) + round1.gamma.borrow(), + t2: (e * round1.sigma.borrow()) + round1.tau.borrow(), + } + } } /// Additional fields in Bob's proof if MtA is run with check pub struct BobCheck { - u: Point, - X: Point, + u: Point, + X: Point, } /// Bob's regular proof #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub struct BobProof { - t: BigInt, - z: BigInt, - e: BigInt, - s: BigInt, - s1: BigInt, - s2: BigInt, - t1: BigInt, - t2: BigInt, + t: BigInt, + z: BigInt, + e: BigInt, + s: BigInt, + s1: BigInt, + s2: BigInt, + t1: BigInt, + t2: BigInt, } #[allow(clippy::too_many_arguments)] impl BobProof { - pub fn verify( - &self, - a_enc: &BigInt, - mta_avc_out: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - check: Option<&BobCheck>, - ) -> bool { - let N = &alice_ek.n; - let NN = &alice_ek.nn; - let N_tilde = &dlog_statement.N; - let h1 = &dlog_statement.g; - let h2 = &dlog_statement.ni; - - if self.s1 > Scalar::::group_order().pow(3) { - return false - } - - let z_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.z, &self.e, N_tilde), N_tilde); - let z_e_inv = match z_e_inv { - // z must be invertible, yet the check is done here - None => return false, - Some(c) => c, - }; - - let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) * - BigInt::mod_pow(h2, &self.s2, N_tilde) * - z_e_inv) % N_tilde; - - let mta_e_inv = BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); - let mta_e_inv = match mta_e_inv { - None => return false, - Some(c) => c, - }; - - let v = (BigInt::mod_pow(a_enc, &self.s1, NN) * - BigInt::mod_pow(&self.s, N, NN) * - (self.t1.borrow() * N + 1) * - mta_e_inv) % NN; - - let t_e_inv = BigInt::mod_inv(&BigInt::mod_pow(&self.t, &self.e, N_tilde), N_tilde); - let t_e_inv = match t_e_inv { - None => return false, - Some(c) => c, - }; - - let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) * - BigInt::mod_pow(h2, &self.t2, N_tilde) * - t_e_inv) % N_tilde; - - let Gen = alice_ek.n.borrow() + 1; - let mut values_to_hash = - vec![&alice_ek.n, &Gen, a_enc, mta_avc_out, &self.z, &z_prim, &self.t, &v, &w]; - let e = match check { - Some(_) => { - let X_x_coor = check.unwrap().X.x_coord().unwrap(); - values_to_hash.push(&X_x_coor); - let X_y_coor = check.unwrap().X.y_coord().unwrap(); - values_to_hash.push(&X_y_coor); - let u_x_coor = check.unwrap().u.x_coord().unwrap(); - values_to_hash.push(&u_x_coor); - let u_y_coor = check.unwrap().u.y_coord().unwrap(); - values_to_hash.push(&u_y_coor); - values_to_hash - .into_iter() - .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) - .result_bigint() - }, - None => values_to_hash - .into_iter() - .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) - .result_bigint(), - }; - - if e != self.e { - return false - } - - true - } - - pub fn generate( - a_encrypted: &BigInt, - mta_encrypted: &BigInt, - b: &Scalar, - beta_prim: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - r: &Randomness, - check: bool, - ) -> (BobProof, Option>) { - let round1 = BobZkpRound1::from( - alice_ek, - dlog_statement, - b, - beta_prim, - a_encrypted, - Scalar::::group_order(), - ); - - let Gen = alice_ek.n.borrow() + 1; - let mut values_to_hash = vec![ - &alice_ek.n, - &Gen, - a_encrypted, - mta_encrypted, - &round1.z, - &round1.z_prim, - &round1.t, - &round1.v, - &round1.w, - ]; - let mut check_u = None; - let e = if check { - let (X, u) = { - let ec_gen = Point::generator(); - let alpha = Scalar::::from(&round1.alpha); - (ec_gen * b, ec_gen * alpha) - }; - check_u = Some(u.clone()); - let X_x_coor = X.x_coord().unwrap(); - values_to_hash.push(&X_x_coor); - let X_y_coor = X.y_coord().unwrap(); - values_to_hash.push(&X_y_coor); - let u_x_coor = u.x_coord().unwrap(); - values_to_hash.push(&u_x_coor); - let u_y_coor = u.y_coord().unwrap(); - values_to_hash.push(&u_y_coor); - values_to_hash - .into_iter() - .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) - .result_bigint() - } else { - values_to_hash - .into_iter() - .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) - .result_bigint() - }; - - let round2 = BobZkpRound2::from(alice_ek, &round1, &e, b, beta_prim, r); - - ( - BobProof { - t: round1.t.clone(), - z: round1.z.clone(), - e, - s: round2.s, - s1: round2.s1, - s2: round2.s2, - t1: round2.t1, - t2: round2.t2, - }, - check_u, - ) - } + pub fn verify( + &self, + a_enc: &BigInt, + mta_avc_out: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + check: Option<&BobCheck>, + ) -> bool { + let N = &alice_ek.n; + let NN = &alice_ek.nn; + let N_tilde = &dlog_statement.N; + let h1 = &dlog_statement.g; + let h2 = &dlog_statement.ni; + + if self.s1 > Scalar::::group_order().pow(3) { + return false + } + + let z_e_inv = BigInt::mod_inv( + &BigInt::mod_pow(&self.z, &self.e, N_tilde), + N_tilde, + ); + let z_e_inv = match z_e_inv { + // z must be invertible, yet the check is done here + None => return false, + Some(c) => c, + }; + + let z_prim = (BigInt::mod_pow(h1, &self.s1, N_tilde) * + BigInt::mod_pow(h2, &self.s2, N_tilde) * + z_e_inv) % + N_tilde; + + let mta_e_inv = + BigInt::mod_inv(&BigInt::mod_pow(mta_avc_out, &self.e, NN), NN); + let mta_e_inv = match mta_e_inv { + None => return false, + Some(c) => c, + }; + + let v = (BigInt::mod_pow(a_enc, &self.s1, NN) * + BigInt::mod_pow(&self.s, N, NN) * + (self.t1.borrow() * N + 1) * + mta_e_inv) % + NN; + + let t_e_inv = BigInt::mod_inv( + &BigInt::mod_pow(&self.t, &self.e, N_tilde), + N_tilde, + ); + let t_e_inv = match t_e_inv { + None => return false, + Some(c) => c, + }; + + let w = (BigInt::mod_pow(h1, &self.t1, N_tilde) * + BigInt::mod_pow(h2, &self.t2, N_tilde) * + t_e_inv) % + N_tilde; + + let Gen = alice_ek.n.borrow() + 1; + let mut values_to_hash = vec![ + &alice_ek.n, + &Gen, + a_enc, + mta_avc_out, + &self.z, + &z_prim, + &self.t, + &v, + &w, + ]; + let e = match check { + Some(_) => { + let X_x_coor = check.unwrap().X.x_coord().unwrap(); + values_to_hash.push(&X_x_coor); + let X_y_coor = check.unwrap().X.y_coord().unwrap(); + values_to_hash.push(&X_y_coor); + let u_x_coor = check.unwrap().u.x_coord().unwrap(); + values_to_hash.push(&u_x_coor); + let u_y_coor = check.unwrap().u.y_coord().unwrap(); + values_to_hash.push(&u_y_coor); + values_to_hash + .into_iter() + .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) + .result_bigint() + }, + None => values_to_hash + .into_iter() + .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) + .result_bigint(), + }; + + if e != self.e { + return false + } + + true + } + + pub fn generate( + a_encrypted: &BigInt, + mta_encrypted: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &Randomness, + check: bool, + ) -> (BobProof, Option>) { + let round1 = BobZkpRound1::from( + alice_ek, + dlog_statement, + b, + beta_prim, + a_encrypted, + Scalar::::group_order(), + ); + + let Gen = alice_ek.n.borrow() + 1; + let mut values_to_hash = vec![ + &alice_ek.n, + &Gen, + a_encrypted, + mta_encrypted, + &round1.z, + &round1.z_prim, + &round1.t, + &round1.v, + &round1.w, + ]; + let mut check_u = None; + let e = if check { + let (X, u) = { + let ec_gen = Point::generator(); + let alpha = Scalar::::from(&round1.alpha); + (ec_gen * b, ec_gen * alpha) + }; + check_u = Some(u.clone()); + let X_x_coor = X.x_coord().unwrap(); + values_to_hash.push(&X_x_coor); + let X_y_coor = X.y_coord().unwrap(); + values_to_hash.push(&X_y_coor); + let u_x_coor = u.x_coord().unwrap(); + values_to_hash.push(&u_x_coor); + let u_y_coor = u.y_coord().unwrap(); + values_to_hash.push(&u_y_coor); + values_to_hash + .into_iter() + .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) + .result_bigint() + } else { + values_to_hash + .into_iter() + .fold(Sha256::new(), |acc, b| acc.chain_bigint(b)) + .result_bigint() + }; + + let round2 = BobZkpRound2::from(alice_ek, &round1, &e, b, beta_prim, r); + + ( + BobProof { + t: round1.t.clone(), + z: round1.z.clone(), + e, + s: round2.s, + s1: round2.s1, + s2: round2.s2, + t1: round2.t1, + t2: round2.t2, + }, + check_u, + ) + } } /// Bob's extended proof, adds the knowledge of $`B = g^b \in \mathcal{G}`$ #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub struct BobProofExt { - proof: BobProof, - u: Point, + proof: BobProof, + u: Point, } #[allow(clippy::too_many_arguments)] impl BobProofExt { - pub fn verify( - &self, - a_enc: &BigInt, - mta_avc_out: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - X: &Point, - ) -> bool { - // check basic proof first - if !self.proof.verify( - a_enc, - mta_avc_out, - alice_ek, - dlog_statement, - Some(&BobCheck { u: self.u.clone(), X: X.clone() }), - ) { - return false - } - - // fiddle with EC points - let (x1, x2) = { - let ec_gen = Point::generator(); - let s1 = Scalar::::from(&self.proof.s1); - let e = Scalar::::from(&self.proof.e); - (ec_gen * s1, (X * &e) + &self.u) - }; - - if x1 != x2 { - return false - } - - true - } + pub fn verify( + &self, + a_enc: &BigInt, + mta_avc_out: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + X: &Point, + ) -> bool { + // check basic proof first + if !self.proof.verify( + a_enc, + mta_avc_out, + alice_ek, + dlog_statement, + Some(&BobCheck { u: self.u.clone(), X: X.clone() }), + ) { + return false + } + + // fiddle with EC points + let (x1, x2) = { + let ec_gen = Point::generator(); + let s1 = Scalar::::from(&self.proof.s1); + let e = Scalar::::from(&self.proof.e); + (ec_gen * s1, (X * &e) + &self.u) + }; + + if x1 != x2 { + return false + } + + true + } } /// sample random value of an element of a multiplicative group pub trait SampleFromMultiplicativeGroup { - fn from_modulo(N: &BigInt) -> BigInt; - fn from_paillier_key(ek: &EncryptionKey) -> BigInt; + fn from_modulo(N: &BigInt) -> BigInt; + fn from_paillier_key(ek: &EncryptionKey) -> BigInt; } impl SampleFromMultiplicativeGroup for BigInt { - fn from_modulo(N: &BigInt) -> BigInt { - let One = BigInt::one(); - loop { - let r = Self::sample_below(N); - if r.gcd(N) == One { - return r - } - } - } - - fn from_paillier_key(ek: &EncryptionKey) -> BigInt { - Self::from_modulo(ek.n.borrow()) - } + fn from_modulo(N: &BigInt) -> BigInt { + let One = BigInt::one(); + loop { + let r = Self::sample_below(N); + if r.gcd(N) == One { + return r + } + } + } + + fn from_paillier_key(ek: &EncryptionKey) -> BigInt { + Self::from_modulo(ek.n.borrow()) + } } #[cfg(test)] pub(crate) mod tests { - use super::*; - use paillier::{ - traits::{Encrypt, EncryptWithChosenRandomness, KeyGeneration}, - Add, DecryptionKey, Mul, Paillier, RawCiphertext, RawPlaintext, - }; - - fn generate( - a_encrypted: &BigInt, - mta_encrypted: &BigInt, - b: &Scalar, - beta_prim: &BigInt, - alice_ek: &EncryptionKey, - dlog_statement: &DLogStatement, - r: &Randomness, - ) -> BobProofExt { - // proving a basic proof (with modified hash) - let (bob_proof, u) = BobProof::generate( - a_encrypted, - mta_encrypted, - b, - beta_prim, - alice_ek, - dlog_statement, - r, - true, - ); - - BobProofExt { proof: bob_proof, u: u.unwrap() } - } - - pub(crate) fn generate_init() -> (DLogStatement, EncryptionKey, DecryptionKey) { - let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&ek_tilde.n); - let (xhi, _) = loop { - let xhi_ = BigInt::sample_below(&phi); - match BigInt::mod_inv(&xhi_, &phi) { - Some(inv) => break (xhi_, inv), - None => continue, - } - }; - let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); - - let (ek, dk) = Paillier::keypair().keys(); - let dlog_statement = DLogStatement { g: h1, ni: h2, N: ek_tilde.n }; - (dlog_statement, ek, dk) - } - - #[test] - fn alice_zkp() { - let (dlog_statement, ek, _) = generate_init(); - - // Alice's secret value - let a = Scalar::::random().to_bigint(); - let r = BigInt::from_paillier_key(&ek); - let cipher = Paillier::encrypt_with_chosen_randomness( - &ek, - RawPlaintext::from(a.clone()), - &Randomness::from(&r), - ) - .0 - .clone() - .into_owned(); - - let alice_proof = AliceProof::generate(&a, &cipher, &ek, &dlog_statement, &r); - - assert!(alice_proof.verify(&cipher, &ek, &dlog_statement)); - } - - #[test] - fn bob_zkp() { - let (dlog_statement, ek, _) = generate_init(); - - (0..5).for_each(|_| { - let alice_public_key = &ek; - - // run MtA protocol with different inputs - (0..5).for_each(|_| { - // Simulate Alice - let a = Scalar::::random().to_bigint(); - let encrypted_a = Paillier::encrypt(alice_public_key, RawPlaintext::from(a)) - .0 - .clone() - .into_owned(); - - // Bob follows MtA - let b = Scalar::::random(); - // E(a) * b - let b_times_enc_a = Paillier::mul( - alice_public_key, - RawCiphertext::from(encrypted_a.clone()), - RawPlaintext::from(&b.to_bigint()), - ); - let beta_prim = BigInt::sample_below(&alice_public_key.n); - let r = Randomness::sample(alice_public_key); - let enc_beta_prim = Paillier::encrypt_with_chosen_randomness( - alice_public_key, - RawPlaintext::from(&beta_prim), - &r, - ); - - let mta_out = Paillier::add(alice_public_key, b_times_enc_a, enc_beta_prim); - - let (bob_proof, _) = BobProof::generate( - &encrypted_a, - &mta_out.0.clone(), - &b, - &beta_prim, - alice_public_key, - &dlog_statement, - &r, - false, - ); - assert!(bob_proof.verify( - &encrypted_a, - &mta_out.0.clone(), - alice_public_key, - &dlog_statement, - None - )); - - // Bob follows MtAwc - let ec_gen = Point::generator(); - let X = ec_gen * &b; - let bob_proof = generate( - &encrypted_a, - &mta_out.0.clone(), - &b, - &beta_prim, - alice_public_key, - &dlog_statement, - &r, - ); - assert!(bob_proof.verify( - &encrypted_a, - &mta_out.0.clone(), - alice_public_key, - &dlog_statement, - &X - )); - }); - }); - } + use super::*; + use paillier::{ + traits::{Encrypt, EncryptWithChosenRandomness, KeyGeneration}, + Add, DecryptionKey, Mul, Paillier, RawCiphertext, RawPlaintext, + }; + + fn generate( + a_encrypted: &BigInt, + mta_encrypted: &BigInt, + b: &Scalar, + beta_prim: &BigInt, + alice_ek: &EncryptionKey, + dlog_statement: &DLogStatement, + r: &Randomness, + ) -> BobProofExt { + // proving a basic proof (with modified hash) + let (bob_proof, u) = BobProof::generate( + a_encrypted, + mta_encrypted, + b, + beta_prim, + alice_ek, + dlog_statement, + r, + true, + ); + + BobProofExt { proof: bob_proof, u: u.unwrap() } + } + + pub(crate) fn generate_init( + ) -> (DLogStatement, EncryptionKey, DecryptionKey) { + let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&ek_tilde.n); + let (xhi, _) = loop { + let xhi_ = BigInt::sample_below(&phi); + match BigInt::mod_inv(&xhi_, &phi) { + Some(inv) => break (xhi_, inv), + None => continue, + } + }; + let h2 = BigInt::mod_pow(&h1, &xhi, &ek_tilde.n); + + let (ek, dk) = Paillier::keypair().keys(); + let dlog_statement = DLogStatement { g: h1, ni: h2, N: ek_tilde.n }; + (dlog_statement, ek, dk) + } + + #[test] + fn alice_zkp() { + let (dlog_statement, ek, _) = generate_init(); + + // Alice's secret value + let a = Scalar::::random().to_bigint(); + let r = BigInt::from_paillier_key(&ek); + let cipher = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(a.clone()), + &Randomness::from(&r), + ) + .0 + .clone() + .into_owned(); + + let alice_proof = + AliceProof::generate(&a, &cipher, &ek, &dlog_statement, &r); + + assert!(alice_proof.verify(&cipher, &ek, &dlog_statement)); + } + + #[test] + fn bob_zkp() { + let (dlog_statement, ek, _) = generate_init(); + + (0..5).for_each(|_| { + let alice_public_key = &ek; + + // run MtA protocol with different inputs + (0..5).for_each(|_| { + // Simulate Alice + let a = Scalar::::random().to_bigint(); + let encrypted_a = + Paillier::encrypt(alice_public_key, RawPlaintext::from(a)) + .0 + .clone() + .into_owned(); + + // Bob follows MtA + let b = Scalar::::random(); + // E(a) * b + let b_times_enc_a = Paillier::mul( + alice_public_key, + RawCiphertext::from(encrypted_a.clone()), + RawPlaintext::from(&b.to_bigint()), + ); + let beta_prim = BigInt::sample_below(&alice_public_key.n); + let r = Randomness::sample(alice_public_key); + let enc_beta_prim = Paillier::encrypt_with_chosen_randomness( + alice_public_key, + RawPlaintext::from(&beta_prim), + &r, + ); + + let mta_out = Paillier::add( + alice_public_key, + b_times_enc_a, + enc_beta_prim, + ); + + let (bob_proof, _) = BobProof::generate( + &encrypted_a, + &mta_out.0.clone(), + &b, + &beta_prim, + alice_public_key, + &dlog_statement, + &r, + false, + ); + assert!(bob_proof.verify( + &encrypted_a, + &mta_out.0.clone(), + alice_public_key, + &dlog_statement, + None + )); + + // Bob follows MtAwc + let ec_gen = Point::generator(); + let X = ec_gen * &b; + let bob_proof = generate( + &encrypted_a, + &mta_out.0.clone(), + &b, + &beta_prim, + alice_public_key, + &dlog_statement, + &r, + ); + assert!(bob_proof.verify( + &encrypted_a, + &mta_out.0.clone(), + alice_public_key, + &dlog_statement, + &X + )); + }); + }); + } } diff --git a/src/utilities/mta/test.rs b/src/utilities/mta/test.rs index edd291d3..c2e6d635 100644 --- a/src/utilities/mta/test.rs +++ b/src/utilities/mta/test.rs @@ -1,16 +1,22 @@ -use crate::utilities::mta::{range_proofs::tests::generate_init, MessageA, MessageB}; +use crate::utilities::mta::{ + range_proofs::tests::generate_init, MessageA, MessageB, +}; use curv::elliptic::curves::{secp256_k1::Secp256k1, Scalar}; #[test] fn test_mta() { - let alice_input = Scalar::::random(); - let (dlog_statement, ek_alice, dk_alice) = generate_init(); - let bob_input = Scalar::::random(); - let (m_a, _) = MessageA::a(&alice_input, &ek_alice, &[dlog_statement.clone()]); - let (m_b, beta, _, _) = MessageB::b(&bob_input, &ek_alice, m_a, &[dlog_statement]).unwrap(); - let alpha = m_b.verify_proofs_get_alpha(&dk_alice, &alice_input).expect("wrong dlog or m_b"); + let alice_input = Scalar::::random(); + let (dlog_statement, ek_alice, dk_alice) = generate_init(); + let bob_input = Scalar::::random(); + let (m_a, _) = + MessageA::a(&alice_input, &ek_alice, &[dlog_statement.clone()]); + let (m_b, beta, _, _) = + MessageB::b(&bob_input, &ek_alice, m_a, &[dlog_statement]).unwrap(); + let alpha = m_b + .verify_proofs_get_alpha(&dk_alice, &alice_input) + .expect("wrong dlog or m_b"); - let left = alpha.0 + beta; - let right = alice_input * bob_input; - assert_eq!(left, right); + let left = alpha.0 + beta; + let right = alice_input * bob_input; + assert_eq!(left, right); } diff --git a/src/utilities/mul/mod.rs b/src/utilities/mul/mod.rs index 22d42e1f..3e22555e 100644 --- a/src/utilities/mul/mod.rs +++ b/src/utilities/mul/mod.rs @@ -1,29 +1,32 @@ #![allow(non_snake_case)] /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies + Copyright 2022 by Webb Technologies - This file is part of CGGMP Threshold ECDSA library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of CGGMP Threshold ECDSA library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - CGGMP Threshold ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + CGGMP Threshold ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ use super::sample_relatively_prime_integer; use crate::{utilities::mod_pow_with_negative, Error}; use curv::{ - arithmetic::{traits::*, Modulo}, - cryptographic_primitives::hashing::{Digest, DigestExt}, - elliptic::curves::Curve, - BigInt, + arithmetic::{traits::*, Modulo}, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::Curve, + BigInt, +}; +use paillier::{ + EncryptWithChosenRandomness, EncryptionKey, Paillier, Randomness, + RawPlaintext, }; -use paillier::{EncryptWithChosenRandomness, EncryptionKey, Paillier, Randomness, RawPlaintext}; use rand::Rng; use rand_chacha::{rand_core::SeedableRng, ChaChaRng}; use serde::{Deserialize, Serialize}; @@ -31,200 +34,217 @@ use std::marker::PhantomData; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierMulStatement { - pub N: BigInt, - pub NN: BigInt, - pub C: BigInt, - pub Y: BigInt, - pub X: BigInt, - pub ek_prover: EncryptionKey, - pub phantom: PhantomData<(E, H)>, + pub N: BigInt, + pub NN: BigInt, + pub C: BigInt, + pub Y: BigInt, + pub X: BigInt, + pub ek_prover: EncryptionKey, + pub phantom: PhantomData<(E, H)>, } pub struct PaillierMulWitness { - x: BigInt, - rho: BigInt, - rho_x: BigInt, - phantom: PhantomData<(E, H)>, + x: BigInt, + rho: BigInt, + rho_x: BigInt, + phantom: PhantomData<(E, H)>, } impl PaillierMulWitness { - pub fn new(x: BigInt, rho: BigInt, rho_x: BigInt) -> Self { - PaillierMulWitness { x, rho, rho_x, phantom: PhantomData } - } + pub fn new(x: BigInt, rho: BigInt, rho_x: BigInt) -> Self { + PaillierMulWitness { x, rho, rho_x, phantom: PhantomData } + } } impl PaillierMulStatement { - #[allow(clippy::too_many_arguments)] - pub fn generate( - rho: BigInt, - rho_x: BigInt, - prover: EncryptionKey, - Y: BigInt, - ) -> (Self, PaillierMulWitness) { - let ek_prover = prover.clone(); - // x <- Z_N - let x = BigInt::sample_below(&prover.n); - // X = (1 + N)^x * rho_x^N mod N^2 - let X = Paillier::encrypt_with_chosen_randomness( - &ek_prover, - RawPlaintext::from(x.clone()), - &Randomness::from(rho_x.clone()), - ); - // C = Y^x * rho^N mod N^2 - let C = BigInt::mod_mul( - &BigInt::mod_pow(&Y, &x, &prover.nn), - &BigInt::mod_pow(&rho, &prover.n, &prover.nn), - &prover.nn, - ); - - ( - Self { - N: prover.n, - NN: prover.nn, - C, - Y, - X: X.clone().into(), - ek_prover, - phantom: PhantomData, - }, - PaillierMulWitness { x, rho, rho_x, phantom: PhantomData }, - ) - } + #[allow(clippy::too_many_arguments)] + pub fn generate( + rho: BigInt, + rho_x: BigInt, + prover: EncryptionKey, + Y: BigInt, + ) -> (Self, PaillierMulWitness) { + let ek_prover = prover.clone(); + // x <- Z_N + let x = BigInt::sample_below(&prover.n); + // X = (1 + N)^x * rho_x^N mod N^2 + let X = Paillier::encrypt_with_chosen_randomness( + &ek_prover, + RawPlaintext::from(x.clone()), + &Randomness::from(rho_x.clone()), + ); + // C = Y^x * rho^N mod N^2 + let C = BigInt::mod_mul( + &BigInt::mod_pow(&Y, &x, &prover.nn), + &BigInt::mod_pow(&rho, &prover.n, &prover.nn), + &prover.nn, + ); + + ( + Self { + N: prover.n, + NN: prover.nn, + C, + Y, + X: X.clone().into(), + ek_prover, + phantom: PhantomData, + }, + PaillierMulWitness { x, rho, rho_x, phantom: PhantomData }, + ) + } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierMulCommitment { - A: BigInt, - B: BigInt, + A: BigInt, + B: BigInt, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierMulProof { - z: BigInt, - u: BigInt, - v: BigInt, - commitment: PaillierMulCommitment, - phantom: PhantomData<(E, H)>, + z: BigInt, + u: BigInt, + v: BigInt, + commitment: PaillierMulCommitment, + phantom: PhantomData<(E, H)>, } // Link to the UC non-interactive threshold ECDSA paper impl PaillierMulProof { - pub fn prove( - witness: &PaillierMulWitness, - statement: &PaillierMulStatement, - ) -> PaillierMulProof { - // α,r,s <- Z∗_N - let alpha = sample_relatively_prime_integer(&statement.N); - let r = sample_relatively_prime_integer(&statement.N); - let s = sample_relatively_prime_integer(&statement.N); - // A = Y^α * r^N mod N^2 - let A = BigInt::mod_mul( - &mod_pow_with_negative(&statement.Y, &alpha, &statement.NN), - &BigInt::mod_pow(&r, &statement.N, &statement.NN), - &statement.NN, - ); - // B = (1 + N)^α * s^N mod N^2 - let B = { - let B_ciphertext = Paillier::encrypt_with_chosen_randomness( - &statement.ek_prover, - RawPlaintext::from(alpha.clone()), - &Randomness::from(s.clone()), - ); - let B_bigint: BigInt = B_ciphertext.into(); - B_bigint.mod_floor(&statement.NN) - }; - // e = H(A,B) - let mut e = H::new().chain_bigint(&A).chain_bigint(&B).result_bigint(); - let mut rng: ChaChaRng = ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); - let val = rng.gen_range(0..2); - e = BigInt::from(val).mul(&BigInt::from(-2)).add(&BigInt::one()).mul(&e); - let commitment: PaillierMulCommitment = PaillierMulCommitment { A, B }; - // z = α + e * x mod N - let z = BigInt::add(&alpha, &BigInt::mul(&e, &witness.x)); - // u = r * rho^e mod N - let u = BigInt::mod_mul( - &r, - &mod_pow_with_negative(&witness.rho, &e, &statement.N), - &statement.N, - ); - // v = s * rho_x^e mod N - let v = BigInt::mod_mul( - &s, - &mod_pow_with_negative(&witness.rho_x, &e, &statement.N), - &statement.N, - ); - // Return the proof - PaillierMulProof { z, u, v, commitment, phantom: PhantomData } - } - - pub fn verify( - proof: &PaillierMulProof, - statement: &PaillierMulStatement, - ) -> Result<(), Error> { - // Compute the challenge - let mut e = H::new() - .chain_bigint(&proof.commitment.A) - .chain_bigint(&proof.commitment.B) - .result_bigint(); - let mut rng: ChaChaRng = ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); - let val = rng.gen_range(0..2); - e = BigInt::from(val).mul(&BigInt::from(-2)).add(&BigInt::one()).mul(&e); - /* - FIRST EQUALITY CHECK - Y^z · u^N = A · C^e mod N^2 - */ - let left_1 = BigInt::mod_mul( - &mod_pow_with_negative(&statement.Y, &proof.z, &statement.NN), - &BigInt::mod_pow(&proof.u, &statement.N, &statement.NN), - &statement.NN, - ); - let right_1 = BigInt::mod_mul( - &proof.commitment.A, - &mod_pow_with_negative(&statement.C, &e, &statement.NN), - &statement.NN, - ); - assert!(left_1 == right_1); - /* - SECOND EQUALITY CHECK - (1 + N)^z · v^N = B · X^e mod N^2 === Enc(z,c) = B · X^e mod N^2 - */ - let left_ciphertext = Paillier::encrypt_with_chosen_randomness( - &statement.ek_prover, - RawPlaintext::from(proof.z.clone()), - &Randomness::from(proof.v.clone()), - ); - let left_2: BigInt = left_ciphertext.into(); - let right_2 = BigInt::mod_mul( - &proof.commitment.B, - &mod_pow_with_negative(&statement.X, &e, &statement.NN), - &statement.NN, - ); - assert!(left_2.mod_floor(&statement.NN) == right_2); - Ok(()) - } + pub fn prove( + witness: &PaillierMulWitness, + statement: &PaillierMulStatement, + ) -> PaillierMulProof { + // α,r,s <- Z∗_N + let alpha = sample_relatively_prime_integer(&statement.N); + let r = sample_relatively_prime_integer(&statement.N); + let s = sample_relatively_prime_integer(&statement.N); + // A = Y^α * r^N mod N^2 + let A = BigInt::mod_mul( + &mod_pow_with_negative(&statement.Y, &alpha, &statement.NN), + &BigInt::mod_pow(&r, &statement.N, &statement.NN), + &statement.NN, + ); + // B = (1 + N)^α * s^N mod N^2 + let B = { + let B_ciphertext = Paillier::encrypt_with_chosen_randomness( + &statement.ek_prover, + RawPlaintext::from(alpha.clone()), + &Randomness::from(s.clone()), + ); + let B_bigint: BigInt = B_ciphertext.into(); + B_bigint.mod_floor(&statement.NN) + }; + // e = H(A,B) + let mut e = H::new().chain_bigint(&A).chain_bigint(&B).result_bigint(); + let mut rng: ChaChaRng = + ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); + let val = rng.gen_range(0..2); + e = BigInt::from(val) + .mul(&BigInt::from(-2)) + .add(&BigInt::one()) + .mul(&e); + let commitment: PaillierMulCommitment = PaillierMulCommitment { A, B }; + // z = α + e * x mod N + let z = BigInt::add(&alpha, &BigInt::mul(&e, &witness.x)); + // u = r * rho^e mod N + let u = BigInt::mod_mul( + &r, + &mod_pow_with_negative(&witness.rho, &e, &statement.N), + &statement.N, + ); + // v = s * rho_x^e mod N + let v = BigInt::mod_mul( + &s, + &mod_pow_with_negative(&witness.rho_x, &e, &statement.N), + &statement.N, + ); + // Return the proof + PaillierMulProof { z, u, v, commitment, phantom: PhantomData } + } + + pub fn verify( + proof: &PaillierMulProof, + statement: &PaillierMulStatement, + ) -> Result<(), Error> { + // Compute the challenge + let mut e = H::new() + .chain_bigint(&proof.commitment.A) + .chain_bigint(&proof.commitment.B) + .result_bigint(); + let mut rng: ChaChaRng = + ChaChaRng::from_seed(e.to_bytes().try_into().unwrap()); + let val = rng.gen_range(0..2); + e = BigInt::from(val) + .mul(&BigInt::from(-2)) + .add(&BigInt::one()) + .mul(&e); + /* + FIRST EQUALITY CHECK + Y^z · u^N = A · C^e mod N^2 + */ + let left_1 = BigInt::mod_mul( + &mod_pow_with_negative(&statement.Y, &proof.z, &statement.NN), + &BigInt::mod_pow(&proof.u, &statement.N, &statement.NN), + &statement.NN, + ); + let right_1 = BigInt::mod_mul( + &proof.commitment.A, + &mod_pow_with_negative(&statement.C, &e, &statement.NN), + &statement.NN, + ); + assert!(left_1 == right_1); + /* + SECOND EQUALITY CHECK + (1 + N)^z · v^N = B · X^e mod N^2 === Enc(z,c) = B · X^e mod N^2 + */ + let left_ciphertext = Paillier::encrypt_with_chosen_randomness( + &statement.ek_prover, + RawPlaintext::from(proof.z.clone()), + &Randomness::from(proof.v.clone()), + ); + let left_2: BigInt = left_ciphertext.into(); + let right_2 = BigInt::mod_mul( + &proof.commitment.B, + &mod_pow_with_negative(&statement.X, &e, &statement.NN), + &statement.NN, + ); + assert!(left_2.mod_floor(&statement.NN) == right_2); + Ok(()) + } } #[cfg(test)] mod tests { - use super::*; - use crate::utilities::{mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER}; - use curv::elliptic::curves::secp256_k1::Secp256k1; - use paillier::{Encrypt, KeyGeneration, Paillier, RawPlaintext}; - use sha2::Sha256; - - #[test] - fn test_paillier_mul() { - let (ek_prover, _) = Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); - let rho: BigInt = BigInt::from_paillier_key(&ek_prover); - let rho_x: BigInt = BigInt::from_paillier_key(&ek_prover); - let Y = Paillier::encrypt(&ek_prover, RawPlaintext::from(BigInt::from(12))); - let (statement, witness) = PaillierMulStatement::::generate( - rho, - rho_x, - ek_prover, - Y.0.into_owned(), - ); - let proof = PaillierMulProof::::prove(&witness, &statement); - assert!(PaillierMulProof::::verify(&proof, &statement).is_ok()); - } + use super::*; + use crate::utilities::{ + mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER, + }; + use curv::elliptic::curves::secp256_k1::Secp256k1; + use paillier::{Encrypt, KeyGeneration, Paillier, RawPlaintext}; + use sha2::Sha256; + + #[test] + fn test_paillier_mul() { + let (ek_prover, _) = + Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); + let rho: BigInt = BigInt::from_paillier_key(&ek_prover); + let rho_x: BigInt = BigInt::from_paillier_key(&ek_prover); + let Y = + Paillier::encrypt(&ek_prover, RawPlaintext::from(BigInt::from(12))); + let (statement, witness) = + PaillierMulStatement::::generate( + rho, + rho_x, + ek_prover, + Y.0.into_owned(), + ); + let proof = + PaillierMulProof::::prove(&witness, &statement); + assert!(PaillierMulProof::::verify( + &proof, &statement + ) + .is_ok()); + } } diff --git a/src/utilities/mul_star/mod.rs b/src/utilities/mul_star/mod.rs index 00a7c46b..01e6f11f 100644 --- a/src/utilities/mul_star/mod.rs +++ b/src/utilities/mul_star/mod.rs @@ -1,18 +1,18 @@ #![allow(non_snake_case)] /* - CGGMP Threshold ECDSA + CGGMP Threshold ECDSA - Copyright 2022 by Webb Technologies + Copyright 2022 by Webb Technologies - This file is part of CGGMP Threshold ECDSA library - (https://github.com/webb-tools/cggmp-threshold-ecdsa) + This file is part of CGGMP Threshold ECDSA library + (https://github.com/webb-tools/cggmp-threshold-ecdsa) - CGGMP Threshold ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + CGGMP Threshold ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ //! Knowledge of Exponent vs Paillier Encryption – Π^{log∗} @@ -24,10 +24,10 @@ use super::sample_relatively_prime_integer; use crate::utilities::{mod_pow_with_negative, L}; use curv::{ - arithmetic::{traits::*, Modulo}, - cryptographic_primitives::hashing::{Digest, DigestExt}, - elliptic::curves::{Curve, Point, Scalar}, - BigInt, + arithmetic::{traits::*, Modulo}, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{Curve, Point, Scalar}, + BigInt, }; use paillier::EncryptionKey; use serde::{Deserialize, Serialize}; @@ -35,252 +35,291 @@ use std::marker::PhantomData; use zk_paillier::zkproofs::IncorrectProof; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PaillierMultiplicationVersusGroupStatement { - pub N0: BigInt, - pub NN0: BigInt, - pub C: BigInt, - pub D: BigInt, - pub X: Point, - pub N_hat: BigInt, - pub s: BigInt, - pub t: BigInt, - pub phantom: PhantomData<(E, H)>, +pub struct PaillierMultiplicationVersusGroupStatement< + E: Curve, + H: Digest + Clone, +> { + pub N0: BigInt, + pub NN0: BigInt, + pub C: BigInt, + pub D: BigInt, + pub X: Point, + pub N_hat: BigInt, + pub s: BigInt, + pub t: BigInt, + pub phantom: PhantomData<(E, H)>, } -pub struct PaillierMultiplicationVersusGroupWitness { - x: BigInt, - rho: BigInt, - phantom: PhantomData<(E, H)>, +pub struct PaillierMultiplicationVersusGroupWitness +{ + x: BigInt, + rho: BigInt, + phantom: PhantomData<(E, H)>, } -impl PaillierMultiplicationVersusGroupWitness { - pub fn new(x: BigInt, rho: BigInt) -> Self { - PaillierMultiplicationVersusGroupWitness { x, rho, phantom: PhantomData } - } +impl + PaillierMultiplicationVersusGroupWitness +{ + pub fn new(x: BigInt, rho: BigInt) -> Self { + PaillierMultiplicationVersusGroupWitness { + x, + rho, + phantom: PhantomData, + } + } } -impl PaillierMultiplicationVersusGroupStatement { - #[allow(clippy::too_many_arguments)] - pub fn generate( - rho: BigInt, - _C: BigInt, - s: BigInt, - t: BigInt, - N_hat: BigInt, - paillier_key: EncryptionKey, - ) -> (Self, PaillierMultiplicationVersusGroupWitness) { - // Set up exponents - let l_exp = BigInt::pow(&BigInt::from(2), L as u32); - // Set up moduli - let N0 = paillier_key.clone().n; - let NN0 = paillier_key.nn; - let x = BigInt::sample_range(&BigInt::from(-1).mul(&l_exp), &l_exp); - let X = Point::::generator().as_point() * Scalar::from(&x); - let C: BigInt = BigInt::zero(); - // D = C^x * rho^(N_0) mod N_0^2 - let D = BigInt::mod_mul( - &mod_pow_with_negative(&C, &x, &NN0), - &BigInt::mod_pow(&rho, &N0, &NN0), - &NN0, - ); - ( - Self { N0, NN0, C, D, X, N_hat, s, t, phantom: PhantomData }, - PaillierMultiplicationVersusGroupWitness { x, rho, phantom: PhantomData }, - ) - } +impl + PaillierMultiplicationVersusGroupStatement +{ + #[allow(clippy::too_many_arguments)] + pub fn generate( + rho: BigInt, + _C: BigInt, + s: BigInt, + t: BigInt, + N_hat: BigInt, + paillier_key: EncryptionKey, + ) -> (Self, PaillierMultiplicationVersusGroupWitness) { + // Set up exponents + let l_exp = BigInt::pow(&BigInt::from(2), L as u32); + // Set up moduli + let N0 = paillier_key.clone().n; + let NN0 = paillier_key.nn; + let x = BigInt::sample_range(&BigInt::from(-1).mul(&l_exp), &l_exp); + let X = Point::::generator().as_point() * Scalar::from(&x); + let C: BigInt = BigInt::zero(); + // D = C^x * rho^(N_0) mod N_0^2 + let D = BigInt::mod_mul( + &mod_pow_with_negative(&C, &x, &NN0), + &BigInt::mod_pow(&rho, &N0, &NN0), + &NN0, + ); + ( + Self { N0, NN0, C, D, X, N_hat, s, t, phantom: PhantomData }, + PaillierMultiplicationVersusGroupWitness { + x, + rho, + phantom: PhantomData, + }, + ) + } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierMultiplicationVersusGroupCommitment { - A: BigInt, - B_x: Point, - E: BigInt, - S: BigInt, + A: BigInt, + B_x: Point, + E: BigInt, + S: BigInt, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaillierMultiplicationVersusGroupProof { - z_1: BigInt, - z_2: BigInt, - w: BigInt, - commitment: PaillierMultiplicationVersusGroupCommitment, - phantom: PhantomData<(E, H)>, + z_1: BigInt, + z_2: BigInt, + w: BigInt, + commitment: PaillierMultiplicationVersusGroupCommitment, + phantom: PhantomData<(E, H)>, } // Link to the UC non-interactive threshold ECDSA paper impl PaillierMultiplicationVersusGroupProof { - pub fn prove( - witness: &PaillierMultiplicationVersusGroupWitness, - statement: &PaillierMultiplicationVersusGroupStatement, - ) -> PaillierMultiplicationVersusGroupProof { - // Step 1: Sample alpha between -2^{l+ε} and 2^{l+ε} - let alpha_upper = BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32); - let alpha_lower = BigInt::from(-1).mul(&alpha_upper); - let alpha = BigInt::sample_range(&alpha_lower, &alpha_upper); - - // Step 2: m, r, r_y gamma - // Sample mu between -2^L * N_hat and 2^L * N_hat - let m_upper = BigInt::mul( - &statement.N_hat, - &BigInt::pow(&BigInt::from(2), crate::utilities::L as u32), - ); - let m_lower = BigInt::from(-1).mul(&m_upper); - let m = BigInt::sample_range(&m_lower, &m_upper); - - // γ ← ± 2^{l+ε} · Nˆ - let gamma_upper = BigInt::mul( - &statement.N_hat, - &BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32), - ); - let gamma_lower = BigInt::from(-1).mul(&gamma_upper); - let gamma = BigInt::sample_range(&gamma_lower, &gamma_upper); - // Sample r from Z*_{N_0} - let r = sample_relatively_prime_integer(&statement.N0.clone()); - - // A = C^alpha * r^N_0 - let A = BigInt::mod_mul( - &mod_pow_with_negative(&statement.C, &alpha, &statement.NN0), - &BigInt::mod_pow(&r, &statement.N0, &statement.NN0), - &statement.NN0, - ); - - // B_x = g^alpha - let B_x: Point = Point::::generator().as_point() * Scalar::from_bigint(&alpha); - - // E = s^alpha t^gamma mod N_hat - let E = BigInt::mod_mul( - &mod_pow_with_negative(&statement.s, &alpha, &statement.N_hat), - &mod_pow_with_negative(&statement.t, &gamma, &statement.N_hat), - &statement.N_hat, - ); - - // S = s^x t^m mod N_hat - let S = BigInt::mod_mul( - &mod_pow_with_negative(&statement.s, &witness.x, &statement.N_hat), - &mod_pow_with_negative(&statement.t, &m, &statement.N_hat), - &statement.N_hat, - ); - - let e = H::new() - .chain_bigint(&A) - .chain_point(&B_x) - .chain_bigint(&E) - .chain_bigint(&S) - .result_bigint(); - - // Step 5: Compute z_1, z_2, z_3 - // z_1 = alpha + ex - let z_1 = BigInt::add(&alpha, &BigInt::mul(&e, &witness.x)); - // z_2 = gamma + e*m - let z_2 = BigInt::add(&gamma, &BigInt::mul(&e, &m)); - // w = r * rho^e mod N_0 - let w = BigInt::mod_mul( - &r, - &mod_pow_with_negative(&witness.rho, &e, &statement.N0), - &statement.N0, - ); - let commitment = PaillierMultiplicationVersusGroupCommitment { A, B_x, E, S }; - Self { z_1, z_2, w, commitment, phantom: PhantomData } - } - - pub fn verify( - proof: &PaillierMultiplicationVersusGroupProof, - statement: &PaillierMultiplicationVersusGroupStatement, - ) -> Result<(), IncorrectProof> { - let e = H::new() - .chain_bigint(&proof.commitment.A) - .chain_point(&proof.commitment.B_x) - .chain_bigint(&proof.commitment.E) - .chain_bigint(&proof.commitment.S) - .result_bigint(); - - // left_1 = (C)^{z_1}w^{N_0} mod N_0^2 - let left_1 = BigInt::mod_mul( - &mod_pow_with_negative(&statement.C, &proof.z_1, &statement.N0), - &BigInt::mod_pow(&proof.w, &statement.N0, &statement.NN0), - &statement.NN0, - ); - - // right_1 = A * D^e - let right_1 = BigInt::mod_mul( - &proof.commitment.A, - &mod_pow_with_negative(&statement.D, &e, &statement.NN0), - &statement.NN0, - ); - - // left_2 = g^z_1 - let left_2 = Point::::generator().as_point() * Scalar::from_bigint(&proof.z_1); - // right_2 = B_x * X^e - let right_2 = - proof.commitment.B_x.clone() + (statement.X.clone() * Scalar::from_bigint(&e)); - - // left_3 = s^z_1 t^z_2 mod N_hat - let left_3 = BigInt::mod_mul( - &mod_pow_with_negative(&statement.s, &proof.z_1, &statement.N_hat), - &mod_pow_with_negative(&statement.t, &proof.z_2, &statement.N_hat), - &statement.N_hat, - ); - - // right_3 = E * S^e mod N_hat - let right_3 = BigInt::mod_mul( - &proof.commitment.E, - &mod_pow_with_negative(&proof.commitment.S, &e, &statement.N_hat), - &statement.N_hat, - ); - - if left_1 != right_1 || left_2 != right_2 || left_3 != right_3 { - return Err(IncorrectProof) - } - - // Range Check -2^{L + eps} <= z_1 <= 2^{L+eps} - let lower_bound_check: bool = proof.z_1 >= - BigInt::from(-1) - .mul(&BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32)); - - let upper_bound_check = - proof.z_1 <= BigInt::pow(&BigInt::from(2), crate::utilities::L_PLUS_EPSILON as u32); - - if !(lower_bound_check && upper_bound_check) { - return Err(IncorrectProof) - } - Ok(()) - } + pub fn prove( + witness: &PaillierMultiplicationVersusGroupWitness, + statement: &PaillierMultiplicationVersusGroupStatement, + ) -> PaillierMultiplicationVersusGroupProof { + // Step 1: Sample alpha between -2^{l+ε} and 2^{l+ε} + let alpha_upper = BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + ); + let alpha_lower = BigInt::from(-1).mul(&alpha_upper); + let alpha = BigInt::sample_range(&alpha_lower, &alpha_upper); + + // Step 2: m, r, r_y gamma + // Sample mu between -2^L * N_hat and 2^L * N_hat + let m_upper = BigInt::mul( + &statement.N_hat, + &BigInt::pow(&BigInt::from(2), crate::utilities::L as u32), + ); + let m_lower = BigInt::from(-1).mul(&m_upper); + let m = BigInt::sample_range(&m_lower, &m_upper); + + // γ ← ± 2^{l+ε} · Nˆ + let gamma_upper = BigInt::mul( + &statement.N_hat, + &BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + ), + ); + let gamma_lower = BigInt::from(-1).mul(&gamma_upper); + let gamma = BigInt::sample_range(&gamma_lower, &gamma_upper); + // Sample r from Z*_{N_0} + let r = sample_relatively_prime_integer(&statement.N0.clone()); + + // A = C^alpha * r^N_0 + let A = BigInt::mod_mul( + &mod_pow_with_negative(&statement.C, &alpha, &statement.NN0), + &BigInt::mod_pow(&r, &statement.N0, &statement.NN0), + &statement.NN0, + ); + + // B_x = g^alpha + let B_x: Point = + Point::::generator().as_point() * Scalar::from_bigint(&alpha); + + // E = s^alpha t^gamma mod N_hat + let E = BigInt::mod_mul( + &mod_pow_with_negative(&statement.s, &alpha, &statement.N_hat), + &mod_pow_with_negative(&statement.t, &gamma, &statement.N_hat), + &statement.N_hat, + ); + + // S = s^x t^m mod N_hat + let S = BigInt::mod_mul( + &mod_pow_with_negative(&statement.s, &witness.x, &statement.N_hat), + &mod_pow_with_negative(&statement.t, &m, &statement.N_hat), + &statement.N_hat, + ); + + let e = H::new() + .chain_bigint(&A) + .chain_point(&B_x) + .chain_bigint(&E) + .chain_bigint(&S) + .result_bigint(); + + // Step 5: Compute z_1, z_2, z_3 + // z_1 = alpha + ex + let z_1 = BigInt::add(&alpha, &BigInt::mul(&e, &witness.x)); + // z_2 = gamma + e*m + let z_2 = BigInt::add(&gamma, &BigInt::mul(&e, &m)); + // w = r * rho^e mod N_0 + let w = BigInt::mod_mul( + &r, + &mod_pow_with_negative(&witness.rho, &e, &statement.N0), + &statement.N0, + ); + let commitment = + PaillierMultiplicationVersusGroupCommitment { A, B_x, E, S }; + Self { z_1, z_2, w, commitment, phantom: PhantomData } + } + + pub fn verify( + proof: &PaillierMultiplicationVersusGroupProof, + statement: &PaillierMultiplicationVersusGroupStatement, + ) -> Result<(), IncorrectProof> { + let e = H::new() + .chain_bigint(&proof.commitment.A) + .chain_point(&proof.commitment.B_x) + .chain_bigint(&proof.commitment.E) + .chain_bigint(&proof.commitment.S) + .result_bigint(); + + // left_1 = (C)^{z_1}w^{N_0} mod N_0^2 + let left_1 = BigInt::mod_mul( + &mod_pow_with_negative(&statement.C, &proof.z_1, &statement.N0), + &BigInt::mod_pow(&proof.w, &statement.N0, &statement.NN0), + &statement.NN0, + ); + + // right_1 = A * D^e + let right_1 = BigInt::mod_mul( + &proof.commitment.A, + &mod_pow_with_negative(&statement.D, &e, &statement.NN0), + &statement.NN0, + ); + + // left_2 = g^z_1 + let left_2 = Point::::generator().as_point() * + Scalar::from_bigint(&proof.z_1); + // right_2 = B_x * X^e + let right_2 = proof.commitment.B_x.clone() + + (statement.X.clone() * Scalar::from_bigint(&e)); + + // left_3 = s^z_1 t^z_2 mod N_hat + let left_3 = BigInt::mod_mul( + &mod_pow_with_negative(&statement.s, &proof.z_1, &statement.N_hat), + &mod_pow_with_negative(&statement.t, &proof.z_2, &statement.N_hat), + &statement.N_hat, + ); + + // right_3 = E * S^e mod N_hat + let right_3 = BigInt::mod_mul( + &proof.commitment.E, + &mod_pow_with_negative(&proof.commitment.S, &e, &statement.N_hat), + &statement.N_hat, + ); + + if left_1 != right_1 || left_2 != right_2 || left_3 != right_3 { + return Err(IncorrectProof) + } + + // Range Check -2^{L + eps} <= z_1 <= 2^{L+eps} + let lower_bound_check: bool = proof.z_1 >= + BigInt::from(-1).mul(&BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + )); + + let upper_bound_check = proof.z_1 <= + BigInt::pow( + &BigInt::from(2), + crate::utilities::L_PLUS_EPSILON as u32, + ); + + if !(lower_bound_check && upper_bound_check) { + return Err(IncorrectProof) + } + Ok(()) + } } #[cfg(test)] mod tests { - use super::*; - use crate::utilities::{mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER}; - use curv::elliptic::curves::secp256_k1::Secp256k1; - use fs_dkr::ring_pedersen_proof::RingPedersenStatement; - use paillier::{Encrypt, KeyGeneration, Paillier, RawPlaintext}; - use sha2::Sha256; - - #[test] - fn test_mul_star_proof() { - let (ring_pedersen_statement, _witness) = - RingPedersenStatement::::generate(); - let (paillier_key, _) = Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); - - let rho: BigInt = BigInt::from_paillier_key(&paillier_key); - - let C: BigInt = - Paillier::encrypt(&paillier_key, RawPlaintext::from(BigInt::from(123))).into(); - let (statement, witness) = - PaillierMultiplicationVersusGroupStatement::::generate( - rho, - C, - ring_pedersen_statement.S, - ring_pedersen_statement.T, - ring_pedersen_statement.N, - paillier_key, - ); - let proof = PaillierMultiplicationVersusGroupProof::::prove( - &witness, &statement, - ); - assert!(PaillierMultiplicationVersusGroupProof::::verify( + use super::*; + use crate::utilities::{ + mta::range_proofs::SampleFromMultiplicativeGroup, BITS_PAILLIER, + }; + use curv::elliptic::curves::secp256_k1::Secp256k1; + use fs_dkr::ring_pedersen_proof::RingPedersenStatement; + use paillier::{Encrypt, KeyGeneration, Paillier, RawPlaintext}; + use sha2::Sha256; + + #[test] + fn test_mul_star_proof() { + let (ring_pedersen_statement, _witness) = + RingPedersenStatement::::generate(); + let (paillier_key, _) = + Paillier::keypair_with_modulus_size(BITS_PAILLIER).keys(); + + let rho: BigInt = BigInt::from_paillier_key(&paillier_key); + + let C: BigInt = Paillier::encrypt( + &paillier_key, + RawPlaintext::from(BigInt::from(123)), + ) + .into(); + let (statement, witness) = PaillierMultiplicationVersusGroupStatement::< + Secp256k1, + Sha256, + >::generate( + rho, + C, + ring_pedersen_statement.S, + ring_pedersen_statement.T, + ring_pedersen_statement.N, + paillier_key, + ); + let proof = + PaillierMultiplicationVersusGroupProof::::prove( + &witness, &statement, + ); + assert!(PaillierMultiplicationVersusGroupProof::::verify( &proof, &statement ) .is_ok()); - } + } } diff --git a/src/utilities/sha2.rs b/src/utilities/sha2.rs index 18273876..135478cc 100644 --- a/src/utilities/sha2.rs +++ b/src/utilities/sha2.rs @@ -6,45 +6,45 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Sha256 { - #[serde(skip)] - inner: sha2::Sha256, + #[serde(skip)] + inner: sha2::Sha256, } impl Digest for Sha256 { - type OutputSize = U32; - - fn new() -> Self { - Self { inner: sha2::Sha256::new() } - } - - fn update(&mut self, data: impl AsRef<[u8]>) { - self.inner.update(data); - } - - fn chain(self, data: impl AsRef<[u8]>) -> Self - where - Self: Sized, - { - Self { inner: self.inner.chain(data) } - } - - fn finalize(self) -> Output { - self.inner.finalize() - } - - fn finalize_reset(&mut self) -> Output { - self.inner.finalize_reset() - } - - fn reset(&mut self) { - self.inner.reset(); - } - - fn output_size() -> usize { - sha2::Sha256::output_size() - } - - fn digest(data: &[u8]) -> Output { - sha2::Sha256::digest(data) - } + type OutputSize = U32; + + fn new() -> Self { + Self { inner: sha2::Sha256::new() } + } + + fn update(&mut self, data: impl AsRef<[u8]>) { + self.inner.update(data); + } + + fn chain(self, data: impl AsRef<[u8]>) -> Self + where + Self: Sized, + { + Self { inner: self.inner.chain(data) } + } + + fn finalize(self) -> Output { + self.inner.finalize() + } + + fn finalize_reset(&mut self) -> Output { + self.inner.finalize_reset() + } + + fn reset(&mut self) { + self.inner.reset(); + } + + fn output_size() -> usize { + sha2::Sha256::output_size() + } + + fn digest(data: &[u8]) -> Output { + sha2::Sha256::digest(data) + } } diff --git a/src/utilities/zk_pdl/mod.rs b/src/utilities/zk_pdl/mod.rs index bd6bb71c..a173f207 100644 --- a/src/utilities/zk_pdl/mod.rs +++ b/src/utilities/zk_pdl/mod.rs @@ -1,18 +1,18 @@ #![allow(non_snake_case)] /* - Multi-party ECDSA + Multi-party ECDSA - Copyright 2018 by Kzen Networks + Copyright 2018 by Kzen Networks - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ //! We use the proof as given in protocol 6.1 in https://eprint.iacr.org/2017/552.pdf @@ -23,13 +23,16 @@ use std::ops::Shl; use curv::{ - arithmetic::traits::*, - cryptographic_primitives::commitments::{hash_commitment::HashCommitment, traits::Commitment}, - elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, - BigInt, + arithmetic::traits::*, + cryptographic_primitives::commitments::{ + hash_commitment::HashCommitment, traits::Commitment, + }, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, }; use paillier::{ - Add, Decrypt, DecryptionKey, Encrypt, EncryptionKey, Mul, Paillier, RawCiphertext, RawPlaintext, + Add, Decrypt, DecryptionKey, Encrypt, EncryptionKey, Mul, Paillier, + RawCiphertext, RawPlaintext, }; use serde::{Deserialize, Serialize}; use sha2::Sha256; @@ -38,208 +41,232 @@ use zk_paillier::zkproofs::{IncorrectProof, RangeProofNi}; #[derive(Error, Debug)] pub enum ZkPdlError { - #[error("zk pdl message2 failed")] - Message2, - #[error("zk pdl finalize failed")] - Finalize, + #[error("zk pdl message2 failed")] + Message2, + #[error("zk pdl finalize failed")] + Finalize, } #[derive(Clone)] pub struct PDLStatement { - pub ciphertext: BigInt, - pub ek: EncryptionKey, - pub Q: Point, - pub G: Point, + pub ciphertext: BigInt, + pub ek: EncryptionKey, + pub Q: Point, + pub G: Point, } #[derive(Clone)] pub struct PDLWitness { - pub x: Scalar, - pub r: BigInt, - pub dk: DecryptionKey, + pub x: Scalar, + pub r: BigInt, + pub dk: DecryptionKey, } #[derive(Debug, Clone)] pub struct PDLVerifierState { - pub c_tag: BigInt, - pub c_tag_tag: BigInt, - a: BigInt, - b: BigInt, - blindness: BigInt, - q_tag: Point, - c_hat: BigInt, + pub c_tag: BigInt, + pub c_tag_tag: BigInt, + a: BigInt, + b: BigInt, + blindness: BigInt, + q_tag: Point, + c_hat: BigInt, } #[derive(Debug, Clone)] pub struct PDLProverState { - pub decommit: PDLProverDecommit, - pub alpha: BigInt, + pub decommit: PDLProverDecommit, + pub alpha: BigInt, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PDLVerifierFirstMessage { - pub c_tag: BigInt, - pub c_tag_tag: BigInt, + pub c_tag: BigInt, + pub c_tag_tag: BigInt, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PDLProverFirstMessage { - pub c_hat: BigInt, - pub range_proof: RangeProofNi, + pub c_hat: BigInt, + pub range_proof: RangeProofNi, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PDLVerifierSecondMessage { - pub a: BigInt, - pub b: BigInt, - pub blindness: BigInt, + pub a: BigInt, + pub b: BigInt, + pub blindness: BigInt, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PDLProverDecommit { - pub q_hat: Point, - pub blindness: BigInt, + pub q_hat: Point, + pub blindness: BigInt, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PDLProverSecondMessage { - pub decommit: PDLProverDecommit, + pub decommit: PDLProverDecommit, } pub struct Prover {} pub struct Verifier {} impl Verifier { - pub fn message1(statement: &PDLStatement) -> (PDLVerifierFirstMessage, PDLVerifierState) { - let a_fe = Scalar::::random(); - let a = a_fe.to_bigint(); - let q = Scalar::::group_order(); - let q_sq = q.pow(2); - let b = BigInt::sample_below(&q_sq); - let b_fe = Scalar::::from(&b); - let b_enc = Paillier::encrypt(&statement.ek, RawPlaintext::from(b.clone())); - let ac = Paillier::mul( - &statement.ek, - RawCiphertext::from(statement.ciphertext.clone()), - RawPlaintext::from(a.clone()), - ); - let c_tag = Paillier::add(&statement.ek, ac, b_enc).0.into_owned(); - let ab_concat = a.clone() + b.clone().shl(a.bit_length()); - let blindness = BigInt::sample_below(q); - let c_tag_tag = HashCommitment::::create_commitment_with_user_defined_randomness( + pub fn message1( + statement: &PDLStatement, + ) -> (PDLVerifierFirstMessage, PDLVerifierState) { + let a_fe = Scalar::::random(); + let a = a_fe.to_bigint(); + let q = Scalar::::group_order(); + let q_sq = q.pow(2); + let b = BigInt::sample_below(&q_sq); + let b_fe = Scalar::::from(&b); + let b_enc = + Paillier::encrypt(&statement.ek, RawPlaintext::from(b.clone())); + let ac = Paillier::mul( + &statement.ek, + RawCiphertext::from(statement.ciphertext.clone()), + RawPlaintext::from(a.clone()), + ); + let c_tag = Paillier::add(&statement.ek, ac, b_enc).0.into_owned(); + let ab_concat = a.clone() + b.clone().shl(a.bit_length()); + let blindness = BigInt::sample_below(q); + let c_tag_tag = HashCommitment::::create_commitment_with_user_defined_randomness( &ab_concat, &blindness, ); - let q_tag = &statement.Q * &a_fe + &statement.G * b_fe; - - ( - PDLVerifierFirstMessage { c_tag: c_tag.clone(), c_tag_tag: c_tag_tag.clone() }, - PDLVerifierState { c_tag, c_tag_tag, a, b, blindness, q_tag, c_hat: BigInt::zero() }, - ) - } - - pub fn message2( - prover_first_messasge: &PDLProverFirstMessage, - statement: &PDLStatement, - state: &mut PDLVerifierState, - ) -> Result { - let decommit_message = PDLVerifierSecondMessage { - a: state.a.clone(), - b: state.b.clone(), - blindness: state.blindness.clone(), - }; - let range_proof_is_ok = - verify_range_proof(statement, &prover_first_messasge.range_proof).is_ok(); - state.c_hat = prover_first_messasge.c_hat.clone(); - if range_proof_is_ok { - Ok(decommit_message) - } else { - Err(ZkPdlError::Message2) - } - } - - pub fn finalize( - prover_first_message: &PDLProverFirstMessage, - prover_second_message: &PDLProverSecondMessage, - state: &PDLVerifierState, - ) -> Result<(), ZkPdlError> { - let c_hat_test = HashCommitment::::create_commitment_with_user_defined_randomness( + let q_tag = &statement.Q * &a_fe + &statement.G * b_fe; + + ( + PDLVerifierFirstMessage { + c_tag: c_tag.clone(), + c_tag_tag: c_tag_tag.clone(), + }, + PDLVerifierState { + c_tag, + c_tag_tag, + a, + b, + blindness, + q_tag, + c_hat: BigInt::zero(), + }, + ) + } + + pub fn message2( + prover_first_messasge: &PDLProverFirstMessage, + statement: &PDLStatement, + state: &mut PDLVerifierState, + ) -> Result { + let decommit_message = PDLVerifierSecondMessage { + a: state.a.clone(), + b: state.b.clone(), + blindness: state.blindness.clone(), + }; + let range_proof_is_ok = + verify_range_proof(statement, &prover_first_messasge.range_proof) + .is_ok(); + state.c_hat = prover_first_messasge.c_hat.clone(); + if range_proof_is_ok { + Ok(decommit_message) + } else { + Err(ZkPdlError::Message2) + } + } + + pub fn finalize( + prover_first_message: &PDLProverFirstMessage, + prover_second_message: &PDLProverSecondMessage, + state: &PDLVerifierState, + ) -> Result<(), ZkPdlError> { + let c_hat_test = HashCommitment::::create_commitment_with_user_defined_randomness( &BigInt::from_bytes(prover_second_message.decommit.q_hat.to_bytes(true).as_ref()), &prover_second_message.decommit.blindness, ); - if prover_first_message.c_hat == c_hat_test && - prover_second_message.decommit.q_hat == state.q_tag - { - Ok(()) - } else { - Err(ZkPdlError::Finalize) - } - } + if prover_first_message.c_hat == c_hat_test && + prover_second_message.decommit.q_hat == state.q_tag + { + Ok(()) + } else { + Err(ZkPdlError::Finalize) + } + } } impl Prover { - pub fn message1( - witness: &PDLWitness, - statement: &PDLStatement, - verifier_first_message: &PDLVerifierFirstMessage, - ) -> (PDLProverFirstMessage, PDLProverState) { - let c_tag = verifier_first_message.c_tag.clone(); - let alpha = Paillier::decrypt(&witness.dk, &RawCiphertext::from(c_tag)); - let alpha_fe = Scalar::::from(alpha.0.as_ref()); - let q_hat = &statement.G * alpha_fe; - let blindness = BigInt::sample_below(Scalar::::group_order()); - let c_hat = HashCommitment::::create_commitment_with_user_defined_randomness( + pub fn message1( + witness: &PDLWitness, + statement: &PDLStatement, + verifier_first_message: &PDLVerifierFirstMessage, + ) -> (PDLProverFirstMessage, PDLProverState) { + let c_tag = verifier_first_message.c_tag.clone(); + let alpha = Paillier::decrypt(&witness.dk, &RawCiphertext::from(c_tag)); + let alpha_fe = Scalar::::from(alpha.0.as_ref()); + let q_hat = &statement.G * alpha_fe; + let blindness = + BigInt::sample_below(Scalar::::group_order()); + let c_hat = HashCommitment::::create_commitment_with_user_defined_randomness( &BigInt::from_bytes(q_hat.to_bytes(true).as_ref()), &blindness, ); - // in parallel generate range proof: - let range_proof = generate_range_proof(statement, witness); - ( - PDLProverFirstMessage { c_hat, range_proof }, - PDLProverState { - decommit: PDLProverDecommit { blindness, q_hat }, - alpha: alpha.0.into_owned(), - }, - ) - } - - pub fn message2( - verifier_first_message: &PDLVerifierFirstMessage, - verifier_second_message: &PDLVerifierSecondMessage, - witness: &PDLWitness, - state: &PDLProverState, - ) -> Result { - let ab_concat = &verifier_second_message.a + - verifier_second_message.b.clone().shl(verifier_second_message.a.bit_length()); // b|a (in the paper it is a|b) - let c_tag_tag_test = + // in parallel generate range proof: + let range_proof = generate_range_proof(statement, witness); + ( + PDLProverFirstMessage { c_hat, range_proof }, + PDLProverState { + decommit: PDLProverDecommit { blindness, q_hat }, + alpha: alpha.0.into_owned(), + }, + ) + } + + pub fn message2( + verifier_first_message: &PDLVerifierFirstMessage, + verifier_second_message: &PDLVerifierSecondMessage, + witness: &PDLWitness, + state: &PDLProverState, + ) -> Result { + let ab_concat = &verifier_second_message.a + + verifier_second_message + .b + .clone() + .shl(verifier_second_message.a.bit_length()); // b|a (in the paper it is a|b) + let c_tag_tag_test = HashCommitment::::create_commitment_with_user_defined_randomness( &ab_concat, &verifier_second_message.blindness, ); - let ax1 = &verifier_second_message.a * witness.x.to_bigint(); - let alpha_test = ax1 + &verifier_second_message.b; - if alpha_test == state.alpha && verifier_first_message.c_tag_tag == c_tag_tag_test { - Ok(PDLProverSecondMessage { decommit: state.decommit.clone() }) - } else { - Err(ZkPdlError::Message2) - } - } + let ax1 = &verifier_second_message.a * witness.x.to_bigint(); + let alpha_test = ax1 + &verifier_second_message.b; + if alpha_test == state.alpha && + verifier_first_message.c_tag_tag == c_tag_tag_test + { + Ok(PDLProverSecondMessage { decommit: state.decommit.clone() }) + } else { + Err(ZkPdlError::Message2) + } + } } -fn generate_range_proof(statement: &PDLStatement, witness: &PDLWitness) -> RangeProofNi { - RangeProofNi::prove( - &statement.ek, - Scalar::::group_order(), - &statement.ciphertext, - &witness.x.to_bigint(), - &witness.r, - ) +fn generate_range_proof( + statement: &PDLStatement, + witness: &PDLWitness, +) -> RangeProofNi { + RangeProofNi::prove( + &statement.ek, + Scalar::::group_order(), + &statement.ciphertext, + &witness.x.to_bigint(), + &witness.r, + ) } fn verify_range_proof( - statement: &PDLStatement, - range_proof: &RangeProofNi, + statement: &PDLStatement, + range_proof: &RangeProofNi, ) -> Result<(), IncorrectProof> { - range_proof.verify(&statement.ek, &statement.ciphertext) + range_proof.verify(&statement.ek, &statement.ciphertext) } #[cfg(test)] diff --git a/src/utilities/zk_pdl/test.rs b/src/utilities/zk_pdl/test.rs index 0f0aadbd..928ca7d6 100644 --- a/src/utilities/zk_pdl/test.rs +++ b/src/utilities/zk_pdl/test.rs @@ -1,48 +1,56 @@ #![allow(non_snake_case)] use curv::{ - arithmetic::traits::*, - elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, - BigInt, + arithmetic::traits::*, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, }; use paillier::{ - core::Randomness, - traits::{EncryptWithChosenRandomness, KeyGeneration}, - Paillier, RawPlaintext, + core::Randomness, + traits::{EncryptWithChosenRandomness, KeyGeneration}, + Paillier, RawPlaintext, }; use crate::utilities::zk_pdl::{PDLStatement, PDLWitness, Prover, Verifier}; #[test] fn test_zk_pdl() { - // pre-test: + // pre-test: - let (ek, dk) = Paillier::keypair().keys(); - let randomness = Randomness::sample(&ek); - let x = Scalar::::random(); - let x: Scalar = - Scalar::::from(&x.to_bigint().div_floor(&BigInt::from(3))); + let (ek, dk) = Paillier::keypair().keys(); + let randomness = Randomness::sample(&ek); + let x = Scalar::::random(); + let x: Scalar = + Scalar::::from(&x.to_bigint().div_floor(&BigInt::from(3))); - let Q = Point::generator() * &x; + let Q = Point::generator() * &x; - let c = Paillier::encrypt_with_chosen_randomness( - &ek, - RawPlaintext::from(x.to_bigint()), - &randomness, - ) - .0 - .into_owned(); - let statement = PDLStatement { ciphertext: c, ek, Q, G: Point::generator().to_point() }; - let witness = PDLWitness { x, r: randomness.0, dk }; - // - let (verifier_message1, mut verifier_state) = Verifier::message1(&statement); - let (prover_message1, prover_state) = - Prover::message1(&witness, &statement, &verifier_message1); - let verifier_message2 = - Verifier::message2(&prover_message1, &statement, &mut verifier_state).expect(""); - let prover_message2 = - Prover::message2(&verifier_message1, &verifier_message2, &witness, &prover_state) - .expect(""); - let result = Verifier::finalize(&prover_message1, &prover_message2, &verifier_state); - assert!(result.is_ok()); + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint()), + &randomness, + ) + .0 + .into_owned(); + let statement = + PDLStatement { ciphertext: c, ek, Q, G: Point::generator().to_point() }; + let witness = PDLWitness { x, r: randomness.0, dk }; + // + let (verifier_message1, mut verifier_state) = + Verifier::message1(&statement); + let (prover_message1, prover_state) = + Prover::message1(&witness, &statement, &verifier_message1); + let verifier_message2 = + Verifier::message2(&prover_message1, &statement, &mut verifier_state) + .expect(""); + let prover_message2 = Prover::message2( + &verifier_message1, + &verifier_message2, + &witness, + &prover_state, + ) + .expect(""); + let result = + Verifier::finalize(&prover_message1, &prover_message2, &verifier_state); + assert!(result.is_ok()); } diff --git a/src/utilities/zk_pdl_with_slack/mod.rs b/src/utilities/zk_pdl_with_slack/mod.rs index 97181326..552757b4 100644 --- a/src/utilities/zk_pdl_with_slack/mod.rs +++ b/src/utilities/zk_pdl_with_slack/mod.rs @@ -1,18 +1,18 @@ #![allow(non_snake_case)] /* - Multi-party ECDSA + Multi-party ECDSA - Copyright 2018 by Kzen Networks + Copyright 2018 by Kzen Networks - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) + This file is part of Multi-party ECDSA library + (https://github.com/KZen-networks/multi-party-ecdsa) - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. + Multi-party ECDSA is free software: you can redistribute + it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either + version 3 of the License, or (at your option) any later version. - @license GPL-3.0+ + @license GPL-3.0+ */ //! We use the proof as given in proof PIi in https://eprint.iacr.org/2016/013.pdf. @@ -20,13 +20,14 @@ //! //! Statement: (c, pk, Q, G) //! witness (x, r) such that Q = xG, c = Enc(pk, x, r) -//! note that because of the range proof, the proof has a slack in the range: x in [-q^3, q^3] +//! note that because of the range proof, the proof has a slack in the range: x +//! in [-q^3, q^3] use curv::{ - arithmetic::traits::*, - cryptographic_primitives::hashing::{Digest, DigestExt}, - elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, - BigInt, + arithmetic::traits::*, + cryptographic_primitives::hashing::{Digest, DigestExt}, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, }; use paillier::EncryptionKey; use serde::{Deserialize, Serialize}; @@ -35,161 +36,182 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum ZkPdlWithSlackError { - #[error("zk pdl with slack verification failed")] - Verify, + #[error("zk pdl with slack verification failed")] + Verify, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PDLwSlackStatement { - pub ciphertext: BigInt, - pub ek: EncryptionKey, - pub Q: Point, - pub G: Point, - pub h1: BigInt, - pub h2: BigInt, - pub N_tilde: BigInt, + pub ciphertext: BigInt, + pub ek: EncryptionKey, + pub Q: Point, + pub G: Point, + pub h1: BigInt, + pub h2: BigInt, + pub N_tilde: BigInt, } #[derive(Clone)] pub struct PDLwSlackWitness { - pub x: Scalar, - pub r: BigInt, + pub x: Scalar, + pub r: BigInt, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PDLwSlackProof { - z: BigInt, - u1: Point, - u2: BigInt, - u3: BigInt, - s1: BigInt, - s2: BigInt, - s3: BigInt, + z: BigInt, + u1: Point, + u2: BigInt, + u3: BigInt, + s1: BigInt, + s2: BigInt, + s3: BigInt, } impl PDLwSlackProof { - pub fn prove(witness: &PDLwSlackWitness, statement: &PDLwSlackStatement) -> Self { - let q3 = Scalar::::group_order().pow(3); - let q_N_tilde = Scalar::::group_order() * &statement.N_tilde; - let q3_N_tilde = &q3 * &statement.N_tilde; - - let alpha = BigInt::sample_below(&q3); - let one = BigInt::one(); - let beta = BigInt::sample_range(&one, &(&statement.ek.n - &one)); - let rho = BigInt::sample_below(&q_N_tilde); - let gamma = BigInt::sample_below(&q3_N_tilde); - - let z = commitment_unknown_order( - &statement.h1, - &statement.h2, - &statement.N_tilde, - &witness.x.to_bigint(), - &rho, - ); - let u1 = &statement.G * &Scalar::::from(&alpha); - let u2 = commitment_unknown_order( - &(&statement.ek.n + BigInt::one()), - &beta, - &statement.ek.nn, - &alpha, - &statement.ek.n, - ); - let u3 = commitment_unknown_order( - &statement.h1, - &statement.h2, - &statement.N_tilde, - &alpha, - &gamma, - ); - - let e = Sha256::new() - .chain_bigint(&BigInt::from_bytes(statement.G.to_bytes(true).as_ref())) - .chain_bigint(&BigInt::from_bytes(statement.Q.to_bytes(true).as_ref())) - .chain_bigint(&statement.ciphertext) - .chain_bigint(&z) - .chain_bigint(&BigInt::from_bytes(u1.to_bytes(true).as_ref())) - .chain_bigint(&u2) - .chain_bigint(&u3) - .result_bigint(); - - let s1 = &e * witness.x.to_bigint() + alpha; - let s2 = commitment_unknown_order(&witness.r, &beta, &statement.ek.n, &e, &BigInt::one()); - let s3 = &e * rho + gamma; - - PDLwSlackProof { z, u1, u2, u3, s1, s2, s3 } - } - - pub fn verify(&self, statement: &PDLwSlackStatement) -> Result<(), ZkPdlWithSlackError> { - let e = Sha256::new() - .chain_bigint(&BigInt::from_bytes(statement.G.to_bytes(true).as_ref())) - .chain_bigint(&BigInt::from_bytes(statement.Q.to_bytes(true).as_ref())) - .chain_bigint(&statement.ciphertext) - .chain_bigint(&self.z) - .chain_bigint(&BigInt::from_bytes(self.u1.to_bytes(true).as_ref())) - .chain_bigint(&self.u2) - .chain_bigint(&self.u3) - .result_bigint(); - - let g_s1 = statement.G.clone() * &Scalar::::from(&self.s1); - let e_fe_neg: Scalar = - Scalar::::from(&(Scalar::::group_order() - &e)); - let y_minus_e = &statement.Q * &e_fe_neg; - let u1_test = g_s1 + y_minus_e; - - let u2_test_tmp = commitment_unknown_order( - &(&statement.ek.n + BigInt::one()), - &self.s2, - &statement.ek.nn, - &self.s1, - &statement.ek.n, - ); - let u2_test = commitment_unknown_order( - &u2_test_tmp, - &statement.ciphertext, - &statement.ek.nn, - &BigInt::one(), - &(-&e), - ); - - let u3_test_tmp = commitment_unknown_order( - &statement.h1, - &statement.h2, - &statement.N_tilde, - &self.s1, - &self.s3, - ); - let u3_test = commitment_unknown_order( - &u3_test_tmp, - &self.z, - &statement.N_tilde, - &BigInt::one(), - &(-&e), - ); - - if self.u1 == u1_test && self.u2 == u2_test && self.u3 == u3_test { - Ok(()) - } else { - Err(ZkPdlWithSlackError::Verify) - } - } + pub fn prove( + witness: &PDLwSlackWitness, + statement: &PDLwSlackStatement, + ) -> Self { + let q3 = Scalar::::group_order().pow(3); + let q_N_tilde = Scalar::::group_order() * &statement.N_tilde; + let q3_N_tilde = &q3 * &statement.N_tilde; + + let alpha = BigInt::sample_below(&q3); + let one = BigInt::one(); + let beta = BigInt::sample_range(&one, &(&statement.ek.n - &one)); + let rho = BigInt::sample_below(&q_N_tilde); + let gamma = BigInt::sample_below(&q3_N_tilde); + + let z = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &witness.x.to_bigint(), + &rho, + ); + let u1 = &statement.G * &Scalar::::from(&alpha); + let u2 = commitment_unknown_order( + &(&statement.ek.n + BigInt::one()), + &beta, + &statement.ek.nn, + &alpha, + &statement.ek.n, + ); + let u3 = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &alpha, + &gamma, + ); + + let e = Sha256::new() + .chain_bigint(&BigInt::from_bytes( + statement.G.to_bytes(true).as_ref(), + )) + .chain_bigint(&BigInt::from_bytes( + statement.Q.to_bytes(true).as_ref(), + )) + .chain_bigint(&statement.ciphertext) + .chain_bigint(&z) + .chain_bigint(&BigInt::from_bytes(u1.to_bytes(true).as_ref())) + .chain_bigint(&u2) + .chain_bigint(&u3) + .result_bigint(); + + let s1 = &e * witness.x.to_bigint() + alpha; + let s2 = commitment_unknown_order( + &witness.r, + &beta, + &statement.ek.n, + &e, + &BigInt::one(), + ); + let s3 = &e * rho + gamma; + + PDLwSlackProof { z, u1, u2, u3, s1, s2, s3 } + } + + pub fn verify( + &self, + statement: &PDLwSlackStatement, + ) -> Result<(), ZkPdlWithSlackError> { + let e = Sha256::new() + .chain_bigint(&BigInt::from_bytes( + statement.G.to_bytes(true).as_ref(), + )) + .chain_bigint(&BigInt::from_bytes( + statement.Q.to_bytes(true).as_ref(), + )) + .chain_bigint(&statement.ciphertext) + .chain_bigint(&self.z) + .chain_bigint(&BigInt::from_bytes(self.u1.to_bytes(true).as_ref())) + .chain_bigint(&self.u2) + .chain_bigint(&self.u3) + .result_bigint(); + + let g_s1 = statement.G.clone() * &Scalar::::from(&self.s1); + let e_fe_neg: Scalar = Scalar::::from( + &(Scalar::::group_order() - &e), + ); + let y_minus_e = &statement.Q * &e_fe_neg; + let u1_test = g_s1 + y_minus_e; + + let u2_test_tmp = commitment_unknown_order( + &(&statement.ek.n + BigInt::one()), + &self.s2, + &statement.ek.nn, + &self.s1, + &statement.ek.n, + ); + let u2_test = commitment_unknown_order( + &u2_test_tmp, + &statement.ciphertext, + &statement.ek.nn, + &BigInt::one(), + &(-&e), + ); + + let u3_test_tmp = commitment_unknown_order( + &statement.h1, + &statement.h2, + &statement.N_tilde, + &self.s1, + &self.s3, + ); + let u3_test = commitment_unknown_order( + &u3_test_tmp, + &self.z, + &statement.N_tilde, + &BigInt::one(), + &(-&e), + ); + + if self.u1 == u1_test && self.u2 == u2_test && self.u3 == u3_test { + Ok(()) + } else { + Err(ZkPdlWithSlackError::Verify) + } + } } pub fn commitment_unknown_order( - h1: &BigInt, - h2: &BigInt, - N_tilde: &BigInt, - x: &BigInt, - r: &BigInt, + h1: &BigInt, + h2: &BigInt, + N_tilde: &BigInt, + x: &BigInt, + r: &BigInt, ) -> BigInt { - let h1_x = BigInt::mod_pow(h1, x, N_tilde); - let h2_r = { - if r < &BigInt::zero() { - let h2_inv = BigInt::mod_inv(h2, N_tilde).unwrap(); - BigInt::mod_pow(&h2_inv, &(-r), N_tilde) - } else { - BigInt::mod_pow(h2, r, N_tilde) - } - }; - BigInt::mod_mul(&h1_x, &h2_r, N_tilde) + let h1_x = BigInt::mod_pow(h1, x, N_tilde); + let h2_r = { + if r < &BigInt::zero() { + let h2_inv = BigInt::mod_inv(h2, N_tilde).unwrap(); + BigInt::mod_pow(&h2_inv, &(-r), N_tilde) + } else { + BigInt::mod_pow(h2, r, N_tilde) + } + }; + BigInt::mod_mul(&h1_x, &h2_r, N_tilde) } #[cfg(test)] diff --git a/src/utilities/zk_pdl_with_slack/test.rs b/src/utilities/zk_pdl_with_slack/test.rs index 66442731..314288fa 100644 --- a/src/utilities/zk_pdl_with_slack/test.rs +++ b/src/utilities/zk_pdl_with_slack/test.rs @@ -1,124 +1,128 @@ #![allow(non_snake_case)] use crate::utilities::zk_pdl_with_slack::*; use curv::{ - elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, - BigInt, + elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, + BigInt, }; use paillier::{ - core::Randomness, - traits::{EncryptWithChosenRandomness, KeyGeneration}, - Paillier, RawPlaintext, + core::Randomness, + traits::{EncryptWithChosenRandomness, KeyGeneration}, + Paillier, RawPlaintext, }; use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; #[test] fn test_zk_pdl_with_slack() { - // N_tilde, h1, h2 generation - let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); - // note: safe primes should be used: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&phi); - let S = BigInt::from(2).pow(256_u32); - let xhi = BigInt::sample_below(&S); - let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); - let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; - - let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); - - // generate the scalar secret and Paillier encrypt it - let (ek, _dk) = Paillier::keypair().keys(); - // note: safe primes should be used here as well: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let randomness = Randomness::sample(&ek); - let x = Scalar::::random(); - - let Q = Point::generator() * &x; - - let c = Paillier::encrypt_with_chosen_randomness( - &ek, - RawPlaintext::from(x.to_bigint()), - &randomness, - ) - .0 - .into_owned(); - - // Generate PDL with slack statement, witness and proof - let pdl_w_slack_statement = PDLwSlackStatement { - ciphertext: c, - ek, - Q, - G: Point::generator().to_point(), - h1, - h2, - N_tilde: ek_tilde.n, - }; - - let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; - - let proof = PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); - // verify h1,h2, N_tilde - let setup_result = composite_dlog_proof.verify(&statement); - assert!(setup_result.is_ok()); - let result = proof.verify(&pdl_w_slack_statement); - assert!(result.is_ok()); + // N_tilde, h1, h2 generation + let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); + // note: safe primes should be used: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&phi); + let S = BigInt::from(2).pow(256_u32); + let xhi = BigInt::sample_below(&S); + let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); + let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); + let statement = + DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; + + let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); + + // generate the scalar secret and Paillier encrypt it + let (ek, _dk) = Paillier::keypair().keys(); + // note: safe primes should be used here as well: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let randomness = Randomness::sample(&ek); + let x = Scalar::::random(); + + let Q = Point::generator() * &x; + + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint()), + &randomness, + ) + .0 + .into_owned(); + + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: c, + ek, + Q, + G: Point::generator().to_point(), + h1, + h2, + N_tilde: ek_tilde.n, + }; + + let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; + + let proof = + PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); + // verify h1,h2, N_tilde + let setup_result = composite_dlog_proof.verify(&statement); + assert!(setup_result.is_ok()); + let result = proof.verify(&pdl_w_slack_statement); + assert!(result.is_ok()); } #[test] #[should_panic] fn test_zk_pdl_with_slack_soundness() { - // N_tilde, h1, h2 generation - let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); - // note: safe primes should be used: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&phi); - let S = BigInt::from(2).pow(256_u32); - let xhi = BigInt::sample_below(&S); - let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); - let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - let statement = DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; - - let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); - - // generate the scalar secret and Paillier encrypt it - let (ek, _dk) = Paillier::keypair().keys(); - // note: safe primes should be used here as well: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); - let randomness = Randomness::sample(&ek); - let x = Scalar::::random(); - - let Q = Point::generator() * &x; - - // here we encrypt x + 1 instead of x: - let c = Paillier::encrypt_with_chosen_randomness( - &ek, - RawPlaintext::from(x.to_bigint() + BigInt::one()), - &randomness, - ) - .0 - .into_owned(); - - // Generate PDL with slack statement, witness and proof - let pdl_w_slack_statement = PDLwSlackStatement { - ciphertext: c, - ek, - Q, - G: Point::generator().to_point(), - h1, - h2, - N_tilde: ek_tilde.n, - }; - - let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; - - let proof = PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); - // verify h1,h2, N_tilde - let setup_result = composite_dlog_proof.verify(&statement); - assert!(setup_result.is_ok()); - let result = proof.verify(&pdl_w_slack_statement); - assert!(result.is_ok()); + // N_tilde, h1, h2 generation + let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); + // note: safe primes should be used: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let one = BigInt::one(); + let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); + let h1 = BigInt::sample_below(&phi); + let S = BigInt::from(2).pow(256_u32); + let xhi = BigInt::sample_below(&S); + let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); + let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); + let statement = + DLogStatement { N: ek_tilde.n.clone(), g: h1.clone(), ni: h2.clone() }; + + let composite_dlog_proof = CompositeDLogProof::prove(&statement, &xhi); + + // generate the scalar secret and Paillier encrypt it + let (ek, _dk) = Paillier::keypair().keys(); + // note: safe primes should be used here as well: + // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys(); + let randomness = Randomness::sample(&ek); + let x = Scalar::::random(); + + let Q = Point::generator() * &x; + + // here we encrypt x + 1 instead of x: + let c = Paillier::encrypt_with_chosen_randomness( + &ek, + RawPlaintext::from(x.to_bigint() + BigInt::one()), + &randomness, + ) + .0 + .into_owned(); + + // Generate PDL with slack statement, witness and proof + let pdl_w_slack_statement = PDLwSlackStatement { + ciphertext: c, + ek, + Q, + G: Point::generator().to_point(), + h1, + h2, + N_tilde: ek_tilde.n, + }; + + let pdl_w_slack_witness = PDLwSlackWitness { x, r: randomness.0 }; + + let proof = + PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); + // verify h1,h2, N_tilde + let setup_result = composite_dlog_proof.verify(&statement); + assert!(setup_result.is_ok()); + let result = proof.verify(&pdl_w_slack_statement); + assert!(result.is_ok()); } From 4ade43686b0a40b1673664cb46c9286262fcd5f5 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 10:14:15 +0800 Subject: [PATCH 13/41] Fix formatting. --- src/utilities/mta/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/mta/mod.rs b/src/utilities/mta/mod.rs index ecff7aac..79bb3d86 100644 --- a/src/utilities/mta/mod.rs +++ b/src/utilities/mta/mod.rs @@ -44,8 +44,8 @@ mod test; pub struct MessageA { pub c: BigInt, // paillier encryption pub range_proofs: Vec, /* proofs (using other parties' - * h1,h2,N_tilde) that the - * plaintext is small */ + * h1,h2,N_tilde) that the + * plaintext is small */ } #[derive(Clone, Debug, Serialize, Deserialize)] From 2802ccf08ab9ad8f47df3ba1a257a0ae484716bc Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 10:17:53 +0800 Subject: [PATCH 14/41] Update build workflow. Need the default features to specify the curv GMP library. --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 7d9b32c4..c5f6d737 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@v2 - uses: hecrj/setup-rust-action@v1 - - run: cargo check --workspace --verbose --no-default-features + - run: cargo check --workspace --verbose # ensures proper formatting and clippy lint lint-check: name: lint From 97ec8af3d27db5c2a17051b68ac70760de09359e Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 10:23:44 +0800 Subject: [PATCH 15/41] Fix top-level clippy warnings. --- src/party_i.rs | 2 +- src/presign/mod.rs | 10 ++++----- src/presign/rounds.rs | 39 +++++++++++------------------------- src/refresh/state_machine.rs | 1 + src/sign/rounds.rs | 1 - 5 files changed, 19 insertions(+), 34 deletions(-) diff --git a/src/party_i.rs b/src/party_i.rs index 634789ca..dc26302b 100644 --- a/src/party_i.rs +++ b/src/party_i.rs @@ -396,7 +396,7 @@ impl Keys { vss.commitments.iter().enumerate() { global_coefficients[i] = - &global_coefficients[i] + &*coefficient_commitment; + &global_coefficients[i] + coefficient_commitment; } } diff --git a/src/presign/mod.rs b/src/presign/mod.rs index 096b9092..d63d6898 100644 --- a/src/presign/mod.rs +++ b/src/presign/mod.rs @@ -79,15 +79,15 @@ impl Zeroize for SSID { self.X.pk_vec = vec![]; for encryption_key in self.X.paillier_key_vec.iter_mut() { - (*encryption_key).n.zeroize(); - (*encryption_key).nn.zeroize(); + encryption_key.n.zeroize(); + encryption_key.nn.zeroize(); } // TODO: Zeroize directly if this is insufficient self.X.y_sum_s = Point::zero(); for dlog_statement in self.X.h1_h2_n_tilde_vec.iter_mut() { - (*dlog_statement).N.zeroize(); - (*dlog_statement).g.zeroize(); - (*dlog_statement).ni.zeroize(); + dlog_statement.N.zeroize(); + dlog_statement.g.zeroize(); + dlog_statement.ni.zeroize(); } self.X.vss_scheme.parameters.threshold.zeroize(); self.X.vss_scheme.parameters.share_count.zeroize(); diff --git a/src/presign/rounds.rs b/src/presign/rounds.rs index 5855b18b..fe2ed53f 100644 --- a/src/presign/rounds.rs +++ b/src/presign/rounds.rs @@ -738,10 +738,8 @@ impl Round2 { } // Gamma = Prod_j (Gamma_j) - let Gamma = Gammas - .values() - .into_iter() - .fold(self.Gamma_i.clone(), |acc, x| acc + x); + let Gamma = + Gammas.values().fold(self.Gamma_i.clone(), |acc, x| acc + x); // Delta = Gamma^{k_i} let Delta_i = Gamma.clone() * Scalar::from_bigint(&self.k_i); @@ -782,30 +780,20 @@ impl Round2 { } // Sum alpha_i_j's - let sum_of_alphas = alpha_i - .values() - .into_iter() - .fold(BigInt::zero(), |acc, x| acc.add(x)); + let sum_of_alphas = + alpha_i.values().fold(BigInt::zero(), |acc, x| acc.add(x)); // Sum alpha_hat_i_j's - let sum_of_alpha_hats = alpha_hat_i - .values() - .into_iter() - .fold(BigInt::zero(), |acc, x| acc.add(x)); + let sum_of_alpha_hats = + alpha_hat_i.values().fold(BigInt::zero(), |acc, x| acc.add(x)); // Sum beta_i_j's - let sum_of_betas = self - .beta_i - .values() - .into_iter() - .fold(BigInt::zero(), |acc, x| acc.add(x)); + let sum_of_betas = + self.beta_i.values().fold(BigInt::zero(), |acc, x| acc.add(x)); // Sum beta_hat_i_j's - let sum_of_beta_hats = self - .beta_hat_i - .values() - .into_iter() - .fold(BigInt::zero(), |acc, x| acc.add(x)); + let sum_of_beta_hats = + self.beta_hat_i.values().fold(BigInt::zero(), |acc, x| acc.add(x)); // delta_i = gamma_i * k_i + sum of alpha_i_j's + sum of beta_i_j's mod // q @@ -1016,15 +1004,12 @@ impl Round3 { // delta = sum of delta_j's let delta = deltas .values() - .into_iter() .fold(self.delta_i.clone(), |acc, x| acc.add(x)) .mod_floor(&self.ssid.q); // Compute product of Delta_j's - let product_of_Deltas = Deltas - .values() - .into_iter() - .fold(self.Delta_i.clone(), |acc, x| acc + x); + let product_of_Deltas = + Deltas.values().fold(self.Delta_i.clone(), |acc, x| acc + x); if product_of_Deltas == Point::::generator().as_point() * diff --git a/src/refresh/state_machine.rs b/src/refresh/state_machine.rs index 45d4d1e0..49955179 100644 --- a/src/refresh/state_machine.rs +++ b/src/refresh/state_machine.rs @@ -379,6 +379,7 @@ enum R { pub struct ProtocolMessage(M); #[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(clippy::large_enum_variant)] enum M { Round1( Option< diff --git a/src/sign/rounds.rs b/src/sign/rounds.rs index 4723d4e3..ae9fb1b2 100644 --- a/src/sign/rounds.rs +++ b/src/sign/rounds.rs @@ -146,7 +146,6 @@ impl Round1 { } let sigma: BigInt = sigmas .values() - .into_iter() .fold(self.sigma_i.clone(), |acc, x| acc.add(x)) .mod_floor(&self.ssid.q); From 3de61ae2241ba9dd28dc24d7d7cbf2dea4c670c8 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 10:28:21 +0800 Subject: [PATCH 16/41] Disable link check in CI as it is giving false positives. --- .github/workflows/checks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c5f6d737..d5a8bf67 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -12,6 +12,7 @@ on: jobs: # checks markdown links link-check: + if: ${{ false }} name: links runs-on: ubuntu-latest steps: From f6447f6f42e338c11ef15a8fb6efa199aca57c33 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 10:40:32 +0800 Subject: [PATCH 17/41] Add .cargo/config.toml for MacOS users with homebrew GMP. --- .cargo/config.toml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..5a35d4fc --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,17 @@ +# For people on MacOS that have installed gmp +# using homebrew this means we don't always have to: +# +# RUSTFLAGS="-L/opt/homebrew/lib" cargo test +# +# To get the curz-kzen library compiling with the system GMP +# library. + +[target.aarch64-apple-darwin] +rustflags = [ + "-L", "/opt/homebrew/lib", +] + +[target.x86_64-apple-darwin] +rustflags = [ + "-L", "/opt/homebrew/lib", +] From 6eb79b39ebf658a4b9141d7ddfdd1b4f6a20e992 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 10:56:30 +0800 Subject: [PATCH 18/41] Use patched version of curv-kzen for webassembly fix. --- Cargo.lock | 13 ++++--------- Cargo.toml | 5 +++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bcd1c236..47fbc667 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -582,6 +582,7 @@ dependencies = [ "fs-dkr", "futures 0.3.28", "generic-array 0.14.7", + "getrandom 0.2.10", "hex", "kzen-paillier", "multi-party-ecdsa", @@ -904,12 +905,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "cryptoxide" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35f15e1a0699dd988fed910dd78fdc6407f44654cd12589c91fa44ea67d9159" - [[package]] name = "csv" version = "1.2.2" @@ -983,10 +978,8 @@ dependencies = [ [[package]] name = "curv-kzen" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a616b5f766fd80307f0e83de6326ccd9fe7b3ba4225fe4e12ae3a692a939d07b" +source = "git+https://github.com/tmpfs/curv.git?branch=wasm-bigint#4bbe397e487bdaa84140433d08277c668e1e1f4e" dependencies = [ - "cryptoxide", "curve25519-dalek", "digest 0.9.0", "ff-zeroize", @@ -1553,8 +1546,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3e21917b..c36df13f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ digest = "0.9" generic-array = "0.14" rand_chacha = "0.3.1" rand = "0.8.5" +getrandom = {version = "0.2", optional = true} [dependencies.paillier] version = "0.4.3" @@ -59,3 +60,7 @@ secp256k1 = { version = "0.20", features = ["global-context"]} default = ["rust-gmp-kzen"] rust-gmp-kzen = ["fs-dkr/rust-gmp-kzen"] num-bigint = ["fs-dkr/num-bigint"] +js = ["getrandom/js"] + +[patch.crates-io] +curv-kzen = { git = "https://github.com/tmpfs/curv.git", branch = "wasm-bigint" } From 0c5cb652e015e91fc6a490645667b4f1e67fcda3 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 11:02:19 +0800 Subject: [PATCH 19/41] Add workflow to check wasm32-unknown-unknown build. --- .github/workflows/checks.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d5a8bf67..53233060 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -38,6 +38,33 @@ jobs: - uses: actions/checkout@v2 - uses: hecrj/setup-rust-action@v1 - run: cargo check --workspace --verbose + + # check compilation for wasm32-unknown-unknown + webassembly-check: + name: lint + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + + - uses: actions/checkout@v2 + - name: Install nightly toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly-2023-10-01 + + - name: Rust Cache + uses: Swatinem/rust-cache@v1.3.0 + + - name: Run Webassembly check + run: | + cargo check --target wasm32-unknown-unknown \ + --features js \ + --features num-bigint \ + --no-default-features + # ensures proper formatting and clippy lint lint-check: name: lint From e3d6997d8166afea47fa290f27a5aa6749683d12 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 11:03:47 +0800 Subject: [PATCH 20/41] Fix workflow name. --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 53233060..85398e06 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -41,7 +41,7 @@ jobs: # check compilation for wasm32-unknown-unknown webassembly-check: - name: lint + name: webassembly runs-on: ubuntu-latest steps: - name: Cancel Previous Runs From aacd432ae8399dc37ae02c4127073c9939eaba49 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 2 Oct 2023 11:13:29 +0800 Subject: [PATCH 21/41] Re-export multi_party_ecdsa library. --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 9e8d27e7..191410cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,8 @@ pub mod sign; pub mod traits; pub mod utilities; +pub use multi_party_ecdsa as mpc_ecdsa; + #[derive(Copy, PartialEq, Eq, Clone, Debug)] pub enum Error { InvalidKey, From e90a475fdf4e4184e6fb73ffab2903d21b2f245b Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 08:35:11 +0800 Subject: [PATCH 22/41] Remove two_party_ecdsa module. --- multi-party-ecdsa/src/protocols/mod.rs | 1 - .../two_party_ecdsa/cclst_2019/mod.rs | 23 - .../two_party_ecdsa/cclst_2019/party_one.rs | 419 ------------ .../two_party_ecdsa/cclst_2019/party_two.rs | 356 ---------- .../two_party_ecdsa/cclst_2019/test.rs | 142 ---- .../two_party_ecdsa/lindell_2017/mod.rs | 23 - .../two_party_ecdsa/lindell_2017/party_one.rs | 607 ------------------ .../two_party_ecdsa/lindell_2017/party_two.rs | 426 ------------ .../two_party_ecdsa/lindell_2017/test.rs | 137 ---- .../src/protocols/two_party_ecdsa/mod.rs | 24 - 10 files changed, 2158 deletions(-) delete mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/mod.rs delete mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_one.rs delete mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_two.rs delete mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/test.rs delete mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/mod.rs delete mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs delete mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_two.rs delete mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/test.rs delete mode 100644 multi-party-ecdsa/src/protocols/two_party_ecdsa/mod.rs diff --git a/multi-party-ecdsa/src/protocols/mod.rs b/multi-party-ecdsa/src/protocols/mod.rs index 9c17a1a0..373276dc 100644 --- a/multi-party-ecdsa/src/protocols/mod.rs +++ b/multi-party-ecdsa/src/protocols/mod.rs @@ -15,4 +15,3 @@ */ pub mod multi_party_ecdsa; -pub mod two_party_ecdsa; diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/mod.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/mod.rs deleted file mode 100644 index 001cd979..00000000 --- a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -/* - Multi-party ECDSA - - Copyright 2018 by Kzen Networks - - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) - - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - - @license GPL-3.0+ -*/ - -const SECURITY_BITS: usize = 256; - -pub mod party_one; -pub mod party_two; - -#[cfg(test)] -mod test; diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_one.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_one.rs deleted file mode 100644 index 7ee1d573..00000000 --- a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_one.rs +++ /dev/null @@ -1,419 +0,0 @@ -/* - Multi-party ECDSA - - Copyright 2018 by Kzen Networks - - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) - - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - - @license GPL-3.0+ -*/ -use std::cmp; - -use class_group::primitives::cl_dl_public_setup::{ - decrypt, verifiably_encrypt, CLDLProof, CLGroup, Ciphertext as CLCiphertext, PK, SK, -}; - -use curv::arithmetic::*; -use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; -use curv::cryptographic_primitives::commitments::traits::Commitment; -use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; -use curv::cryptographic_primitives::proofs::sigma_dlog::*; -use curv::cryptographic_primitives::proofs::sigma_ec_ddh::*; -use curv::cryptographic_primitives::proofs::ProofError; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use subtle::ConstantTimeEq; - -use super::party_two::EphKeyGenFirstMsg as Party2EphKeyGenFirstMessage; -use super::party_two::EphKeyGenSecondMsg as Party2EphKeyGenSecondMessage; -use super::SECURITY_BITS; -use crate::Error::{self, InvalidSig}; - -//****************** Begin: Party One structs ******************// -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EcKeyPair { - pub public_share: Point, - secret_share: Scalar, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CommWitness { - pub pk_commitment_blind_factor: BigInt, - pub zk_pok_blind_factor: BigInt, - pub public_share: Point, - pub d_log_proof: DLogProof, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KeyGenFirstMsg { - pub pk_commitment: BigInt, - pub zk_pok_commitment: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct KeyGenSecondMsg { - pub comm_witness: CommWitness, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct HSMCL { - pub public: PK, - pub secret: SK, - pub encrypted_share: CLCiphertext, - pub cl_group: CLGroup, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct HSMCLPublic { - pub cl_pub_key: PK, - pub proof: CLDLProof, - pub encrypted_share: CLCiphertext, - pub cl_group: CLGroup, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SignatureRecid { - pub s: BigInt, - pub r: BigInt, - pub recid: u8, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Signature { - pub s: BigInt, - pub r: BigInt, -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct Party1Private { - x1: Scalar, - hsmcl_pub: PK, - hsmcl_priv: SK, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLFirstMessage { - pub c_hat: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLdecommit { - pub q_hat: Point, - pub blindness: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLSecondMessage { - pub decommit: PDLdecommit, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EphEcKeyPair { - pub public_share: Point, - secret_share: Scalar, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct EphKeyGenFirstMsg { - pub d_log_proof: ECDDHProof, - pub public_share: Point, - pub c: Point, //c = secret_share * base_point2 -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct EphKeyGenSecondMsg {} - -//****************** End: Party One structs ******************// - -impl KeyGenFirstMsg { - pub fn create_commitments() -> (KeyGenFirstMsg, CommWitness, EcKeyPair) { - let base = Point::generator(); - - let secret_share = Scalar::::random(); - //in Lindell's protocol range proof works only for x1 = - Scalar::::from(&secret_share.to_bigint().div_floor(&BigInt::from(3))); - - let public_share = base * &secret_share; - - let d_log_proof = DLogProof::prove(&secret_share); - // we use hash based commitment - let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); - let pk_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), - &pk_commitment_blind_factor, - ); - - let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); - let zk_pok_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(d_log_proof.pk_t_rand_commitment.to_bytes(true).as_ref()), - &zk_pok_blind_factor, - ); - let ec_key_pair = EcKeyPair { - public_share, - secret_share, - }; - ( - KeyGenFirstMsg { - pk_commitment, - zk_pok_commitment, - }, - CommWitness { - pk_commitment_blind_factor, - zk_pok_blind_factor, - public_share: ec_key_pair.public_share.clone(), - d_log_proof, - }, - ec_key_pair, - ) - } - - pub fn create_commitments_with_fixed_secret_share( - secret_share: Scalar, - ) -> (KeyGenFirstMsg, CommWitness, EcKeyPair) { - let base = Point::generator(); - let public_share = base * &secret_share; - - let d_log_proof = DLogProof::prove(&secret_share); - - let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); - let pk_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), - &pk_commitment_blind_factor, - ); - - let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); - let zk_pok_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(d_log_proof.pk_t_rand_commitment.to_bytes(true).as_ref()), - &zk_pok_blind_factor, - ); - - let ec_key_pair = EcKeyPair { - public_share, - secret_share, - }; - ( - KeyGenFirstMsg { - pk_commitment, - zk_pok_commitment, - }, - CommWitness { - pk_commitment_blind_factor, - zk_pok_blind_factor, - public_share: ec_key_pair.public_share.clone(), - d_log_proof, - }, - ec_key_pair, - ) - } -} - -impl KeyGenSecondMsg { - pub fn verify_and_decommit( - comm_witness: CommWitness, - proof: &DLogProof, - ) -> Result { - DLogProof::verify(proof)?; - Ok(KeyGenSecondMsg { comm_witness }) - } -} - -pub fn compute_pubkey( - party_one_private: &Party1Private, - other_share_public_share: &Point, -) -> Point { - other_share_public_share * &party_one_private.x1 -} - -impl Party1Private { - pub fn set_private_key(ec_key: &EcKeyPair, hsmcl: &HSMCL) -> Party1Private { - Party1Private { - x1: ec_key.secret_share.clone(), - hsmcl_pub: hsmcl.public.clone(), - hsmcl_priv: hsmcl.secret.clone(), - } - } -} - -impl HSMCL { - pub fn generate_keypair_and_encrypted_share_and_proof( - keygen: &EcKeyPair, - seed: &BigInt, - ) -> (HSMCL, HSMCLPublic) { - let cl_group = CLGroup::new_from_setup(&1348, seed); - let (secret_key, public_key) = cl_group.keygen(); - let (ciphertext, proof) = verifiably_encrypt( - &cl_group, - &public_key, - (&keygen.secret_share, &keygen.public_share), - ); - - ( - HSMCL { - cl_group: cl_group.clone(), - public: public_key.clone(), - secret: secret_key, - encrypted_share: ciphertext.clone(), - }, - HSMCLPublic { - cl_pub_key: public_key, - proof, - encrypted_share: ciphertext, - cl_group, - }, - ) - } -} - -impl EphKeyGenFirstMsg { - pub fn create() -> (EphKeyGenFirstMsg, EphEcKeyPair) { - let base = Point::generator(); - let secret_share = Scalar::::random(); - let public_share = base * &secret_share; - let h = Point::::base_point2(); - let w = ECDDHWitness { - x: secret_share.clone(), - }; - let c = h * &secret_share; - let delta = ECDDHStatement { - g1: base.to_point(), - h1: public_share.clone(), - g2: h.clone(), - h2: c.clone(), - }; - let d_log_proof = ECDDHProof::prove(&w, &delta); - let ec_key_pair = EphEcKeyPair { - public_share: public_share.clone(), - secret_share, - }; - ( - EphKeyGenFirstMsg { - d_log_proof, - public_share, - c, - }, - ec_key_pair, - ) - } -} - -impl EphKeyGenSecondMsg { - pub fn verify_commitments_and_dlog_proof( - party_two_first_message: &Party2EphKeyGenFirstMessage, - party_two_second_message: &Party2EphKeyGenSecondMessage, - ) -> Result { - let party_two_pk_commitment = &party_two_first_message.pk_commitment; - let party_two_zk_pok_commitment = &party_two_first_message.zk_pok_commitment; - let party_two_zk_pok_blind_factor = - &party_two_second_message.comm_witness.zk_pok_blind_factor; - let party_two_public_share = &party_two_second_message.comm_witness.public_share; - let party_two_pk_commitment_blind_factor = &party_two_second_message - .comm_witness - .pk_commitment_blind_factor; - let party_two_d_log_proof = &party_two_second_message.comm_witness.d_log_proof; - let mut flag = true; - if party_two_pk_commitment - != &HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(party_two_public_share.to_bytes(true).as_ref()), - party_two_pk_commitment_blind_factor, - ) - { - flag = false - } - if party_two_zk_pok_commitment - != &HashCommitment::::create_commitment_with_user_defined_randomness( - &Sha256::new() - .chain_points([&party_two_d_log_proof.a1, &party_two_d_log_proof.a2]) - .result_bigint(), - party_two_zk_pok_blind_factor, - ) - { - flag = false - } - if !flag { - return Err(ProofError); - } - let delta = ECDDHStatement { - g1: Point::generator().to_point(), - h1: party_two_public_share.clone(), - g2: Point::::base_point2().clone(), - h2: party_two_second_message.comm_witness.c.clone(), - }; - party_two_d_log_proof.verify(&delta)?; - Ok(EphKeyGenSecondMsg {}) - } -} - -impl Signature { - pub fn compute( - hsmcl: &HSMCL, - party_one_private: &Party1Private, - partial_sig_c3: CLCiphertext, - ephemeral_local_share: &EphEcKeyPair, - ephemeral_other_public_share: &Point, - ) -> Signature { - //compute r = k2* R1 - let r = ephemeral_other_public_share * &ephemeral_local_share.secret_share; - - let rx = r - .x_coord() - .unwrap() - .mod_floor(Scalar::::group_order()); - let k1 = &ephemeral_local_share.secret_share.to_bigint(); - let k1_inv = BigInt::mod_inv(k1, Scalar::::group_order()).unwrap(); - let s_tag = decrypt( - &hsmcl.cl_group, - &party_one_private.hsmcl_priv, - &partial_sig_c3, - ); - let s_tag_tag = BigInt::mod_mul( - &k1_inv, - &s_tag.to_bigint(), - Scalar::::group_order(), - ); - let s = cmp::min( - s_tag_tag.clone(), - Scalar::::group_order().clone() - s_tag_tag, - ); - Signature { s, r: rx } - } -} - -pub fn verify( - signature: &Signature, - pubkey: &Point, - message: &BigInt, -) -> Result<(), Error> { - let s_fe = Scalar::::from(&signature.s); - let rx_fe = Scalar::::from(&signature.r); - - let s_inv_fe = s_fe.invert().ok_or(Error::InvalidSig)?; - let e_fe: Scalar = - Scalar::::from(&message.mod_floor(Scalar::::group_order())); - let u1 = Point::generator() * e_fe * &s_inv_fe; - let u2 = pubkey * rx_fe * &s_inv_fe; - - // second condition is against malleability - let rx_bytes = &BigInt::to_bytes(&signature.r)[..]; - let u1_plus_u2_bytes = &BigInt::to_bytes(&(u1 + u2).x_coord().unwrap())[..]; - - if rx_bytes.ct_eq(u1_plus_u2_bytes).unwrap_u8() == 1 - && signature.s < Scalar::::group_order() - signature.s.clone() - { - Ok(()) - } else { - Err(InvalidSig) - } -} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_two.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_two.rs deleted file mode 100644 index 55f2aeef..00000000 --- a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/party_two.rs +++ /dev/null @@ -1,356 +0,0 @@ -/* - Multi-party ECDSA - - Copyright 2018 by Kzen Networks - - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) - - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - - @license GPL-3.0+ -*/ -use super::party_one::HSMCLPublic; -use class_group::primitives::cl_dl_public_setup::PK as HSMCLPK; -use class_group::primitives::cl_dl_public_setup::{ - encrypt, eval_scal, eval_sum, CLGroup, Ciphertext as CLCiphertext, -}; -use curv::arithmetic::traits::*; -use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; -use curv::cryptographic_primitives::commitments::traits::Commitment; -use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; -use curv::cryptographic_primitives::proofs::sigma_dlog::*; -use curv::cryptographic_primitives::proofs::sigma_ec_ddh::*; -use curv::cryptographic_primitives::proofs::ProofError; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; - -use super::party_one::EphKeyGenFirstMsg as Party1EphKeyGenFirstMsg; -use super::party_one::KeyGenFirstMsg as Party1KeyGenFirstMessage; -use super::party_one::KeyGenSecondMsg as Party1KeyGenSecondMessage; -use super::SECURITY_BITS; - -//****************** Begin: Party Two structs ******************// - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EcKeyPair { - pub public_share: Point, - secret_share: Scalar, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KeyGenFirstMsg { - pub d_log_proof: DLogProof, - pub public_share: Point, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct KeyGenSecondMsg {} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Party2Public { - pub group: CLGroup, - pub ek: HSMCLPK, - pub encrypted_secret_share: CLCiphertext, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PartialSig { - pub c3: CLCiphertext, -} - -#[derive(Serialize, Deserialize)] -pub struct Party2Private { - x2: Scalar, -} -#[derive(Debug)] -pub struct PDLchallenge { - pub c_tag: BigInt, - pub c_tag_tag: BigInt, - a: BigInt, - b: BigInt, - blindness: BigInt, - q_tag: Point, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLFirstMessage { - pub c_tag: BigInt, - pub c_tag_tag: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLdecommit { - pub a: BigInt, - pub b: BigInt, - pub blindness: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLSecondMessage { - pub decommit: PDLdecommit, -} -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EphEcKeyPair { - pub public_share: Point, - secret_share: Scalar, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EphCommWitness { - pub pk_commitment_blind_factor: BigInt, - pub zk_pok_blind_factor: BigInt, - pub public_share: Point, - pub d_log_proof: ECDDHProof, - pub c: Point, //c = secret_share * base_point2 -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EphKeyGenFirstMsg { - pub pk_commitment: BigInt, - pub zk_pok_commitment: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct EphKeyGenSecondMsg { - pub comm_witness: EphCommWitness, -} - -//****************** End: Party Two structs ******************// - -impl KeyGenFirstMsg { - pub fn create() -> (KeyGenFirstMsg, EcKeyPair) { - let base = Point::generator(); - let secret_share = Scalar::::random(); - let public_share = base * &secret_share; - let d_log_proof = DLogProof::prove(&secret_share); - let ec_key_pair = EcKeyPair { - public_share: public_share.clone(), - secret_share, - }; - ( - KeyGenFirstMsg { - d_log_proof, - public_share, - }, - ec_key_pair, - ) - } - - pub fn create_with_fixed_secret_share( - secret_share: Scalar, - ) -> (KeyGenFirstMsg, EcKeyPair) { - let base = Point::generator(); - let public_share = base * &secret_share; - let d_log_proof = DLogProof::prove(&secret_share); - let ec_key_pair = EcKeyPair { - public_share: public_share.clone(), - secret_share, - }; - ( - KeyGenFirstMsg { - d_log_proof, - public_share, - }, - ec_key_pair, - ) - } -} - -impl KeyGenSecondMsg { - pub fn verify_commitments_and_dlog_proof( - party_one_first_message: &Party1KeyGenFirstMessage, - party_one_second_message: &Party1KeyGenSecondMessage, - ) -> Result { - let party_one_pk_commitment = &party_one_first_message.pk_commitment; - let party_one_zk_pok_commitment = &party_one_first_message.zk_pok_commitment; - let party_one_zk_pok_blind_factor = - &party_one_second_message.comm_witness.zk_pok_blind_factor; - let party_one_public_share = &party_one_second_message.comm_witness.public_share; - let party_one_pk_commitment_blind_factor = &party_one_second_message - .comm_witness - .pk_commitment_blind_factor; - let party_one_d_log_proof = &party_one_second_message.comm_witness.d_log_proof; - - let mut flag = true; - if party_one_pk_commitment - != &HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(party_one_public_share.to_bytes(true).as_ref()), - party_one_pk_commitment_blind_factor, - ) - { - flag = false - } - if party_one_zk_pok_commitment - != &HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes( - party_one_d_log_proof - .pk_t_rand_commitment - .to_bytes(true) - .as_ref(), - ), - party_one_zk_pok_blind_factor, - ) - { - flag = false - } - - if !flag { - return Err(ProofError); - } - - DLogProof::verify(party_one_d_log_proof)?; - Ok(KeyGenSecondMsg {}) - } -} - -pub fn compute_pubkey( - local_share: &EcKeyPair, - other_share_public_share: &Point, -) -> Point { - other_share_public_share * &local_share.secret_share -} - -impl Party2Private { - pub fn set_private_key(ec_key: &EcKeyPair) -> Party2Private { - Party2Private { - x2: ec_key.secret_share.clone(), - } - } -} - -impl Party2Public { - pub fn verify_setup_and_zkcldl_proof( - hsmcl_public: &HSMCLPublic, - seed: &BigInt, - party1_ec_pubkey: &Point, - ) -> Result { - let setup_verify = hsmcl_public.cl_group.setup_verify(seed); - - let proof_verify = hsmcl_public.proof.verify( - &hsmcl_public.cl_group, - &hsmcl_public.cl_pub_key, - &hsmcl_public.encrypted_share, - party1_ec_pubkey, - ); - if proof_verify.is_ok() && setup_verify.is_ok() { - Ok(Party2Public { - group: hsmcl_public.cl_group.clone(), - ek: hsmcl_public.cl_pub_key.clone(), - encrypted_secret_share: hsmcl_public.encrypted_share.clone(), - }) - } else { - Err(ProofError) - } - } -} - -impl EphKeyGenFirstMsg { - pub fn create_commitments() -> (EphKeyGenFirstMsg, EphCommWitness, EphEcKeyPair) { - let base = Point::generator(); - - let secret_share = Scalar::::random(); - - let public_share = base * &secret_share; - - let h = Point::base_point2(); - let w = ECDDHWitness { - x: secret_share.clone(), - }; - let c = h * &secret_share; - let delta = ECDDHStatement { - g1: base.to_point(), - h1: public_share.clone(), - g2: h.clone(), - h2: c.clone(), - }; - let d_log_proof = ECDDHProof::prove(&w, &delta); - - // we use hash based commitment - let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); - let pk_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), - &pk_commitment_blind_factor, - ); - - let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); - let zk_pok_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &Sha256::new() - .chain_points([&d_log_proof.a1, &d_log_proof.a2]) - .result_bigint(), - &zk_pok_blind_factor, - ); - - let ec_key_pair = EphEcKeyPair { - public_share, - secret_share, - }; - ( - EphKeyGenFirstMsg { - pk_commitment, - zk_pok_commitment, - }, - EphCommWitness { - pk_commitment_blind_factor, - zk_pok_blind_factor, - public_share: ec_key_pair.public_share.clone(), - d_log_proof, - c, - }, - ec_key_pair, - ) - } -} - -impl EphKeyGenSecondMsg { - pub fn verify_and_decommit( - comm_witness: EphCommWitness, - party_one_first_message: &Party1EphKeyGenFirstMsg, - ) -> Result { - let delta = ECDDHStatement { - g1: Point::generator().to_point(), - h1: party_one_first_message.public_share.clone(), - g2: Point::::base_point2().clone(), - h2: party_one_first_message.c.clone(), - }; - party_one_first_message.d_log_proof.verify(&delta)?; - Ok(EphKeyGenSecondMsg { comm_witness }) - } -} - -impl PartialSig { - pub fn compute( - party_two_public: Party2Public, - local_share: &Party2Private, - ephemeral_local_share: &EphEcKeyPair, - ephemeral_other_public_share: &Point, - message: &BigInt, - ) -> PartialSig { - let q = Scalar::::group_order(); - //compute r = k2* R1 - let r: Point = - ephemeral_other_public_share * &ephemeral_local_share.secret_share; - - let rx = r.x_coord().unwrap().mod_floor(q); - let k2 = &ephemeral_local_share.secret_share.to_bigint(); - let k2_inv = BigInt::mod_inv(k2, q).unwrap(); - let k2_inv_m = BigInt::mod_mul(&k2_inv, message, q); - let k2_inv_m_fe = Scalar::::from(&k2_inv_m); - let c1 = encrypt(&party_two_public.group, &party_two_public.ek, &k2_inv_m_fe); - let v = BigInt::mod_mul(&k2_inv, &local_share.x2.to_bigint(), q); - let v = BigInt::mod_mul(&v, &rx, q); - - let c2 = eval_scal(&party_two_public.encrypted_secret_share, &v); - let c3 = eval_sum(&c1.0, &c2); - - //c3: - PartialSig { c3 } - } -} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/test.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/test.rs deleted file mode 100644 index 87aad0d0..00000000 --- a/multi-party-ecdsa/src/protocols/two_party_ecdsa/cclst_2019/test.rs +++ /dev/null @@ -1,142 +0,0 @@ -// For integration tests, please add your tests in /tests instead - -use super::*; -use curv::arithmetic::Converter; -use curv::elliptic::curves::*; -use curv::BigInt; - -#[test] -fn test_d_log_proof_party_two_party_one() { - let (party_one_first_message, comm_witness, _ec_key_pair_party1) = - party_one::KeyGenFirstMsg::create_commitments(); - let (party_two_first_message, _ec_key_pair_party2) = party_two::KeyGenFirstMsg::create(); - let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( - comm_witness, - &party_two_first_message.d_log_proof, - ) - .expect("failed to verify and decommit"); - - let _party_two_second_message = party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( - &party_one_first_message, - &party_one_second_message, - ) - .expect("failed to verify commitments and DLog proof"); -} - -#[test] -fn test_full_key_gen() { - let (party_one_first_message, comm_witness, ec_key_pair_party1) = - party_one::KeyGenFirstMsg::create_commitments_with_fixed_secret_share( - Scalar::::random(), - ); - let (party_two_first_message, _ec_key_pair_party2) = - party_two::KeyGenFirstMsg::create_with_fixed_secret_share(Scalar::::from( - &BigInt::from(10), - )); - let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( - comm_witness, - &party_two_first_message.d_log_proof, - ) - .expect("failed to verify and decommit"); - - let _party_two_second_message = party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( - &party_one_first_message, - &party_one_second_message, - ) - .expect("failed to verify commitments and DLog proof"); - - // init HSMCL keypair: - let seed: BigInt = BigInt::from_str_radix( - "314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848", - 10, - ).unwrap(); - let (hsmcl, hsmcl_public) = party_one::HSMCL::generate_keypair_and_encrypted_share_and_proof( - &ec_key_pair_party1, - &seed, - ); - - //P1 sends P2 hsmcl_public - let _party_one_private = party_one::Party1Private::set_private_key(&ec_key_pair_party1, &hsmcl); - - let _party_two_hsmcl_pub = party_two::Party2Public::verify_setup_and_zkcldl_proof( - &hsmcl_public, - &seed, - &party_one_second_message.comm_witness.public_share, - ) - .expect("proof error"); -} - -#[test] -fn test_two_party_sign() { - ////////// Simulate KeyGen ///////////////// - // assume party1 and party2 engaged with KeyGen in the past resulting in - // party1 owning private share and HSMCL key-pair - // party2 owning private share and HSMCL encryption of party1 share - let (_party_one_private_share_gen, comm_witness, ec_key_pair_party1) = - party_one::KeyGenFirstMsg::create_commitments(); - let (party_two_private_share_gen, ec_key_pair_party2) = party_two::KeyGenFirstMsg::create(); - - //pi (nothing up my sleeve) - let seed: BigInt = BigInt::from_str_radix( - "314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848", - 10, - ).unwrap(); - - let (party_one_hsmcl, hsmcl_public) = - party_one::HSMCL::generate_keypair_and_encrypted_share_and_proof( - &ec_key_pair_party1, - &seed, - ); - - let party1_private = - party_one::Party1Private::set_private_key(&ec_key_pair_party1, &party_one_hsmcl); - - let party_two_hsmcl_pub = party_two::Party2Public::verify_setup_and_zkcldl_proof( - &hsmcl_public, - &seed, - &comm_witness.public_share, - ) - .expect("proof error"); - - ////////// Start Signing ///////////////// - // creating the ephemeral private shares: - - let (eph_party_two_first_message, eph_comm_witness, eph_ec_key_pair_party2) = - party_two::EphKeyGenFirstMsg::create_commitments(); - let (eph_party_one_first_message, eph_ec_key_pair_party1) = - party_one::EphKeyGenFirstMsg::create(); - let eph_party_two_second_message = party_two::EphKeyGenSecondMsg::verify_and_decommit( - eph_comm_witness, - &eph_party_one_first_message, - ) - .expect("party1 DLog proof failed"); - - let _eph_party_one_second_message = - party_one::EphKeyGenSecondMsg::verify_commitments_and_dlog_proof( - &eph_party_two_first_message, - &eph_party_two_second_message, - ) - .expect("failed to verify commitments and DLog proof"); - let party2_private = party_two::Party2Private::set_private_key(&ec_key_pair_party2); - let message = BigInt::from(1234); - - let partial_sig = party_two::PartialSig::compute( - party_two_hsmcl_pub, - &party2_private, - &eph_ec_key_pair_party2, - &eph_party_one_first_message.public_share, - &message, - ); - - let signature = party_one::Signature::compute( - &party_one_hsmcl, - &party1_private, - partial_sig.c3, - &eph_ec_key_pair_party1, - &eph_party_two_second_message.comm_witness.public_share, - ); - - let pubkey = - party_one::compute_pubkey(&party1_private, &party_two_private_share_gen.public_share); - party_one::verify(&signature, &pubkey, &message).expect("Invalid signature") -} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/mod.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/mod.rs deleted file mode 100644 index 001cd979..00000000 --- a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -/* - Multi-party ECDSA - - Copyright 2018 by Kzen Networks - - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) - - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - - @license GPL-3.0+ -*/ - -const SECURITY_BITS: usize = 256; - -pub mod party_one; -pub mod party_two; - -#[cfg(test)] -mod test; diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs deleted file mode 100644 index ed8a51ad..00000000 --- a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_one.rs +++ /dev/null @@ -1,607 +0,0 @@ -/* - Multi-party ECDSA - - Copyright 2018 by Kzen Networks - - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) - - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - - @license GPL-3.0+ -*/ -use std::cmp; - -use centipede::juggling::proof_system::{Helgamalsegmented, Witness}; -use centipede::juggling::segmentation::Msegmentation; -use curv::arithmetic::traits::*; -use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; -use curv::cryptographic_primitives::commitments::traits::Commitment; -use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; -use curv::cryptographic_primitives::proofs::sigma_dlog::*; -use curv::cryptographic_primitives::proofs::sigma_ec_ddh::*; -use curv::cryptographic_primitives::proofs::ProofError; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; -use paillier::Paillier; -use paillier::{Decrypt, EncryptWithChosenRandomness, KeyGeneration}; -use paillier::{DecryptionKey, EncryptionKey, Randomness, RawCiphertext, RawPlaintext}; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use subtle::ConstantTimeEq; -use zk_paillier::zkproofs::NiCorrectKeyProof; - -use super::party_two::EphKeyGenFirstMsg as Party2EphKeyGenFirstMessage; -use super::party_two::EphKeyGenSecondMsg as Party2EphKeyGenSecondMessage; -use super::SECURITY_BITS; - -use crate::utilities::mta::MessageB; -use crate::Error; - -use crate::utilities::zk_pdl_with_slack::PDLwSlackProof; -use crate::utilities::zk_pdl_with_slack::PDLwSlackStatement; -use crate::utilities::zk_pdl_with_slack::PDLwSlackWitness; -use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; - -//****************** Begin: Party One structs ******************// -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EcKeyPair { - pub public_share: Point, - secret_share: Scalar, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CommWitness { - pub pk_commitment_blind_factor: BigInt, - pub zk_pok_blind_factor: BigInt, - pub public_share: Point, - pub d_log_proof: DLogProof, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KeyGenFirstMsg { - pub pk_commitment: BigInt, - pub zk_pok_commitment: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct KeyGenSecondMsg { - pub comm_witness: CommWitness, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PaillierKeyPair { - pub ek: EncryptionKey, - dk: DecryptionKey, - pub encrypted_share: BigInt, - randomness: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SignatureRecid { - pub s: BigInt, - pub r: BigInt, - pub recid: u8, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Signature { - pub s: BigInt, - pub r: BigInt, -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct Party1Private { - x1: Scalar, - paillier_priv: DecryptionKey, - c_key_randomness: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLFirstMessage { - pub c_hat: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLdecommit { - pub q_hat: Point, - pub blindness: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLSecondMessage { - pub decommit: PDLdecommit, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EphEcKeyPair { - pub public_share: Point, - secret_share: Scalar, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct EphKeyGenFirstMsg { - pub d_log_proof: ECDDHProof, - pub public_share: Point, - pub c: Point, //c = secret_share * base_point2 -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct EphKeyGenSecondMsg {} - -//****************** End: Party One structs ******************// - -impl KeyGenFirstMsg { - pub fn create_commitments() -> (KeyGenFirstMsg, CommWitness, EcKeyPair) { - let base = Point::generator(); - - let secret_share = Scalar::::random(); - - let public_share = base * &secret_share; - - let d_log_proof = DLogProof::::prove(&secret_share); - // we use hash based commitment - let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); - let pk_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), - &pk_commitment_blind_factor, - ); - - let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); - let zk_pok_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(d_log_proof.pk_t_rand_commitment.to_bytes(true).as_ref()), - &zk_pok_blind_factor, - ); - let ec_key_pair = EcKeyPair { - public_share, - secret_share, - }; - ( - KeyGenFirstMsg { - pk_commitment, - zk_pok_commitment, - }, - CommWitness { - pk_commitment_blind_factor, - zk_pok_blind_factor, - public_share: ec_key_pair.public_share.clone(), - d_log_proof, - }, - ec_key_pair, - ) - } - - pub fn create_commitments_with_fixed_secret_share( - secret_share: Scalar, - ) -> (KeyGenFirstMsg, CommWitness, EcKeyPair) { - let base = Point::generator(); - let public_share = base * &secret_share; - - let d_log_proof = DLogProof::::prove(&secret_share); - - let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); - let pk_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), - &pk_commitment_blind_factor, - ); - - let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); - let zk_pok_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(d_log_proof.pk_t_rand_commitment.to_bytes(true).as_ref()), - &zk_pok_blind_factor, - ); - - let ec_key_pair = EcKeyPair { - public_share, - secret_share, - }; - ( - KeyGenFirstMsg { - pk_commitment, - zk_pok_commitment, - }, - CommWitness { - pk_commitment_blind_factor, - zk_pok_blind_factor, - public_share: ec_key_pair.public_share.clone(), - d_log_proof, - }, - ec_key_pair, - ) - } -} - -impl KeyGenSecondMsg { - pub fn verify_and_decommit( - comm_witness: CommWitness, - proof: &DLogProof, - ) -> Result { - DLogProof::verify(proof)?; - Ok(KeyGenSecondMsg { comm_witness }) - } -} - -pub fn compute_pubkey( - party_one_private: &Party1Private, - other_share_public_share: &Point, -) -> Point { - other_share_public_share * &party_one_private.x1 -} - -impl Party1Private { - pub fn set_private_key(ec_key: &EcKeyPair, paillier_key: &PaillierKeyPair) -> Party1Private { - Party1Private { - x1: ec_key.secret_share.clone(), - paillier_priv: paillier_key.dk.clone(), - c_key_randomness: paillier_key.randomness.clone(), - } - } - pub fn refresh_private_key( - party_one_private: &Party1Private, - factor: &BigInt, - ) -> ( - EncryptionKey, - BigInt, - Party1Private, - NiCorrectKeyProof, - PDLwSlackStatement, - PDLwSlackProof, - CompositeDLogProof, - ) { - let (ek_new, dk_new) = Paillier::keypair().keys(); - let randomness = Randomness::sample(&ek_new); - let factor_fe = Scalar::::from(factor); - let x1_new = &party_one_private.x1 * factor_fe; - let c_key_new = Paillier::encrypt_with_chosen_randomness( - &ek_new, - RawPlaintext::from(x1_new.to_bigint()), - &randomness, - ) - .0 - .into_owned(); - let correct_key_proof_new = NiCorrectKeyProof::proof(&dk_new, None); - - let paillier_key_pair = PaillierKeyPair { - ek: ek_new.clone(), - dk: dk_new.clone(), - encrypted_share: c_key_new.clone(), - randomness: randomness.0.clone(), - }; - - let party_one_private_new = Party1Private { - x1: x1_new, - paillier_priv: dk_new, - c_key_randomness: randomness.0, - }; - - let (pdl_statement, pdl_proof, composite_dlog_proof) = - PaillierKeyPair::pdl_proof(&party_one_private_new, &paillier_key_pair); - - ( - ek_new, - c_key_new, - party_one_private_new, - correct_key_proof_new, - pdl_statement, - pdl_proof, - composite_dlog_proof, - ) - } - - // used for verifiable recovery - pub fn to_encrypted_segment( - &self, - segment_size: usize, - num_of_segments: usize, - pub_ke_y: &Point, - g: &Point, - ) -> (Witness, Helgamalsegmented) { - Msegmentation::to_encrypted_segments(&self.x1, &segment_size, num_of_segments, pub_ke_y, g) - } - - // used to transform lindell master key to gg18 master key - pub fn to_mta_message_b( - &self, - message_b: MessageB, - ) -> Result<(Scalar, BigInt), Error> { - message_b.verify_proofs_get_alpha(&self.paillier_priv, &self.x1) - } -} - -impl PaillierKeyPair { - pub fn generate_keypair_and_encrypted_share(keygen: &EcKeyPair) -> PaillierKeyPair { - let (ek, dk) = Paillier::keypair().keys(); - let randomness = Randomness::sample(&ek); - - let encrypted_share = Paillier::encrypt_with_chosen_randomness( - &ek, - RawPlaintext::from(keygen.secret_share.to_bigint()), - &randomness, - ) - .0 - .into_owned(); - - PaillierKeyPair { - ek, - dk, - encrypted_share, - randomness: randomness.0, - } - } - - pub fn generate_encrypted_share_from_fixed_paillier_keypair( - ek: &EncryptionKey, - dk: &DecryptionKey, - keygen: &EcKeyPair, - ) -> PaillierKeyPair { - let randomness = Randomness::sample(ek); - - let encrypted_share = Paillier::encrypt_with_chosen_randomness( - ek, - RawPlaintext::from(keygen.secret_share.to_bigint()), - &randomness, - ) - .0 - .into_owned(); - - PaillierKeyPair { - ek: ek.clone(), - dk: dk.clone(), - encrypted_share, - randomness: randomness.0, - } - } - - pub fn generate_ni_proof_correct_key(paillier_context: &PaillierKeyPair) -> NiCorrectKeyProof { - NiCorrectKeyProof::proof(&paillier_context.dk, None) - } - - pub fn pdl_proof( - party1_private: &Party1Private, - paillier_key_pair: &PaillierKeyPair, - ) -> (PDLwSlackStatement, PDLwSlackProof, CompositeDLogProof) { - let (n_tilde, h1, h2, xhi) = generate_h1_h2_n_tilde(); - let dlog_statement = DLogStatement { - N: n_tilde, - g: h1, - ni: h2, - }; - let composite_dlog_proof = CompositeDLogProof::prove(&dlog_statement, &xhi); - - // Generate PDL with slack statement, witness and proof - let pdl_w_slack_statement = PDLwSlackStatement { - ciphertext: paillier_key_pair.encrypted_share.clone(), - ek: paillier_key_pair.ek.clone(), - Q: Point::generator() * &party1_private.x1, - G: Point::generator().to_point(), - h1: dlog_statement.g.clone(), - h2: dlog_statement.ni.clone(), - N_tilde: dlog_statement.N, - }; - - let pdl_w_slack_witness = PDLwSlackWitness { - x: party1_private.x1.clone(), - r: party1_private.c_key_randomness.clone(), - }; - - let pdl_w_slack_proof = PDLwSlackProof::prove(&pdl_w_slack_witness, &pdl_w_slack_statement); - ( - pdl_w_slack_statement, - pdl_w_slack_proof, - composite_dlog_proof, - ) - } -} - -impl EphKeyGenFirstMsg { - pub fn create() -> (EphKeyGenFirstMsg, EphEcKeyPair) { - let base = Point::generator(); - let secret_share = Scalar::::random(); - let public_share = &*base * &secret_share; - let h = Point::::base_point2(); - - let c = h * &secret_share; - let w = ECDDHWitness { - x: secret_share.clone(), - }; - let delta = ECDDHStatement { - g1: base.to_point(), - h1: public_share.clone(), - g2: h.clone(), - h2: c.clone(), - }; - let d_log_proof = ECDDHProof::prove(&w, &delta); - let ec_key_pair = EphEcKeyPair { - public_share: public_share.clone(), - secret_share, - }; - ( - EphKeyGenFirstMsg { - d_log_proof, - public_share, - c, - }, - ec_key_pair, - ) - } -} - -impl EphKeyGenSecondMsg { - pub fn verify_commitments_and_dlog_proof( - party_two_first_message: &Party2EphKeyGenFirstMessage, - party_two_second_message: &Party2EphKeyGenSecondMessage, - ) -> Result { - let party_two_pk_commitment = &party_two_first_message.pk_commitment; - let party_two_zk_pok_commitment = &party_two_first_message.zk_pok_commitment; - let party_two_zk_pok_blind_factor = - &party_two_second_message.comm_witness.zk_pok_blind_factor; - let party_two_public_share = &party_two_second_message.comm_witness.public_share; - let party_two_pk_commitment_blind_factor = &party_two_second_message - .comm_witness - .pk_commitment_blind_factor; - let party_two_d_log_proof = &party_two_second_message.comm_witness.d_log_proof; - let mut flag = true; - if party_two_pk_commitment - != &HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(party_two_public_share.to_bytes(true).as_ref()), - party_two_pk_commitment_blind_factor, - ) - { - flag = false - } - if party_two_zk_pok_commitment - != &HashCommitment::::create_commitment_with_user_defined_randomness( - &Sha256::new() - .chain_points([&party_two_d_log_proof.a1, &party_two_d_log_proof.a2]) - .result_bigint(), - party_two_zk_pok_blind_factor, - ) - { - flag = false - } - - if !flag { - return Err(ProofError); - } - - let delta = ECDDHStatement { - g1: Point::generator().to_point(), - h1: party_two_public_share.clone(), - g2: Point::::base_point2().clone(), - h2: party_two_second_message.comm_witness.c.clone(), - }; - party_two_d_log_proof.verify(&delta)?; - Ok(EphKeyGenSecondMsg {}) - } -} - -impl Signature { - pub fn compute( - party_one_private: &Party1Private, - partial_sig_c3: &BigInt, - ephemeral_local_share: &EphEcKeyPair, - ephemeral_other_public_share: &Point, - ) -> Signature { - //compute r = k2* R1 - let r = ephemeral_other_public_share * &ephemeral_local_share.secret_share; - - let rx = r - .x_coord() - .unwrap() - .mod_floor(Scalar::::group_order()); - - let k1_inv = ephemeral_local_share.secret_share.invert().unwrap(); - - let s_tag = Paillier::decrypt( - &party_one_private.paillier_priv, - &RawCiphertext::from(partial_sig_c3), - ) - .0; - let s_tag_fe = Scalar::::from(s_tag.as_ref()); - let s_tag_tag = s_tag_fe * k1_inv; - let s_tag_tag_bn = s_tag_tag.to_bigint(); - - let s = cmp::min( - s_tag_tag_bn.clone(), - Scalar::::group_order().clone() - s_tag_tag_bn, - ); - - Signature { s, r: rx } - } - - pub fn compute_with_recid( - party_one_private: &Party1Private, - partial_sig_c3: &BigInt, - ephemeral_local_share: &EphEcKeyPair, - ephemeral_other_public_share: &Point, - ) -> SignatureRecid { - //compute r = k2* R1 - let r = ephemeral_other_public_share * &ephemeral_local_share.secret_share; - - let rx = r - .x_coord() - .unwrap() - .mod_floor(Scalar::::group_order()); - let ry = r - .y_coord() - .unwrap() - .mod_floor(Scalar::::group_order()); - let k1_inv = ephemeral_local_share.secret_share.invert().unwrap(); - - let s_tag = Paillier::decrypt( - &party_one_private.paillier_priv, - &RawCiphertext::from(partial_sig_c3), - ) - .0; - let s_tag_fe = Scalar::::from(s_tag.as_ref()); - let s_tag_tag = s_tag_fe * k1_inv; - let s_tag_tag_bn = s_tag_tag.to_bigint(); - let s = cmp::min( - s_tag_tag_bn.clone(), - Scalar::::group_order() - &s_tag_tag_bn, - ); - - /* - Calculate recovery id - it is not possible to compute the public key out of the signature - itself. Recovery id is used to enable extracting the public key uniquely. - 1. id = R.y & 1 - 2. if (s > curve.q / 2) id = id ^ 1 - */ - let is_ry_odd = ry.test_bit(0); - let mut recid = if is_ry_odd { 1 } else { 0 }; - if s_tag_tag_bn > Scalar::::group_order() - &s_tag_tag_bn { - recid ^= 1; - } - - SignatureRecid { s, r: rx, recid } - } -} - -pub fn verify( - signature: &Signature, - pubkey: &Point, - message: &BigInt, -) -> Result<(), Error> { - let s_fe = Scalar::::from(&signature.s); - let rx_fe = Scalar::::from(&signature.r); - - let s_inv_fe = s_fe.invert().unwrap(); - let e_fe: Scalar = - Scalar::::from(&message.mod_floor(Scalar::::group_order())); - let u1 = Point::generator() * e_fe * &s_inv_fe; - let u2 = pubkey * rx_fe * &s_inv_fe; - - // second condition is against malleability - let rx_bytes = &BigInt::to_bytes(&signature.r)[..]; - let u1_plus_u2_bytes = &BigInt::to_bytes(&(u1 + u2).x_coord().unwrap())[..]; - - if rx_bytes.ct_eq(u1_plus_u2_bytes).unwrap_u8() == 1 - && signature.s < Scalar::::group_order() - signature.s.clone() - { - Ok(()) - } else { - Err(Error::InvalidSig) - } -} - -pub fn generate_h1_h2_n_tilde() -> (BigInt, BigInt, BigInt, BigInt) { - //note, should be safe primes: - // let (ek_tilde, dk_tilde) = Paillier::keypair_safe_primes().keys();; - let (ek_tilde, dk_tilde) = Paillier::keypair().keys(); - let one = BigInt::one(); - let phi = (&dk_tilde.p - &one) * (&dk_tilde.q - &one); - let h1 = BigInt::sample_below(&phi); - let s = BigInt::from(2).pow(256_u32); - let xhi = BigInt::sample_below(&s); - let h1_inv = BigInt::mod_inv(&h1, &ek_tilde.n).unwrap(); - let h2 = BigInt::mod_pow(&h1_inv, &xhi, &ek_tilde.n); - - (ek_tilde.n, h1, h2, xhi) -} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_two.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_two.rs deleted file mode 100644 index 19d3e1f8..00000000 --- a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/party_two.rs +++ /dev/null @@ -1,426 +0,0 @@ -/* - Multi-party ECDSA - - Copyright 2018 by Kzen Networks - - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) - - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - - @license GPL-3.0+ -*/ - -use centipede::juggling::proof_system::{Helgamalsegmented, Witness}; -use centipede::juggling::segmentation::Msegmentation; -use curv::arithmetic::traits::*; -use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; -use curv::cryptographic_primitives::commitments::traits::Commitment; -use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; -use curv::cryptographic_primitives::proofs::sigma_dlog::*; -use curv::cryptographic_primitives::proofs::sigma_ec_ddh::*; -use curv::cryptographic_primitives::proofs::ProofError; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use curv::BigInt; -use paillier::Paillier; -use paillier::{Add, Encrypt, Mul}; -use paillier::{EncryptionKey, RawCiphertext, RawPlaintext}; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use zk_paillier::zkproofs::{IncorrectProof, NiCorrectKeyProof}; - -use super::party_one::EphKeyGenFirstMsg as Party1EphKeyGenFirstMsg; -use super::party_one::KeyGenFirstMsg as Party1KeyGenFirstMessage; -use super::party_one::KeyGenSecondMsg as Party1KeyGenSecondMessage; -use super::SECURITY_BITS; -use crate::utilities::mta::{MessageA, MessageB}; - -use crate::utilities::zk_pdl_with_slack::PDLwSlackProof; -use crate::utilities::zk_pdl_with_slack::PDLwSlackStatement; -use thiserror::Error; -use zk_paillier::zkproofs::{CompositeDLogProof, DLogStatement}; - -#[derive(Error, Debug)] -pub enum PartyTwoError { - #[error("party two pdl verify failed (lindell 2017)")] - PdlVerify, -} - -const PAILLIER_KEY_SIZE: usize = 2048; -//****************** Begin: Party Two structs ******************// - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EcKeyPair { - pub public_share: Point, - secret_share: Scalar, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KeyGenFirstMsg { - pub d_log_proof: DLogProof, - pub public_share: Point, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct KeyGenSecondMsg {} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PaillierPublic { - pub ek: EncryptionKey, - pub encrypted_secret_share: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PartialSig { - pub c3: BigInt, -} - -#[derive(Serialize, Deserialize)] -pub struct Party2Private { - x2: Scalar, -} - -#[derive(Debug)] -#[allow(dead_code)] -pub struct PDLchallenge { - pub c_tag: BigInt, - pub c_tag_tag: BigInt, - a: BigInt, - b: BigInt, - blindness: BigInt, - q_tag: Point, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLFirstMessage { - pub c_tag: BigInt, - pub c_tag_tag: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLdecommit { - pub a: BigInt, - pub b: BigInt, - pub blindness: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PDLSecondMessage { - pub decommit: PDLdecommit, -} -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EphEcKeyPair { - pub public_share: Point, - secret_share: Scalar, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EphCommWitness { - pub pk_commitment_blind_factor: BigInt, - pub zk_pok_blind_factor: BigInt, - pub public_share: Point, - pub d_log_proof: ECDDHProof, - pub c: Point, //c = secret_share * base_point2 -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EphKeyGenFirstMsg { - pub pk_commitment: BigInt, - pub zk_pok_commitment: BigInt, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct EphKeyGenSecondMsg { - pub comm_witness: EphCommWitness, -} - -//****************** End: Party Two structs ******************// - -impl KeyGenFirstMsg { - pub fn create() -> (KeyGenFirstMsg, EcKeyPair) { - let base = Point::generator(); - let secret_share = Scalar::::random(); - let public_share = base * &secret_share; - let d_log_proof = DLogProof::prove(&secret_share); - let ec_key_pair = EcKeyPair { - public_share: public_share.clone(), - secret_share, - }; - ( - KeyGenFirstMsg { - d_log_proof, - public_share, - }, - ec_key_pair, - ) - } - - pub fn create_with_fixed_secret_share( - secret_share: Scalar, - ) -> (KeyGenFirstMsg, EcKeyPair) { - let base = Point::generator(); - let public_share = base * &secret_share; - let d_log_proof = DLogProof::prove(&secret_share); - let ec_key_pair = EcKeyPair { - public_share: public_share.clone(), - secret_share, - }; - ( - KeyGenFirstMsg { - d_log_proof, - public_share, - }, - ec_key_pair, - ) - } -} - -impl KeyGenSecondMsg { - pub fn verify_commitments_and_dlog_proof( - party_one_first_message: &Party1KeyGenFirstMessage, - party_one_second_message: &Party1KeyGenSecondMessage, - ) -> Result { - let party_one_pk_commitment = &party_one_first_message.pk_commitment; - let party_one_zk_pok_commitment = &party_one_first_message.zk_pok_commitment; - let party_one_zk_pok_blind_factor = - &party_one_second_message.comm_witness.zk_pok_blind_factor; - let party_one_public_share = &party_one_second_message.comm_witness.public_share; - let party_one_pk_commitment_blind_factor = &party_one_second_message - .comm_witness - .pk_commitment_blind_factor; - let party_one_d_log_proof = &party_one_second_message.comm_witness.d_log_proof; - - let mut flag = true; - if party_one_pk_commitment - != &HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(party_one_public_share.to_bytes(true).as_ref()), - party_one_pk_commitment_blind_factor, - ) - { - flag = false - } - if party_one_zk_pok_commitment - != &HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes( - party_one_d_log_proof - .pk_t_rand_commitment - .to_bytes(true) - .as_ref(), - ), - party_one_zk_pok_blind_factor, - ) - { - flag = false - } - - if !flag { - return Err(ProofError); - } - - DLogProof::verify(party_one_d_log_proof)?; - Ok(KeyGenSecondMsg {}) - } -} - -pub fn compute_pubkey( - local_share: &EcKeyPair, - other_share_public_share: &Point, -) -> Point { - let pubkey = other_share_public_share; - pubkey * &local_share.secret_share -} - -impl Party2Private { - pub fn set_private_key(ec_key: &EcKeyPair) -> Party2Private { - Party2Private { - x2: ec_key.secret_share.clone(), - } - } - - pub fn update_private_key(party_two_private: &Party2Private, factor: &BigInt) -> Party2Private { - let factor_fe = Scalar::::from(factor); - Party2Private { - x2: &party_two_private.x2 * &factor_fe, - } - } - - // used for verifiable recovery - pub fn to_encrypted_segment( - &self, - segment_size: usize, - num_of_segments: usize, - pub_ke_y: &Point, - g: &Point, - ) -> (Witness, Helgamalsegmented) { - Msegmentation::to_encrypted_segments(&self.x2, &segment_size, num_of_segments, pub_ke_y, g) - } - - // used to transform lindell master key to gg18 master key - pub fn to_mta_message_b( - &self, - ek: &EncryptionKey, - ciphertext: &BigInt, - ) -> (MessageB, Scalar) { - let message_a = MessageA { - c: ciphertext.clone(), - range_proofs: vec![], - }; - let (a, b, _, _) = MessageB::b(&self.x2, ek, message_a, &[]).unwrap(); - (a, b) - } -} - -impl PaillierPublic { - pub fn pdl_verify( - composite_dlog_proof: &CompositeDLogProof, - pdl_w_slack_statement: &PDLwSlackStatement, - pdl_w_slack_proof: &PDLwSlackProof, - paillier_public: &PaillierPublic, - q1: &Point, - ) -> Result<(), PartyTwoError> { - if pdl_w_slack_statement.ek != paillier_public.ek - || pdl_w_slack_statement.ciphertext != paillier_public.encrypted_secret_share - || &pdl_w_slack_statement.Q != q1 - { - return Err(PartyTwoError::PdlVerify); - } - let dlog_statement = DLogStatement { - N: pdl_w_slack_statement.N_tilde.clone(), - g: pdl_w_slack_statement.h1.clone(), - ni: pdl_w_slack_statement.h2.clone(), - }; - if composite_dlog_proof.verify(&dlog_statement).is_ok() - && pdl_w_slack_proof.verify(pdl_w_slack_statement).is_ok() - { - Ok(()) - } else { - Err(PartyTwoError::PdlVerify) - } - } - - pub fn verify_ni_proof_correct_key( - proof: NiCorrectKeyProof, - ek: &EncryptionKey, - ) -> Result<(), IncorrectProof> { - // - if ek.n.bit_length() < PAILLIER_KEY_SIZE - 1 { - return Err(IncorrectProof); - }; - proof.verify(ek, zk_paillier::zkproofs::SALT_STRING) - } -} - -impl EphKeyGenFirstMsg { - pub fn create_commitments() -> (EphKeyGenFirstMsg, EphCommWitness, EphEcKeyPair) { - let base = Point::generator(); - - let secret_share = Scalar::::random(); - - let public_share = base * &secret_share; - - let h = Point::::base_point2(); - - let c = h * &secret_share; - let w = ECDDHWitness { - x: secret_share.clone(), - }; - let delta = ECDDHStatement { - g1: base.to_point(), - h1: public_share.clone(), - g2: h.clone(), - h2: c.clone(), - }; - let d_log_proof = ECDDHProof::prove(&w, &delta); - - // we use hash based commitment - let pk_commitment_blind_factor = BigInt::sample(SECURITY_BITS); - let pk_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(public_share.to_bytes(true).as_ref()), - &pk_commitment_blind_factor, - ); - - let zk_pok_blind_factor = BigInt::sample(SECURITY_BITS); - let zk_pok_commitment = - HashCommitment::::create_commitment_with_user_defined_randomness( - &Sha256::new() - .chain_points([&d_log_proof.a1, &d_log_proof.a2]) - .result_bigint(), - &zk_pok_blind_factor, - ); - - let ec_key_pair = EphEcKeyPair { - public_share, - secret_share, - }; - ( - EphKeyGenFirstMsg { - pk_commitment, - zk_pok_commitment, - }, - EphCommWitness { - pk_commitment_blind_factor, - zk_pok_blind_factor, - public_share: ec_key_pair.public_share.clone(), - d_log_proof, - c, - }, - ec_key_pair, - ) - } -} - -impl EphKeyGenSecondMsg { - pub fn verify_and_decommit( - comm_witness: EphCommWitness, - party_one_first_message: &Party1EphKeyGenFirstMsg, - ) -> Result { - let delta = ECDDHStatement { - g1: Point::generator().to_point(), - h1: party_one_first_message.public_share.clone(), - g2: Point::::base_point2().clone(), - h2: party_one_first_message.c.clone(), - }; - party_one_first_message.d_log_proof.verify(&delta)?; - Ok(EphKeyGenSecondMsg { comm_witness }) - } -} - -impl PartialSig { - pub fn compute( - ek: &EncryptionKey, - encrypted_secret_share: &BigInt, - local_share: &Party2Private, - ephemeral_local_share: &EphEcKeyPair, - ephemeral_other_public_share: &Point, - message: &BigInt, - ) -> PartialSig { - let q = Scalar::::group_order(); - //compute r = k2* R1 - let r = ephemeral_other_public_share * &ephemeral_local_share.secret_share; - - let rx = r.x_coord().unwrap().mod_floor(q); - let rho = BigInt::sample_below(&q.pow(2)); - let k2_inv = BigInt::mod_inv(&ephemeral_local_share.secret_share.to_bigint(), q).unwrap(); - let partial_sig = rho * q + BigInt::mod_mul(&k2_inv, message, q); - - let c1 = Paillier::encrypt(ek, RawPlaintext::from(partial_sig)); - let v = BigInt::mod_mul( - &k2_inv, - &BigInt::mod_mul(&rx, &local_share.x2.to_bigint(), q), - q, - ); - let c2 = Paillier::mul( - ek, - RawCiphertext::from(encrypted_secret_share.clone()), - RawPlaintext::from(v), - ); - //c3: - PartialSig { - c3: Paillier::add(ek, c2, c1).0.into_owned(), - } - } -} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/test.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/test.rs deleted file mode 100644 index 60c142f6..00000000 --- a/multi-party-ecdsa/src/protocols/two_party_ecdsa/lindell_2017/test.rs +++ /dev/null @@ -1,137 +0,0 @@ -// For integration tests, please add your tests in /tests instead - -use crate::protocols::two_party_ecdsa::lindell_2017::{party_one, party_two}; -use curv::arithmetic::traits::Samplable; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Scalar}; -use curv::BigInt; - -#[test] -fn test_d_log_proof_party_two_party_one() { - let (party_one_first_message, comm_witness, _ec_key_pair_party1) = - party_one::KeyGenFirstMsg::create_commitments(); - let (party_two_first_message, _ec_key_pair_party2) = party_two::KeyGenFirstMsg::create(); - let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( - comm_witness, - &party_two_first_message.d_log_proof, - ) - .expect("failed to verify and decommit"); - - let _party_two_second_message = party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( - &party_one_first_message, - &party_one_second_message, - ) - .expect("failed to verify commitments and DLog proof"); -} - -#[test] - -fn test_full_key_gen() { - let (party_one_first_message, comm_witness, ec_key_pair_party1) = - party_one::KeyGenFirstMsg::create_commitments_with_fixed_secret_share( - Scalar::::from(&BigInt::sample(253)), - ); - let (party_two_first_message, _ec_key_pair_party2) = - party_two::KeyGenFirstMsg::create_with_fixed_secret_share(Scalar::::from( - &BigInt::from(10), - )); - let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( - comm_witness, - &party_two_first_message.d_log_proof, - ) - .expect("failed to verify and decommit"); - - let _party_two_second_message = party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( - &party_one_first_message, - &party_one_second_message, - ) - .expect("failed to verify commitments and DLog proof"); - - // init paillier keypair: - let paillier_key_pair = - party_one::PaillierKeyPair::generate_keypair_and_encrypted_share(&ec_key_pair_party1); - - let party_one_private = - party_one::Party1Private::set_private_key(&ec_key_pair_party1, &paillier_key_pair); - - let party_two_paillier = party_two::PaillierPublic { - ek: paillier_key_pair.ek.clone(), - encrypted_secret_share: paillier_key_pair.encrypted_share.clone(), - }; - - // zk proof of correct paillier key - let correct_key_proof = - party_one::PaillierKeyPair::generate_ni_proof_correct_key(&paillier_key_pair); - party_two::PaillierPublic::verify_ni_proof_correct_key( - correct_key_proof, - &party_two_paillier.ek, - ) - .expect("bad paillier key"); - - //zk_pdl - - let (pdl_statement, pdl_proof, composite_dlog_proof) = - party_one::PaillierKeyPair::pdl_proof(&party_one_private, &paillier_key_pair); - party_two::PaillierPublic::pdl_verify( - &composite_dlog_proof, - &pdl_statement, - &pdl_proof, - &party_two_paillier, - &party_one_second_message.comm_witness.public_share, - ) - .expect("PDL error"); -} - -#[test] -fn test_two_party_sign() { - // assume party1 and party2 engaged with KeyGen in the past resulting in - // party1 owning private share and paillier key-pair - // party2 owning private share and paillier encryption of party1 share - let (_party_one_private_share_gen, _comm_witness, ec_key_pair_party1) = - party_one::KeyGenFirstMsg::create_commitments(); - let (party_two_private_share_gen, ec_key_pair_party2) = party_two::KeyGenFirstMsg::create(); - - let keypair = - party_one::PaillierKeyPair::generate_keypair_and_encrypted_share(&ec_key_pair_party1); - - // creating the ephemeral private shares: - - let (eph_party_two_first_message, eph_comm_witness, eph_ec_key_pair_party2) = - party_two::EphKeyGenFirstMsg::create_commitments(); - let (eph_party_one_first_message, eph_ec_key_pair_party1) = - party_one::EphKeyGenFirstMsg::create(); - let eph_party_two_second_message = party_two::EphKeyGenSecondMsg::verify_and_decommit( - eph_comm_witness, - &eph_party_one_first_message, - ) - .expect("party1 DLog proof failed"); - - let _eph_party_one_second_message = - party_one::EphKeyGenSecondMsg::verify_commitments_and_dlog_proof( - &eph_party_two_first_message, - &eph_party_two_second_message, - ) - .expect("failed to verify commitments and DLog proof"); - let party2_private = party_two::Party2Private::set_private_key(&ec_key_pair_party2); - let message = BigInt::from(1234); - let partial_sig = party_two::PartialSig::compute( - &keypair.ek, - &keypair.encrypted_share, - &party2_private, - &eph_ec_key_pair_party2, - &eph_party_one_first_message.public_share, - &message, - ); - - let party1_private = party_one::Party1Private::set_private_key(&ec_key_pair_party1, &keypair); - - let signature = party_one::Signature::compute( - &party1_private, - &partial_sig.c3, - &eph_ec_key_pair_party1, - &eph_party_two_second_message.comm_witness.public_share, - ); - - let pubkey = - party_one::compute_pubkey(&party1_private, &party_two_private_share_gen.public_share); - party_one::verify(&signature, &pubkey, &message).expect("Invalid signature") -} diff --git a/multi-party-ecdsa/src/protocols/two_party_ecdsa/mod.rs b/multi-party-ecdsa/src/protocols/two_party_ecdsa/mod.rs deleted file mode 100644 index d1785d24..00000000 --- a/multi-party-ecdsa/src/protocols/two_party_ecdsa/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -/* - Multi-party ECDSA - - Copyright 2018 by Kzen Networks - - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) - - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - - @license GPL-3.0+ -*/ - -// Fast Secure Two-Party ECDSA Signing by Yehuda Lindell (https://eprint.iacr.org/2017/552.pdf). - -pub mod lindell_2017; - -// Two-Party ECDSA from Hash Proof Systems and -//Efficient Instantiations (https://eprint.iacr.org/2019/503.pdf) -#[cfg(feature = "cclst")] -pub mod cclst_2019; From 782741df57479aea27c88717ac202095f303de47 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 08:44:57 +0800 Subject: [PATCH 23/41] Remove gg_2018 module and examples. --- multi-party-ecdsa/Cargo.toml | 37 - .../examples/gg18_keygen_client.rs | 271 ------- .../examples/gg18_sign_client.rs | 531 ------------- multi-party-ecdsa/examples/gg18_sm_manager.rs | 143 ---- .../multi_party_ecdsa/gg_2018/mod.rs | 23 - .../multi_party_ecdsa/gg_2018/party_i.rs | 738 ------------------ .../multi_party_ecdsa/gg_2018/test.rs | 460 ----------- .../multi_party_ecdsa/gg_2020/mod.rs | 4 + .../multi_party_ecdsa/gg_2020/party_i.rs | 2 +- .../gg_2020/state_machine/keygen.rs | 3 +- .../gg_2020/state_machine/keygen/rounds.rs | 5 +- .../multi_party_ecdsa/gg_2020/test.rs | 2 +- .../src/protocols/multi_party_ecdsa/mod.rs | 1 - multi-party-ecdsa/src/utilities/mta/mod.rs | 2 +- 14 files changed, 10 insertions(+), 2212 deletions(-) delete mode 100644 multi-party-ecdsa/examples/gg18_keygen_client.rs delete mode 100644 multi-party-ecdsa/examples/gg18_sign_client.rs delete mode 100644 multi-party-ecdsa/examples/gg18_sm_manager.rs delete mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/mod.rs delete mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/party_i.rs delete mode 100644 multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/test.rs diff --git a/multi-party-ecdsa/Cargo.toml b/multi-party-ecdsa/Cargo.toml index 7542b57a..0264c01e 100644 --- a/multi-party-ecdsa/Cargo.toml +++ b/multi-party-ecdsa/Cargo.toml @@ -65,44 +65,7 @@ secp256k1 = { version = "0.20", features = ["global-context"]} thiserror = "1.0.23" round-based = { git = "https://github.com/webb-tools/round-based-protocol", features = ["dev"] } -[[example]] -name = "gg18_sm_manager" - -[[example]] -name = "gg18_sign_client" - -[[example]] -name = "gg18_keygen_client" [[example]] name = "common" crate-type = ["lib"] - -[[bench]] -name = "cclst_keygen" -path = "benches/two_party_ecdsa/cclst_2019/keygen.rs" -required-features = ["cclst"] -harness = false - -[[bench]] -name = "cclst_sign" -path = "benches/two_party_ecdsa/cclst_2019/sign.rs" -required-features = ["cclst"] -harness = false - - -[[bench]] -name = "gg18" -path = "benches/multi_party_ecdsa/gg18/keygen.rs" -harness = false - -[[bench]] -name = "lindel2017_keygen" -path = "benches/two_party_ecdsa/lindell_2017/keygen.rs" -harness = false - - -[[bench]] -name = "lindel2017_sign" -path = "benches/two_party_ecdsa/lindell_2017/sign.rs" -harness = false diff --git a/multi-party-ecdsa/examples/gg18_keygen_client.rs b/multi-party-ecdsa/examples/gg18_keygen_client.rs deleted file mode 100644 index f65845c0..00000000 --- a/multi-party-ecdsa/examples/gg18_keygen_client.rs +++ /dev/null @@ -1,271 +0,0 @@ -#![allow(non_snake_case)] -/// to run: -/// 1: go to rocket_server -> cargo run -/// 2: cargo run from PARTIES number of terminals -use curv::{ - arithmetic::traits::Converter, - cryptographic_primitives::{ - proofs::sigma_dlog::DLogProof, secret_sharing::feldman_vss::VerifiableSS, - }, - elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, - BigInt, -}; -use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2018::party_i::{ - KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Keys, Parameters, -}; -use paillier::EncryptionKey; -use reqwest::Client; -use sha2::Sha256; -use std::{env, fs, time}; - -mod common; -use common::{ - aes_decrypt, aes_encrypt, broadcast, poll_for_broadcasts, poll_for_p2p, postb, sendp2p, Params, - PartySignup, AEAD, AES_KEY_BYTES_LEN, -}; - -fn main() { - if env::args().nth(3).is_some() { - panic!("too many arguments") - } - if env::args().nth(2).is_none() { - panic!("too few arguments") - } - //read parameters: - let data = fs::read_to_string("params.json") - .expect("Unable to read params, make sure config file is present in the same folder "); - let params: Params = serde_json::from_str(&data).unwrap(); - let PARTIES: u16 = params.parties.parse::().unwrap(); - let THRESHOLD: u16 = params.threshold.parse::().unwrap(); - - let client = Client::new(); - - // delay: - let delay = time::Duration::from_millis(25); - let params = Parameters { - threshold: THRESHOLD, - share_count: PARTIES, - }; - - //signup: - let (party_num_int, uuid) = match signup(&client).unwrap() { - PartySignup { number, uuid } => (number, uuid), - }; - println!("number: {:?}, uuid: {:?}", party_num_int, uuid); - - let party_keys = Keys::create(party_num_int); - let (bc_i, decom_i) = party_keys.phase1_broadcast_phase3_proof_of_correct_key(); - - // send commitment to ephemeral public keys, get round 1 commitments of other parties - assert!(broadcast( - &client, - party_num_int, - "round1", - serde_json::to_string(&bc_i).unwrap(), - uuid.clone() - ) - .is_ok()); - let round1_ans_vec = poll_for_broadcasts( - &client, - party_num_int, - PARTIES, - delay, - "round1", - uuid.clone(), - ); - - let mut bc1_vec = round1_ans_vec - .iter() - .map(|m| serde_json::from_str::(m).unwrap()) - .collect::>(); - - bc1_vec.insert(party_num_int as usize - 1, bc_i); - - // send ephemeral public keys and check commitments correctness - assert!(broadcast( - &client, - party_num_int, - "round2", - serde_json::to_string(&decom_i).unwrap(), - uuid.clone() - ) - .is_ok()); - let round2_ans_vec = poll_for_broadcasts( - &client, - party_num_int, - PARTIES, - delay, - "round2", - uuid.clone(), - ); - - let mut j = 0; - let mut point_vec: Vec> = Vec::new(); - let mut decom_vec: Vec = Vec::new(); - let mut enc_keys: Vec> = Vec::new(); - for i in 1..=PARTIES { - if i == party_num_int { - point_vec.push(decom_i.y_i.clone()); - decom_vec.push(decom_i.clone()); - } else { - let decom_j: KeyGenDecommitMessage1 = serde_json::from_str(&round2_ans_vec[j]).unwrap(); - point_vec.push(decom_j.y_i.clone()); - decom_vec.push(decom_j.clone()); - let key_bn: BigInt = (decom_j.y_i.clone() * party_keys.u_i.clone()) - .x_coord() - .unwrap(); - let key_bytes = BigInt::to_bytes(&key_bn); - let mut template: Vec = vec![0u8; AES_KEY_BYTES_LEN - key_bytes.len()]; - template.extend_from_slice(&key_bytes[..]); - enc_keys.push(template); - j += 1; - } - } - - let (head, tail) = point_vec.split_at(1); - let y_sum = tail.iter().fold(head[0].clone(), |acc, x| acc + x); - - let (vss_scheme, secret_shares, _index) = party_keys - .phase1_verify_com_phase3_verify_correct_key_phase2_distribute( - ¶ms, &decom_vec, &bc1_vec, - ) - .expect("invalid key"); - - ////////////////////////////////////////////////////////////////////////////// - - let mut j = 0; - for (k, i) in (1..=PARTIES).enumerate() { - if i != party_num_int { - // prepare encrypted ss for party i: - let key_i = &enc_keys[j]; - let plaintext = BigInt::to_bytes(&secret_shares[k].to_bigint()); - let aead_pack_i = aes_encrypt(key_i, &plaintext); - assert!(sendp2p( - &client, - party_num_int, - i, - "round3", - serde_json::to_string(&aead_pack_i).unwrap(), - uuid.clone() - ) - .is_ok()); - j += 1; - } - } - - let round3_ans_vec = poll_for_p2p( - &client, - party_num_int, - PARTIES, - delay, - "round3", - uuid.clone(), - ); - - let mut j = 0; - let mut party_shares: Vec> = Vec::new(); - for i in 1..=PARTIES { - if i == party_num_int { - party_shares.push(secret_shares[(i - 1) as usize].clone()); - } else { - let aead_pack: AEAD = serde_json::from_str(&round3_ans_vec[j]).unwrap(); - let key_i = &enc_keys[j]; - let out = aes_decrypt(key_i, aead_pack); - let out_bn = BigInt::from_bytes(&out[..]); - let out_fe = Scalar::::from(&out_bn); - party_shares.push(out_fe); - - j += 1; - } - } - - // round 4: send vss commitments - assert!(broadcast( - &client, - party_num_int, - "round4", - serde_json::to_string(&vss_scheme).unwrap(), - uuid.clone() - ) - .is_ok()); - let round4_ans_vec = poll_for_broadcasts( - &client, - party_num_int, - PARTIES, - delay, - "round4", - uuid.clone(), - ); - - let mut j = 0; - let mut vss_scheme_vec: Vec> = Vec::new(); - for i in 1..=PARTIES { - if i == party_num_int { - vss_scheme_vec.push(vss_scheme.clone()); - } else { - let vss_scheme_j: VerifiableSS = - serde_json::from_str(&round4_ans_vec[j]).unwrap(); - vss_scheme_vec.push(vss_scheme_j); - j += 1; - } - } - - let (shared_keys, dlog_proof) = party_keys - .phase2_verify_vss_construct_keypair_phase3_pok_dlog( - ¶ms, - &point_vec, - &party_shares, - &vss_scheme_vec, - party_num_int, - ) - .expect("invalid vss"); - - // round 5: send dlog proof - assert!(broadcast( - &client, - party_num_int, - "round5", - serde_json::to_string(&dlog_proof).unwrap(), - uuid.clone() - ) - .is_ok()); - let round5_ans_vec = - poll_for_broadcasts(&client, party_num_int, PARTIES, delay, "round5", uuid); - - let mut j = 0; - let mut dlog_proof_vec: Vec> = Vec::new(); - for i in 1..=PARTIES { - if i == party_num_int { - dlog_proof_vec.push(dlog_proof.clone()); - } else { - let dlog_proof_j: DLogProof = - serde_json::from_str(&round5_ans_vec[j]).unwrap(); - dlog_proof_vec.push(dlog_proof_j); - j += 1; - } - } - Keys::verify_dlog_proofs(¶ms, &dlog_proof_vec, &point_vec).expect("bad dlog proof"); - - //save key to file: - let paillier_key_vec = (0..PARTIES) - .map(|i| bc1_vec[i as usize].e.clone()) - .collect::>(); - - let keygen_json = serde_json::to_string(&( - party_keys, - shared_keys, - party_num_int, - vss_scheme_vec, - paillier_key_vec, - y_sum, - )) - .unwrap(); - fs::write(env::args().nth(2).unwrap(), keygen_json).expect("Unable to save !"); -} - -pub fn signup(client: &Client) -> Result { - let key = "signup-keygen".to_string(); - - let res_body = postb(client, "signupkeygen", key).unwrap(); - serde_json::from_str(&res_body).unwrap() -} diff --git a/multi-party-ecdsa/examples/gg18_sign_client.rs b/multi-party-ecdsa/examples/gg18_sign_client.rs deleted file mode 100644 index 1cd39c23..00000000 --- a/multi-party-ecdsa/examples/gg18_sign_client.rs +++ /dev/null @@ -1,531 +0,0 @@ -#![allow(non_snake_case)] - -use curv::{ - arithmetic::traits::*, - cryptographic_primitives::{ - proofs::sigma_correct_homomorphic_elgamal_enc::HomoELGamalProof, - proofs::sigma_dlog::DLogProof, secret_sharing::feldman_vss::VerifiableSS, - }, - elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}, - BigInt, -}; -use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2018::party_i::{ - Keys, LocalSignature, PartyPrivate, Phase5ADecom1, Phase5Com1, Phase5Com2, Phase5DDecom2, - SharedKeys, SignBroadcastPhase1, SignDecommitPhase1, SignKeys, -}; -use multi_party_ecdsa::utilities::mta::*; -use sha2::Sha256; - -use paillier::EncryptionKey; -use reqwest::Client; -use std::{env, fs, time}; - -mod common; -use common::{ - broadcast, check_sig, poll_for_broadcasts, poll_for_p2p, postb, sendp2p, Params, PartySignup, -}; - -#[allow(clippy::cognitive_complexity)] -fn main() { - if env::args().nth(4).is_some() { - panic!("too many arguments") - } - if env::args().nth(3).is_none() { - panic!("too few arguments") - } - let message_str = env::args().nth(3).unwrap_or_else(|| "".to_string()); - let message = match hex::decode(message_str.clone()) { - Ok(x) => x, - Err(_e) => message_str.as_bytes().to_vec(), - }; - let message = &message[..]; - let client = Client::new(); - // delay: - let delay = time::Duration::from_millis(25); - // read key file - let data = fs::read_to_string(env::args().nth(2).unwrap()) - .expect("Unable to load keys, did you run keygen first? "); - let (party_keys, shared_keys, party_id, vss_scheme_vec, paillier_key_vector, y_sum): ( - Keys, - SharedKeys, - u16, - Vec>, - Vec, - Point, - ) = serde_json::from_str(&data).unwrap(); - - //read parameters: - let data = fs::read_to_string("params.json") - .expect("Unable to read params, make sure config file is present in the same folder "); - let params: Params = serde_json::from_str(&data).unwrap(); - let THRESHOLD = params.threshold.parse::().unwrap(); - - //signup: - let (party_num_int, uuid) = match signup(&client).unwrap() { - PartySignup { number, uuid } => (number, uuid), - }; - println!("number: {:?}, uuid: {:?}", party_num_int, uuid); - - // round 0: collect signers IDs - assert!(broadcast( - &client, - party_num_int, - "round0", - serde_json::to_string(&party_id).unwrap(), - uuid.clone() - ) - .is_ok()); - let round0_ans_vec = poll_for_broadcasts( - &client, - party_num_int, - THRESHOLD + 1, - delay, - "round0", - uuid.clone(), - ); - - let mut j = 0; - let mut signers_vec: Vec = Vec::new(); - for i in 1..=THRESHOLD + 1 { - if i == party_num_int { - signers_vec.push(party_id - 1); - } else { - let signer_j: u16 = serde_json::from_str(&round0_ans_vec[j]).unwrap(); - signers_vec.push(signer_j - 1); - j += 1; - } - } - - let private = PartyPrivate::set_private(party_keys.clone(), shared_keys); - - let sign_keys = SignKeys::create( - &private, - &vss_scheme_vec[usize::from(signers_vec[usize::from(party_num_int - 1)])], - signers_vec[usize::from(party_num_int - 1)], - &signers_vec, - ); - - let xi_com_vec = Keys::get_commitments_to_xi(&vss_scheme_vec); - ////////////////////////////////////////////////////////////////////////////// - let (com, decommit) = sign_keys.phase1_broadcast(); - let (m_a_k, _) = MessageA::a(&sign_keys.k_i, &party_keys.ek, &[]); - assert!(broadcast( - &client, - party_num_int, - "round1", - serde_json::to_string(&(com.clone(), m_a_k)).unwrap(), - uuid.clone() - ) - .is_ok()); - let round1_ans_vec = poll_for_broadcasts( - &client, - party_num_int, - THRESHOLD + 1, - delay, - "round1", - uuid.clone(), - ); - - let mut j = 0; - let mut bc1_vec: Vec = Vec::new(); - let mut m_a_vec: Vec = Vec::new(); - - for i in 1..THRESHOLD + 2 { - if i == party_num_int { - bc1_vec.push(com.clone()); - // m_a_vec.push(m_a_k.clone()); - } else { - // if signers_vec.contains(&(i as usize)) { - let (bc1_j, m_a_party_j): (SignBroadcastPhase1, MessageA) = - serde_json::from_str(&round1_ans_vec[j]).unwrap(); - bc1_vec.push(bc1_j); - m_a_vec.push(m_a_party_j); - - j += 1; - // } - } - } - assert_eq!(signers_vec.len(), bc1_vec.len()); - - ////////////////////////////////////////////////////////////////////////////// - let mut m_b_gamma_send_vec: Vec = Vec::new(); - let mut beta_vec: Vec> = Vec::new(); - let mut m_b_w_send_vec: Vec = Vec::new(); - let mut ni_vec: Vec> = Vec::new(); - let mut j = 0; - for i in 1..THRESHOLD + 2 { - if i != party_num_int { - let (m_b_gamma, beta_gamma, _, _) = MessageB::b( - &sign_keys.gamma_i, - &paillier_key_vector[usize::from(signers_vec[usize::from(i - 1)])], - m_a_vec[j].clone(), - &[], - ) - .unwrap(); - let (m_b_w, beta_wi, _, _) = MessageB::b( - &sign_keys.w_i, - &paillier_key_vector[usize::from(signers_vec[usize::from(i - 1)])], - m_a_vec[j].clone(), - &[], - ) - .unwrap(); - m_b_gamma_send_vec.push(m_b_gamma); - m_b_w_send_vec.push(m_b_w); - beta_vec.push(beta_gamma); - ni_vec.push(beta_wi); - j += 1; - } - } - - let mut j = 0; - for i in 1..THRESHOLD + 2 { - if i != party_num_int { - assert!(sendp2p( - &client, - party_num_int, - i, - "round2", - serde_json::to_string(&(m_b_gamma_send_vec[j].clone(), m_b_w_send_vec[j].clone())) - .unwrap(), - uuid.clone() - ) - .is_ok()); - j += 1; - } - } - - let round2_ans_vec = poll_for_p2p( - &client, - party_num_int, - THRESHOLD + 1, - delay, - "round2", - uuid.clone(), - ); - - let mut m_b_gamma_rec_vec: Vec = Vec::new(); - let mut m_b_w_rec_vec: Vec = Vec::new(); - - for i in 0..THRESHOLD { - // if signers_vec.contains(&(i as usize)) { - let (m_b_gamma_i, m_b_w_i): (MessageB, MessageB) = - serde_json::from_str(&round2_ans_vec[i as usize]).unwrap(); - m_b_gamma_rec_vec.push(m_b_gamma_i); - m_b_w_rec_vec.push(m_b_w_i); - // } - } - - let mut alpha_vec: Vec> = Vec::new(); - let mut miu_vec: Vec> = Vec::new(); - - let mut j = 0; - for i in 1..THRESHOLD + 2 { - if i != party_num_int { - let m_b = m_b_gamma_rec_vec[j].clone(); - - let alpha_ij_gamma = m_b - .verify_proofs_get_alpha(&party_keys.dk, &sign_keys.k_i) - .expect("wrong dlog or m_b"); - let m_b = m_b_w_rec_vec[j].clone(); - let alpha_ij_wi = m_b - .verify_proofs_get_alpha(&party_keys.dk, &sign_keys.k_i) - .expect("wrong dlog or m_b"); - alpha_vec.push(alpha_ij_gamma.0); - miu_vec.push(alpha_ij_wi.0); - let g_w_i = Keys::update_commitments_to_xi( - &xi_com_vec[usize::from(signers_vec[usize::from(i - 1)])], - &vss_scheme_vec[usize::from(signers_vec[usize::from(i - 1)])], - signers_vec[usize::from(i - 1)], - &signers_vec, - ); - assert_eq!(m_b.b_proof.pk, g_w_i); - j += 1; - } - } - ////////////////////////////////////////////////////////////////////////////// - let delta_i = sign_keys.phase2_delta_i(&alpha_vec, &beta_vec); - let sigma = sign_keys.phase2_sigma_i(&miu_vec, &ni_vec); - - assert!(broadcast( - &client, - party_num_int, - "round3", - serde_json::to_string(&delta_i).unwrap(), - uuid.clone() - ) - .is_ok()); - let round3_ans_vec = poll_for_broadcasts( - &client, - party_num_int, - THRESHOLD + 1, - delay, - "round3", - uuid.clone(), - ); - let mut delta_vec: Vec> = Vec::new(); - format_vec_from_reads( - &round3_ans_vec, - party_num_int as usize, - delta_i, - &mut delta_vec, - ); - let delta_inv = SignKeys::phase3_reconstruct_delta(&delta_vec); - - ////////////////////////////////////////////////////////////////////////////// - // decommit to gamma_i - assert!(broadcast( - &client, - party_num_int, - "round4", - serde_json::to_string(&decommit).unwrap(), - uuid.clone() - ) - .is_ok()); - let round4_ans_vec = poll_for_broadcasts( - &client, - party_num_int, - THRESHOLD + 1, - delay, - "round4", - uuid.clone(), - ); - - let mut decommit_vec: Vec = Vec::new(); - format_vec_from_reads( - &round4_ans_vec, - party_num_int as usize, - decommit, - &mut decommit_vec, - ); - let decomm_i = decommit_vec.remove(usize::from(party_num_int - 1)); - bc1_vec.remove(usize::from(party_num_int - 1)); - let b_proof_vec = (0..m_b_gamma_rec_vec.len()) - .map(|i| &m_b_gamma_rec_vec[i].b_proof) - .collect::>>(); - let R = SignKeys::phase4(&delta_inv, &b_proof_vec, decommit_vec, &bc1_vec) - .expect("bad gamma_i decommit"); - - // adding local g_gamma_i - let R = R + decomm_i.g_gamma_i * delta_inv; - - // we assume the message is already hashed (by the signer). - let message_bn = BigInt::from_bytes(message); - let local_sig = - LocalSignature::phase5_local_sig(&sign_keys.k_i, &message_bn, &R, &sigma, &y_sum); - - let (phase5_com, phase_5a_decom, helgamal_proof, dlog_proof_rho) = - local_sig.phase5a_broadcast_5b_zkproof(); - - //phase (5A) broadcast commit - assert!(broadcast( - &client, - party_num_int, - "round5", - serde_json::to_string(&phase5_com).unwrap(), - uuid.clone() - ) - .is_ok()); - let round5_ans_vec = poll_for_broadcasts( - &client, - party_num_int, - THRESHOLD + 1, - delay, - "round5", - uuid.clone(), - ); - - let mut commit5a_vec: Vec = Vec::new(); - format_vec_from_reads( - &round5_ans_vec, - party_num_int as usize, - phase5_com, - &mut commit5a_vec, - ); - - //phase (5B) broadcast decommit and (5B) ZK proof - assert!(broadcast( - &client, - party_num_int, - "round6", - serde_json::to_string(&( - phase_5a_decom.clone(), - helgamal_proof.clone(), - dlog_proof_rho.clone() - )) - .unwrap(), - uuid.clone() - ) - .is_ok()); - let round6_ans_vec = poll_for_broadcasts( - &client, - party_num_int, - THRESHOLD + 1, - delay, - "round6", - uuid.clone(), - ); - - let mut decommit5a_and_elgamal_and_dlog_vec: Vec<( - Phase5ADecom1, - HomoELGamalProof, - DLogProof, - )> = Vec::new(); - format_vec_from_reads( - &round6_ans_vec, - party_num_int as usize, - (phase_5a_decom.clone(), helgamal_proof, dlog_proof_rho), - &mut decommit5a_and_elgamal_and_dlog_vec, - ); - let decommit5a_and_elgamal_and_dlog_vec_includes_i = - decommit5a_and_elgamal_and_dlog_vec.clone(); - decommit5a_and_elgamal_and_dlog_vec.remove(usize::from(party_num_int - 1)); - commit5a_vec.remove(usize::from(party_num_int - 1)); - let phase_5a_decomm_vec = (0..THRESHOLD) - .map(|i| decommit5a_and_elgamal_and_dlog_vec[i as usize].0.clone()) - .collect::>(); - let phase_5a_elgamal_vec = (0..THRESHOLD) - .map(|i| decommit5a_and_elgamal_and_dlog_vec[i as usize].1.clone()) - .collect::>>(); - let phase_5a_dlog_vec = (0..THRESHOLD) - .map(|i| decommit5a_and_elgamal_and_dlog_vec[i as usize].2.clone()) - .collect::>>(); - let (phase5_com2, phase_5d_decom2) = local_sig - .phase5c( - &phase_5a_decomm_vec, - &commit5a_vec, - &phase_5a_elgamal_vec, - &phase_5a_dlog_vec, - &phase_5a_decom.V_i, - &R, - ) - .expect("error phase5"); - - ////////////////////////////////////////////////////////////////////////////// - assert!(broadcast( - &client, - party_num_int, - "round7", - serde_json::to_string(&phase5_com2).unwrap(), - uuid.clone() - ) - .is_ok()); - let round7_ans_vec = poll_for_broadcasts( - &client, - party_num_int, - THRESHOLD + 1, - delay, - "round7", - uuid.clone(), - ); - - let mut commit5c_vec: Vec = Vec::new(); - format_vec_from_reads( - &round7_ans_vec, - party_num_int as usize, - phase5_com2, - &mut commit5c_vec, - ); - - //phase (5B) broadcast decommit and (5B) ZK proof - assert!(broadcast( - &client, - party_num_int, - "round8", - serde_json::to_string(&phase_5d_decom2).unwrap(), - uuid.clone() - ) - .is_ok()); - let round8_ans_vec = poll_for_broadcasts( - &client, - party_num_int, - THRESHOLD + 1, - delay, - "round8", - uuid.clone(), - ); - - let mut decommit5d_vec: Vec = Vec::new(); - format_vec_from_reads( - &round8_ans_vec, - party_num_int as usize, - phase_5d_decom2, - &mut decommit5d_vec, - ); - - let phase_5a_decomm_vec_includes_i = (0..=THRESHOLD) - .map(|i| { - decommit5a_and_elgamal_and_dlog_vec_includes_i[i as usize] - .0 - .clone() - }) - .collect::>(); - let s_i = local_sig - .phase5d( - &decommit5d_vec, - &commit5c_vec, - &phase_5a_decomm_vec_includes_i, - ) - .expect("bad com 5d"); - - ////////////////////////////////////////////////////////////////////////////// - assert!(broadcast( - &client, - party_num_int, - "round9", - serde_json::to_string(&s_i).unwrap(), - uuid.clone() - ) - .is_ok()); - let round9_ans_vec = - poll_for_broadcasts(&client, party_num_int, THRESHOLD + 1, delay, "round9", uuid); - - let mut s_i_vec: Vec> = Vec::new(); - format_vec_from_reads(&round9_ans_vec, party_num_int as usize, s_i, &mut s_i_vec); - - s_i_vec.remove(usize::from(party_num_int - 1)); - let sig = local_sig - .output_signature(&s_i_vec) - .expect("verification failed"); - println!("party {:?} Output Signature: \n", party_num_int); - println!("R: {:?}", sig.r); - println!("s: {:?} \n", sig.s); - println!("recid: {:?} \n", sig.recid.clone()); - - let sign_json = serde_json::to_string(&( - "r", - BigInt::from_bytes(sig.r.to_bytes().as_ref()).to_str_radix(16), - "s", - BigInt::from_bytes(sig.s.to_bytes().as_ref()).to_str_radix(16), - )) - .unwrap(); - - // check sig against secp256k1 - check_sig(&sig.r, &sig.s, &message_bn, &y_sum); - - fs::write("signature".to_string(), sign_json).expect("Unable to save !"); -} - -fn format_vec_from_reads<'a, T: serde::Deserialize<'a> + Clone>( - ans_vec: &'a [String], - party_num: usize, - value_i: T, - new_vec: &'a mut Vec, -) { - let mut j = 0; - for i in 1..ans_vec.len() + 2 { - if i == party_num { - new_vec.push(value_i.clone()); - } else { - let value_j: T = serde_json::from_str(&ans_vec[j]).unwrap(); - new_vec.push(value_j); - j += 1; - } - } -} - -pub fn signup(client: &Client) -> Result { - let key = "signup-sign".to_string(); - - let res_body = postb(client, "signupsign", key).unwrap(); - serde_json::from_str(&res_body).unwrap() -} diff --git a/multi-party-ecdsa/examples/gg18_sm_manager.rs b/multi-party-ecdsa/examples/gg18_sm_manager.rs deleted file mode 100644 index 9bbe55dd..00000000 --- a/multi-party-ecdsa/examples/gg18_sm_manager.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::collections::HashMap; -use std::fs; -use std::sync::RwLock; - -use rocket::serde::json::Json; -use rocket::{post, routes, State}; -use uuid::Uuid; - -mod common; -use common::{Entry, Index, Key, Params, PartySignup}; - -#[post("/get", format = "json", data = "")] -fn get( - db_mtx: &State>>, - request: Json, -) -> Json> { - let index: Index = request.0; - let hm = db_mtx.read().unwrap(); - match hm.get(&index.key) { - Some(v) => { - let entry = Entry { - key: index.key, - value: v.clone(), - }; - Json(Ok(entry)) - } - None => Json(Err(())), - } -} - -#[post("/set", format = "json", data = "")] -fn set(db_mtx: &State>>, request: Json) -> Json> { - let entry: Entry = request.0; - let mut hm = db_mtx.write().unwrap(); - hm.insert(entry.key.clone(), entry.value); - Json(Ok(())) -} - -#[post("/signupkeygen", format = "json")] -fn signup_keygen(db_mtx: &State>>) -> Json> { - let data = fs::read_to_string("params.json") - .expect("Unable to read params, make sure config file is present in the same folder "); - let params: Params = serde_json::from_str(&data).unwrap(); - let parties = params.parties.parse::().unwrap(); - - let key = "signup-keygen".to_string(); - - let party_signup = { - let hm = db_mtx.read().unwrap(); - let value = hm.get(&key).unwrap(); - let client_signup: PartySignup = serde_json::from_str(value).unwrap(); - if client_signup.number < parties { - PartySignup { - number: client_signup.number + 1, - uuid: client_signup.uuid, - } - } else { - PartySignup { - number: 1, - uuid: Uuid::new_v4().to_string(), - } - } - }; - - let mut hm = db_mtx.write().unwrap(); - hm.insert(key, serde_json::to_string(&party_signup).unwrap()); - Json(Ok(party_signup)) -} - -#[post("/signupsign", format = "json")] -fn signup_sign(db_mtx: &State>>) -> Json> { - //read parameters: - let data = fs::read_to_string("params.json") - .expect("Unable to read params, make sure config file is present in the same folder "); - let params: Params = serde_json::from_str(&data).unwrap(); - let threshold = params.threshold.parse::().unwrap(); - let key = "signup-sign".to_string(); - - let party_signup = { - let hm = db_mtx.read().unwrap(); - let value = hm.get(&key).unwrap(); - let client_signup: PartySignup = serde_json::from_str(value).unwrap(); - if client_signup.number < threshold + 1 { - PartySignup { - number: client_signup.number + 1, - uuid: client_signup.uuid, - } - } else { - PartySignup { - number: 1, - uuid: Uuid::new_v4().to_string(), - } - } - }; - - let mut hm = db_mtx.write().unwrap(); - hm.insert(key, serde_json::to_string(&party_signup).unwrap()); - Json(Ok(party_signup)) -} - -#[tokio::main] -async fn main() { - // let mut my_config = Config::development(); - // my_config.set_port(18001); - let db: HashMap = HashMap::new(); - let db_mtx = RwLock::new(db); - //rocket::custom(my_config).mount("/", routes![get, set]).manage(db_mtx).launch(); - - ///////////////////////////////////////////////////////////////// - //////////////////////////init signups:////////////////////////// - ///////////////////////////////////////////////////////////////// - - let keygen_key = "signup-keygen".to_string(); - let sign_key = "signup-sign".to_string(); - - let uuid_keygen = Uuid::new_v4().to_string(); - let uuid_sign = Uuid::new_v4().to_string(); - - let party1 = 0; - let party_signup_keygen = PartySignup { - number: party1, - uuid: uuid_keygen, - }; - let party_signup_sign = PartySignup { - number: party1, - uuid: uuid_sign, - }; - { - let mut hm = db_mtx.write().unwrap(); - hm.insert( - keygen_key, - serde_json::to_string(&party_signup_keygen).unwrap(), - ); - hm.insert(sign_key, serde_json::to_string(&party_signup_sign).unwrap()); - } - ///////////////////////////////////////////////////////////////// - rocket::build() - .mount("/", routes![get, set, signup_keygen, signup_sign]) - .manage(db_mtx) - .launch() - .await - .unwrap(); -} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/mod.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/mod.rs deleted file mode 100644 index c526d54f..00000000 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -/* - Multi-party ECDSA - - Copyright 2018 by Kzen Networks - - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) - - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - - @license GPL-3.0+ -*/ - -pub mod party_i; - -#[cfg(test)] -mod test; - -use curv::cryptographic_primitives::secret_sharing::feldman_vss::VerifiableSS as VSS; -pub type VerifiableSS = VSS; diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/party_i.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/party_i.rs deleted file mode 100644 index 0eaf37df..00000000 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/party_i.rs +++ /dev/null @@ -1,738 +0,0 @@ -#![allow(non_snake_case)] - -/* - Multi-party ECDSA - - Copyright 2018 by Kzen Networks - - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) - - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - - @license GPL-3.0+ -*/ - -use std::convert::TryFrom; - -use centipede::juggling::proof_system::{Helgamalsegmented, Witness}; -use centipede::juggling::segmentation::Msegmentation; -use curv::arithmetic::traits::*; -use curv::cryptographic_primitives::commitments::hash_commitment::HashCommitment; -use curv::cryptographic_primitives::commitments::traits::Commitment; -use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; -use curv::cryptographic_primitives::proofs::sigma_correct_homomorphic_elgamal_enc::*; -use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; -use curv::elliptic::curves::{Curve, Point, Scalar, Secp256k1}; -use curv::BigInt; -use paillier::{ - Decrypt, DecryptionKey, EncryptionKey, KeyGeneration, Paillier, RawCiphertext, RawPlaintext, -}; -use sha2::Sha256; -use zk_paillier::zkproofs::NiCorrectKeyProof; - -use serde::{Deserialize, Serialize}; - -use crate::Error::{self, InvalidCom, InvalidKey, InvalidSS, InvalidSig}; - -use super::VerifiableSS; - -const SECURITY: usize = 256; - -#[derive(Debug)] -pub struct Parameters { - pub threshold: u16, //t - pub share_count: u16, //n -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Keys { - pub u_i: Scalar, - pub y_i: Point, - pub dk: DecryptionKey, - pub ek: EncryptionKey, - pub party_index: u16, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PartyPrivate { - u_i: Scalar, - x_i: Scalar, - dk: DecryptionKey, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KeyGenBroadcastMessage1 { - pub e: EncryptionKey, - pub com: BigInt, - pub correct_key_proof: NiCorrectKeyProof, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KeyGenDecommitMessage1 { - pub blind_factor: BigInt, - pub y_i: Point, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SharedKeys { - pub y: Point, - pub x_i: Scalar, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SignKeys { - pub w_i: Scalar, - pub g_w_i: Point, - pub k_i: Scalar, - pub gamma_i: Scalar, - pub g_gamma_i: Point, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SignBroadcastPhase1 { - pub com: BigInt, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SignDecommitPhase1 { - pub blind_factor: BigInt, - pub g_gamma_i: Point, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LocalSignature { - pub l_i: Scalar, - pub rho_i: Scalar, - pub R: Point, - pub s_i: Scalar, - pub m: BigInt, - pub y: Point, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Phase5Com1 { - pub com: BigInt, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Phase5Com2 { - pub com: BigInt, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Phase5ADecom1 { - pub V_i: Point, - pub A_i: Point, - pub B_i: Point, - pub blind_factor: BigInt, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Phase5DDecom2 { - pub u_i: Point, - pub t_i: Point, - pub blind_factor: BigInt, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SignatureRecid { - pub r: Scalar, - pub s: Scalar, - pub recid: u8, -} - -impl Keys { - pub fn create(index: u16) -> Self { - let u = Scalar::::random(); - let y = Point::generator() * &u; - let (ek, dk) = Paillier::keypair().keys(); - - Self { - u_i: u, - y_i: y, - dk, - ek, - party_index: index, - } - } - - // we recommend using safe primes if the code is used in production - pub fn create_safe_prime(index: u16) -> Keys { - let u = Scalar::::random(); - let y = Point::generator() * &u; - - let (ek, dk) = Paillier::keypair_safe_primes().keys(); - - Keys { - u_i: u, - y_i: y, - dk, - ek, - party_index: index, - } - } - pub fn create_from(u: Scalar, index: u16) -> Keys { - let y = Point::generator() * &u; - let (ek, dk) = Paillier::keypair().keys(); - - Self { - u_i: u, - y_i: y, - dk, - ek, - party_index: index, - } - } - - pub fn phase1_broadcast_phase3_proof_of_correct_key( - &self, - ) -> (KeyGenBroadcastMessage1, KeyGenDecommitMessage1) { - let blind_factor = BigInt::sample(SECURITY); - let correct_key_proof = NiCorrectKeyProof::proof(&self.dk, None); - let com = HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(self.y_i.to_bytes(true).as_ref()), - &blind_factor, - ); - let bcm1 = KeyGenBroadcastMessage1 { - e: self.ek.clone(), - com, - correct_key_proof, - }; - let decom1 = KeyGenDecommitMessage1 { - blind_factor, - y_i: self.y_i.clone(), - }; - (bcm1, decom1) - } - - #[allow(clippy::type_complexity)] - pub fn phase1_verify_com_phase3_verify_correct_key_phase2_distribute( - &self, - params: &Parameters, - decom_vec: &[KeyGenDecommitMessage1], - bc1_vec: &[KeyGenBroadcastMessage1], - ) -> Result<(VerifiableSS, Vec>, u16), Error> { - // test length: - assert_eq!(decom_vec.len(), usize::from(params.share_count)); - assert_eq!(bc1_vec.len(), usize::from(params.share_count)); - // test paillier correct key and test decommitments - let correct_key_correct_decom_all = (0..bc1_vec.len()).all(|i| { - HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(decom_vec[i].y_i.to_bytes(true).as_ref()), - &decom_vec[i].blind_factor, - ) == bc1_vec[i].com - && bc1_vec[i] - .correct_key_proof - .verify(&bc1_vec[i].e, zk_paillier::zkproofs::SALT_STRING) - .is_ok() - }); - - let (vss_scheme, secret_shares) = - VerifiableSS::share(params.threshold, params.share_count, &self.u_i); - if correct_key_correct_decom_all { - Ok((vss_scheme, secret_shares.to_vec(), self.party_index)) - } else { - Err(InvalidKey) - } - } - - pub fn phase2_verify_vss_construct_keypair_phase3_pok_dlog( - &self, - params: &Parameters, - y_vec: &[Point], - secret_shares_vec: &[Scalar], - vss_scheme_vec: &[VerifiableSS], - index: u16, - ) -> Result<(SharedKeys, DLogProof), Error> { - assert_eq!(y_vec.len(), usize::from(params.share_count)); - assert_eq!(secret_shares_vec.len(), usize::from(params.share_count)); - assert_eq!(vss_scheme_vec.len(), usize::from(params.share_count)); - - let correct_ss_verify = (0..y_vec.len()).all(|i| { - vss_scheme_vec[i] - .validate_share(&secret_shares_vec[i], index) - .is_ok() - && vss_scheme_vec[i].commitments[0] == y_vec[i] - }); - - if correct_ss_verify { - let y: Point = y_vec.iter().sum(); - let x_i: Scalar = secret_shares_vec.iter().sum(); - let dlog_proof = DLogProof::prove(&x_i); - Ok((SharedKeys { y, x_i }, dlog_proof)) - } else { - Err(InvalidSS) - } - } - - pub fn get_commitments_to_xi( - vss_scheme_vec: &[VerifiableSS], - ) -> Vec> { - let len = vss_scheme_vec.len(); - (1..=u16::try_from(len).unwrap()) - .map(|i| { - (0..len) - .map(|j| vss_scheme_vec[j].get_point_commitment(i)) - .sum() - }) - .collect::>>() - } - - pub fn update_commitments_to_xi( - comm: &Point, - vss_scheme: &VerifiableSS, - index: u16, - s: &[u16], - ) -> Point { - let li = - VerifiableSS::::map_share_to_new_params(&vss_scheme.parameters, index, s); - comm * &li - } - - pub fn verify_dlog_proofs( - params: &Parameters, - dlog_proofs_vec: &[DLogProof], - y_vec: &[Point], - ) -> Result<(), Error> { - assert_eq!(y_vec.len(), usize::from(params.share_count)); - assert_eq!(dlog_proofs_vec.len(), usize::from(params.share_count)); - - let xi_dlog_verify = - (0..y_vec.len()).all(|i| DLogProof::verify(&dlog_proofs_vec[i]).is_ok()); - - if xi_dlog_verify { - Ok(()) - } else { - Err(InvalidKey) - } - } -} - -impl PartyPrivate { - pub fn set_private(key: Keys, shared_key: SharedKeys) -> Self { - Self { - u_i: key.u_i, - x_i: shared_key.x_i, - dk: key.dk, - } - } - - pub fn y_i(&self) -> Point { - Point::generator() * &self.u_i - } - - pub fn decrypt(&self, ciphertext: BigInt) -> RawPlaintext { - Paillier::decrypt(&self.dk, &RawCiphertext::from(ciphertext)) - } - - pub fn refresh_private_key(&self, factor: &Scalar, index: u16) -> Keys { - let u: Scalar = &self.u_i + factor; - let y = Point::generator() * &u; - let (ek, dk) = Paillier::keypair().keys(); - - Keys { - u_i: u, - y_i: y, - dk, - ek, - party_index: index, - } - } - - // we recommend using safe primes if the code is used in production - pub fn refresh_private_key_safe_prime(&self, factor: &Scalar, index: u16) -> Keys { - let u: Scalar = &self.u_i + factor; - let y = Point::generator() * &u; - let (ek, dk) = Paillier::keypair_safe_primes().keys(); - - Keys { - u_i: u, - y_i: y, - dk, - ek, - party_index: index, - } - } - - // used for verifiable recovery - pub fn to_encrypted_segment( - &self, - segment_size: usize, - num_of_segments: usize, - pub_ke_y: &Point, - g: &Point, - ) -> (Witness, Helgamalsegmented) { - Msegmentation::to_encrypted_segments(&self.u_i, &segment_size, num_of_segments, pub_ke_y, g) - } - - pub fn update_private_key( - &self, - factor_u_i: &Scalar, - factor_x_i: &Scalar, - ) -> Self { - PartyPrivate { - u_i: &self.u_i + factor_u_i, - x_i: &self.x_i + factor_x_i, - dk: self.dk.clone(), - } - } -} - -impl SignKeys { - pub fn create( - private: &PartyPrivate, - vss_scheme: &VerifiableSS, - index: u16, - s: &[u16], - ) -> Self { - let li = - VerifiableSS::::map_share_to_new_params(&vss_scheme.parameters, index, s); - let w_i = li * &private.x_i; - let g = Point::generator(); - let g_w_i = g * &w_i; - let gamma_i = Scalar::::random(); - let g_gamma_i = g * &gamma_i; - - Self { - w_i, - g_w_i, - k_i: Scalar::::random(), - gamma_i, - g_gamma_i, - } - } - - pub fn phase1_broadcast(&self) -> (SignBroadcastPhase1, SignDecommitPhase1) { - let blind_factor = BigInt::sample(SECURITY); - let g = Point::generator(); - let g_gamma_i = g * &self.gamma_i; - let com = HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(g_gamma_i.to_bytes(true).as_ref()), - &blind_factor, - ); - - ( - SignBroadcastPhase1 { com }, - SignDecommitPhase1 { - blind_factor, - g_gamma_i: self.g_gamma_i.clone(), - }, - ) - } - - pub fn phase2_delta_i( - &self, - alpha_vec: &[Scalar], - beta_vec: &[Scalar], - ) -> Scalar { - assert_eq!(alpha_vec.len(), beta_vec.len()); - let ki_gamma_i = &self.k_i * &self.gamma_i; - ki_gamma_i + alpha_vec.iter().chain(beta_vec).sum::>() - } - - pub fn phase2_sigma_i( - &self, - miu_vec: &[Scalar], - ni_vec: &[Scalar], - ) -> Scalar { - assert_eq!(miu_vec.len(), ni_vec.len()); - let ki_w_i = &self.k_i * &self.w_i; - ki_w_i + miu_vec.iter().chain(ni_vec).sum::>() - } - - pub fn phase3_reconstruct_delta(delta_vec: &[Scalar]) -> Scalar { - delta_vec - .iter() - .sum::>() - .invert() - .expect("sum of deltas is zero") - } - - pub fn phase4( - delta_inv: &Scalar, - b_proof_vec: &[&DLogProof], - phase1_decommit_vec: Vec, - bc1_vec: &[SignBroadcastPhase1], - ) -> Result, Error> { - // note: b_proof_vec is populated using the results - //from the MtAwc, which is handling the proof of knowledge verification of gamma_i such that - // Gamme_i = gamma_i * G in the verify_proofs_get_alpha() - let test_b_vec_and_com = (0..b_proof_vec.len()).all(|i| { - b_proof_vec[i].pk == phase1_decommit_vec[i].g_gamma_i - && HashCommitment::::create_commitment_with_user_defined_randomness( - &BigInt::from_bytes(phase1_decommit_vec[i].g_gamma_i.to_bytes(true).as_ref()), - &phase1_decommit_vec[i].blind_factor, - ) == bc1_vec[i].com - }); - - if test_b_vec_and_com { - Ok({ - let gamma_sum: Point = phase1_decommit_vec - .iter() - .map(|decom| &decom.g_gamma_i) - .sum(); - // R - gamma_sum * delta_inv - }) - } else { - Err(InvalidKey) - } - } -} - -impl LocalSignature { - pub fn phase5_local_sig( - k_i: &Scalar, - message: &BigInt, - R: &Point, - sigma_i: &Scalar, - pubkey: &Point, - ) -> Self { - let m_fe = Scalar::::from(message); - let r = Scalar::::from( - &R.x_coord() - .unwrap() - .mod_floor(Scalar::::group_order()), - ); - let s_i = m_fe * k_i + r * sigma_i; - let l_i = Scalar::::random(); - let rho_i = Scalar::::random(); - Self { - l_i, - rho_i, - R: R.clone(), - s_i, - m: message.clone(), - y: pubkey.clone(), - } - } - - pub fn phase5a_broadcast_5b_zkproof( - &self, - ) -> ( - Phase5Com1, - Phase5ADecom1, - HomoELGamalProof, - DLogProof, - ) { - let blind_factor = BigInt::sample(SECURITY); - let g = Point::generator(); - let A_i = g * &self.rho_i; - let l_i_rho_i = &self.l_i * &self.rho_i; - let B_i = g * l_i_rho_i; - let V_i = &self.R * &self.s_i + g * &self.l_i; - let input_hash = Sha256::new() - .chain_points([&V_i, &A_i, &B_i]) - .result_bigint(); - let com = HashCommitment::::create_commitment_with_user_defined_randomness( - &input_hash, - &blind_factor, - ); - let witness = HomoElGamalWitness { - r: self.l_i.clone(), - x: self.s_i.clone(), - }; - let delta = HomoElGamalStatement { - G: A_i.clone(), - H: self.R.clone(), - Y: g.to_point(), - D: V_i.clone(), - E: B_i.clone(), - }; - let dlog_proof_rho = DLogProof::prove(&self.rho_i); - let proof = HomoELGamalProof::prove(&witness, &delta); - - ( - Phase5Com1 { com }, - Phase5ADecom1 { - V_i, - A_i, - B_i, - blind_factor, - }, - proof, - dlog_proof_rho, - ) - } - - pub fn phase5c( - &self, - decom_vec: &[Phase5ADecom1], - com_vec: &[Phase5Com1], - elgamal_proofs: &[HomoELGamalProof], - dlog_proofs_rho: &[DLogProof], - v_i: &Point, - R: &Point, - ) -> Result<(Phase5Com2, Phase5DDecom2), Error> { - assert_eq!(decom_vec.len(), com_vec.len()); - - let g = Point::generator(); - let test_com_elgamal = (0..com_vec.len()).all(|i| { - let delta = HomoElGamalStatement { - G: decom_vec[i].A_i.clone(), - H: R.clone(), - Y: g.to_point(), - D: decom_vec[i].V_i.clone(), - E: decom_vec[i].B_i.clone(), - }; - - let input_hash = Sha256::new() - .chain_points([&decom_vec[i].V_i, &decom_vec[i].A_i, &decom_vec[i].B_i]) - .result_bigint(); - - HashCommitment::::create_commitment_with_user_defined_randomness( - &input_hash, - &decom_vec[i].blind_factor, - ) == com_vec[i].com - && elgamal_proofs[i].verify(&delta).is_ok() - && DLogProof::verify(&dlog_proofs_rho[i]).is_ok() - }); - - let v_iter = (0..com_vec.len()).map(|i| &decom_vec[i].V_i); - let a_iter = (0..com_vec.len()).map(|i| &decom_vec[i].A_i); - - let v = v_i + v_iter.sum::>(); - // V = -mG -ry - sum (vi) - let a: Point = a_iter.sum(); - - let r = Scalar::::from( - &self - .R - .x_coord() - .ok_or(Error::InvalidSig)? - .mod_floor(Scalar::::group_order()), - ); - let yr = &self.y * r; - let g = Point::generator(); - let m_fe = Scalar::::from(&self.m); - let gm = g * m_fe; - let v = v - &gm - &yr; - let u_i = v * &self.rho_i; - let t_i = a * &self.l_i; - let input_hash = Sha256::new().chain_points([&u_i, &t_i]).result_bigint(); - let blind_factor = BigInt::sample(SECURITY); - let com = HashCommitment::::create_commitment_with_user_defined_randomness( - &input_hash, - &blind_factor, - ); - - if test_com_elgamal { - Ok({ - ( - Phase5Com2 { com }, - Phase5DDecom2 { - u_i, - t_i, - blind_factor, - }, - ) - }) - } else { - Err(InvalidCom) - } - } - - pub fn phase5d( - &self, - decom_vec2: &[Phase5DDecom2], - com_vec2: &[Phase5Com2], - decom_vec1: &[Phase5ADecom1], - ) -> Result, Error> { - assert_eq!(decom_vec2.len(), decom_vec1.len()); - assert_eq!(decom_vec2.len(), com_vec2.len()); - - let test_com = (0..com_vec2.len()).all(|i| { - let input_hash = Sha256::new() - .chain_points([&decom_vec2[i].u_i, &decom_vec2[i].t_i]) - .result_bigint(); - HashCommitment::::create_commitment_with_user_defined_randomness( - &input_hash, - &decom_vec2[i].blind_factor, - ) == com_vec2[i].com - }); - - let t_iter = decom_vec2.iter().map(|decom| &decom.t_i); - let u_iter = decom_vec2.iter().map(|decom| &decom.u_i); - let b_iter = decom_vec1.iter().map(|decom| &decom.B_i); - - let g = Point::generator(); - let biased_sum_tb = g + t_iter.chain(b_iter).sum::>(); - let biased_sum_tb_minus_u = biased_sum_tb - u_iter.sum::>(); - if test_com { - if *g.as_point() == biased_sum_tb_minus_u { - Ok(self.s_i.clone()) - } else { - Err(InvalidKey) - } - } else { - Err(InvalidCom) - } - } - pub fn output_signature(&self, s_vec: &[Scalar]) -> Result { - let mut s = &self.s_i + s_vec.iter().sum::>(); - let s_bn = s.to_bigint(); - - let r = Scalar::::from( - &self - .R - .x_coord() - .ok_or(Error::InvalidSig)? - .mod_floor(Scalar::::group_order()), - ); - let ry: BigInt = self - .R - .y_coord() - .ok_or(Error::InvalidSig)? - .mod_floor(Scalar::::group_order()); - - /* - Calculate recovery id - it is not possible to compute the public key out of the signature - itself. Recovery id is used to enable extracting the public key uniquely. - 1. id = R.y & 1 - 2. if (s > curve.q / 2) id = id ^ 1 - */ - let is_ry_odd = ry.test_bit(0); - let mut recid = if is_ry_odd { 1 } else { 0 }; - let s_tag_bn = Scalar::::group_order() - &s_bn; - if s_bn > s_tag_bn { - s = Scalar::::from(&s_tag_bn); - recid ^= 1; - } - let sig = SignatureRecid { r, s, recid }; - let ver = verify(&sig, &self.y, &self.m).is_ok(); - if ver { - Ok(sig) - } else { - Err(InvalidSig) - } - } -} - -pub fn verify(sig: &SignatureRecid, y: &Point, message: &BigInt) -> Result<(), Error> { - let b = sig.s.invert().ok_or(Error::InvalidSig)?; - let a = Scalar::::from(message); - let u1 = a * &b; - let u2 = &sig.r * &b; - - let g = Point::generator(); - let gu1 = g * u1; - let yu2 = y * &u2; - // can be faster using shamir trick - - if sig.r - == Scalar::::from( - &(gu1 + yu2) - .x_coord() - .ok_or(Error::InvalidSig)? - .mod_floor(Scalar::::group_order()), - ) - { - Ok(()) - } else { - Err(InvalidSig) - } -} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/test.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/test.rs deleted file mode 100644 index 3e37f271..00000000 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2018/test.rs +++ /dev/null @@ -1,460 +0,0 @@ -#![allow(non_snake_case)] - -/* - Multi-party ECDSA - - Copyright 2018 by Kzen Networks - - This file is part of Multi-party ECDSA library - (https://github.com/KZen-networks/multi-party-ecdsa) - - Multi-party ECDSA is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public - License as published by the Free Software Foundation, either - version 3 of the License, or (at your option) any later version. - - @license GPL-3.0+ -*/ - -use crate::protocols::multi_party_ecdsa::gg_2018::party_i::{ - verify, KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Keys, LocalSignature, Parameters, - PartyPrivate, Phase5ADecom1, Phase5Com1, SharedKeys, SignKeys, -}; -use crate::utilities::mta::{MessageA, MessageB}; - -use curv::arithmetic::traits::Converter; -use curv::cryptographic_primitives::hashing::{Digest, DigestExt}; -use curv::cryptographic_primitives::proofs::sigma_dlog::DLogProof; -use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; -use paillier::*; -use sha2::Sha256; - -use super::VerifiableSS; - -#[test] -fn test_keygen_t1_n2() { - keygen_t_n_parties(1, 2); -} - -#[test] -fn test_keygen_t2_n3() { - keygen_t_n_parties(2, 3); -} - -#[test] -fn test_keygen_t2_n4() { - keygen_t_n_parties(2, 4); -} - -#[test] -fn test_sign_n5_t2_ttag4() { - sign(2, 5, 4, vec![0, 2, 3, 4]) -} -#[test] -fn test_sign_n8_t4_ttag6() { - sign(4, 8, 6, vec![0, 1, 2, 4, 6, 7]) -} - -fn keygen_t_n_parties( - t: u16, - n: u16, -) -> ( - Vec, - Vec>, - Vec>, - Point, - VerifiableSS, -) { - let parames = Parameters { - threshold: t, - share_count: n, - }; - let party_keys_vec = (0..n).map(Keys::create).collect::>(); - - let (bc1_vec, decom_vec): (Vec<_>, Vec<_>) = party_keys_vec - .iter() - .map(|k| k.phase1_broadcast_phase3_proof_of_correct_key()) - .unzip(); - - let y_vec = (0..usize::from(n)) - .map(|i| decom_vec[i].y_i.clone()) - .collect::>>(); - let mut y_vec_iter = y_vec.iter(); - let head = y_vec_iter.next().unwrap(); - let tail = y_vec_iter; - let y_sum = tail.fold(head.clone(), |acc, x| acc + x); - - let mut vss_scheme_vec = Vec::new(); - let mut secret_shares_vec = Vec::new(); - let mut index_vec = Vec::new(); - - let vss_result: Vec<_> = party_keys_vec - .iter() - .map(|k| { - k.phase1_verify_com_phase3_verify_correct_key_phase2_distribute( - ¶mes, &decom_vec, &bc1_vec, - ) - .expect("invalid key") - }) - .collect(); - - for (vss_scheme, secret_shares, index) in vss_result { - vss_scheme_vec.push(vss_scheme); - secret_shares_vec.push(secret_shares); // cannot unzip - index_vec.push(index as u16); - } - - let vss_scheme_for_test = vss_scheme_vec.clone(); - - let party_shares = (0..usize::from(n)) - .map(|i| { - (0..usize::from(n)) - .map(|j| secret_shares_vec[j][i].clone()) - .collect::>>() - }) - .collect::>>>(); - - let mut shared_keys_vec = Vec::new(); - let mut dlog_proof_vec = Vec::new(); - for (i, key) in party_keys_vec.iter().enumerate() { - let (shared_keys, dlog_proof) = key - .phase2_verify_vss_construct_keypair_phase3_pok_dlog( - ¶mes, - &y_vec, - &party_shares[i], - &vss_scheme_vec, - (&index_vec[i] + 1).into(), - ) - .expect("invalid vss"); - shared_keys_vec.push(shared_keys); - dlog_proof_vec.push(dlog_proof); - } - - let pk_vec = dlog_proof_vec - .iter() - .map(|dlog_proof| dlog_proof.pk.clone()) - .collect::>>(); - - //both parties run: - Keys::verify_dlog_proofs(¶mes, &dlog_proof_vec, &y_vec).expect("bad dlog proof"); - - //test - let xi_vec = shared_keys_vec - .iter() - .take(usize::from(t + 1)) - .map(|shared_keys| shared_keys.x_i.clone()) - .collect::>>(); - let x = vss_scheme_for_test[0] - .clone() - .reconstruct(&index_vec[0..=usize::from(t)], &xi_vec); - let sum_u_i = party_keys_vec - .iter() - .fold(Scalar::::zero(), |acc, x| acc + &x.u_i); - assert_eq!(x, sum_u_i); - - ( - party_keys_vec, - shared_keys_vec, - pk_vec, - y_sum, - vss_scheme_for_test[0].clone(), - ) -} - -fn sign(t: u16, n: u16, ttag: u16, s: Vec) { - // full key gen emulation - let (party_keys_vec, shared_keys_vec, _pk_vec, y, vss_scheme) = keygen_t_n_parties(t, n); - - let private_vec = (0..shared_keys_vec.len()) - .map(|i| PartyPrivate::set_private(party_keys_vec[i].clone(), shared_keys_vec[i].clone())) - .collect::>(); - // make sure that we have t t); - let ttag = ttag as usize; - assert_eq!(s.len(), ttag); - - // each party creates a signing key. This happens in parallel IRL. In this test we - // create a vector of signing keys, one for each party. - // throughout i will index parties - let sign_keys_vec = (0..ttag) - .map(|i| SignKeys::create(&private_vec[usize::from(s[i])], &vss_scheme, s[i], &s)) - .collect::>(); - - // each party computes [Ci,Di] = com(g^gamma_i) and broadcast the commitments - let (bc1_vec, decommit_vec1): (Vec<_>, Vec<_>) = - sign_keys_vec.iter().map(|k| k.phase1_broadcast()).unzip(); - - // each party i sends encryption of k_i under her Paillier key - // m_a_vec = [ma_0;ma_1;,...] - // range proofs are ignored here, as there's no h1, h2, N_tilde setup in this version of GG18 - let m_a_vec: Vec<_> = sign_keys_vec - .iter() - .enumerate() - .map(|(i, k)| MessageA::a(&k.k_i, &party_keys_vec[usize::from(s[i])].ek, &[]).0) - .collect(); - - // each party i sends responses to m_a_vec she received (one response with input gamma_i and one with w_i) - // m_b_gamma_vec_all is a matrix where column i is a vector of message_b's that party i answers to all ma_{j!=i} using paillier key of party j to answer to ma_j - - // aggregation of the n messages of all parties - let mut m_b_gamma_vec_all = Vec::new(); - let mut beta_vec_all = Vec::new(); - let mut m_b_w_vec_all = Vec::new(); - let mut ni_vec_all = Vec::new(); - - for (i, key) in sign_keys_vec.iter().enumerate() { - let mut m_b_gamma_vec = Vec::new(); - let mut beta_vec = Vec::new(); - let mut m_b_w_vec = Vec::new(); - let mut ni_vec = Vec::new(); - - for j in 0..ttag - 1 { - let ind = if j < i { j } else { j + 1 }; - - let (m_b_gamma, beta_gamma, _, _) = MessageB::b( - &key.gamma_i, - &party_keys_vec[usize::from(s[ind])].ek, - m_a_vec[ind].clone(), - &[], - ) - .unwrap(); - let (m_b_w, beta_wi, _, _) = MessageB::b( - &key.w_i, - &party_keys_vec[usize::from(s[ind])].ek, - m_a_vec[ind].clone(), - &[], - ) - .unwrap(); - - m_b_gamma_vec.push(m_b_gamma); - beta_vec.push(beta_gamma); - m_b_w_vec.push(m_b_w); - ni_vec.push(beta_wi); - } - m_b_gamma_vec_all.push(m_b_gamma_vec.clone()); - beta_vec_all.push(beta_vec.clone()); - m_b_w_vec_all.push(m_b_w_vec.clone()); - ni_vec_all.push(ni_vec.clone()); - } - - // Here we complete the MwA protocols by taking the mb matrices and starting with the first column generating the appropriate message - // for example for index i=0 j=0 we need party at index s[1] to answer to mb that party s[0] sent, completing a protocol between s[0] and s[1]. - // for index i=1 j=0 we need party at index s[0] to answer to mb that party s[1]. etc. - // IRL each party i should get only the mb messages that other parties sent in response to the party i ma's. - // TODO: simulate as IRL - let mut alpha_vec_all = Vec::new(); - let mut miu_vec_all = Vec::new(); - - for i in 0..ttag { - let mut alpha_vec = Vec::new(); - let mut miu_vec = Vec::new(); - - let m_b_gamma_vec_i = &m_b_gamma_vec_all[i]; - let m_b_w_vec_i = &m_b_w_vec_all[i]; - - for j in 0..ttag - 1 { - let ind = if j < i { j } else { j + 1 }; - let m_b = m_b_gamma_vec_i[j].clone(); - - let alpha_ij_gamma = m_b - .verify_proofs_get_alpha( - &party_keys_vec[usize::from(s[ind])].dk, - &sign_keys_vec[ind].k_i, - ) - .expect("wrong dlog or m_b"); - let m_b = m_b_w_vec_i[j].clone(); - let alpha_ij_wi = m_b - .verify_proofs_get_alpha( - &party_keys_vec[usize::from(s[ind])].dk, - &sign_keys_vec[ind].k_i, - ) - .expect("wrong dlog or m_b"); - - // since we actually run two MtAwc each party needs to make sure that the values B are the same as the public values - // here for b=w_i the parties already know W_i = g^w_i for each party so this check is done here. for b = gamma_i the check will be later when g^gamma_i will become public - // currently we take the W_i from the other parties signing keys - // TODO: use pk_vec (first change from x_i to w_i) for this check. - assert_eq!(m_b.b_proof.pk, sign_keys_vec[i].g_w_i); - - alpha_vec.push(alpha_ij_gamma); - miu_vec.push(alpha_ij_wi); - } - alpha_vec_all.push(alpha_vec.clone()); - miu_vec_all.push(miu_vec.clone()); - } - - let mut delta_vec = Vec::new(); - let mut sigma_vec = Vec::new(); - - for i in 0..ttag { - let alpha_vec: Vec> = (0..alpha_vec_all[i].len()) - .map(|j| alpha_vec_all[i][j].0.clone()) - .collect(); - let miu_vec: Vec> = (0..miu_vec_all[i].len()) - .map(|j| miu_vec_all[i][j].0.clone()) - .collect(); - - let delta = sign_keys_vec[i].phase2_delta_i(&alpha_vec[..], &beta_vec_all[i]); - let sigma = sign_keys_vec[i].phase2_sigma_i(&miu_vec[..], &ni_vec_all[i]); - delta_vec.push(delta); - sigma_vec.push(sigma); - } - - // all parties broadcast delta_i and compute delta_i ^(-1) - let delta_inv = SignKeys::phase3_reconstruct_delta(&delta_vec); - - // de-commit to g^gamma_i from phase1, test comm correctness, and that it is the same value used in MtA. - // Return R - - let _g_gamma_i_vec = (0..ttag) - .map(|i| sign_keys_vec[i].g_gamma_i.clone()) - .collect::>>(); - - let R_vec = (0..ttag) - .map(|_| { - // each party i tests all B = g^b = g ^ gamma_i she received. - let b_proof_vec = (0..ttag) - .map(|j| { - let b_gamma_vec = &m_b_gamma_vec_all[j]; - &b_gamma_vec[0].b_proof - }) - .collect::>>(); - SignKeys::phase4(&delta_inv, &b_proof_vec, decommit_vec1.clone(), &bc1_vec) - .expect("bad gamma_i decommit") - }) - .collect::>>(); - - let message: [u8; 4] = [79, 77, 69, 82]; - let message_bn = Sha256::new() - .chain_bigint(&BigInt::from_bytes(&message[..])) - .result_bigint(); - let mut local_sig_vec = Vec::new(); - - // each party computes s_i but don't send it yet. we start with phase5 - for i in 0..ttag { - let local_sig = LocalSignature::phase5_local_sig( - &sign_keys_vec[i].k_i, - &message_bn, - &R_vec[i], - &sigma_vec[i], - &y, - ); - local_sig_vec.push(local_sig); - } - - let mut phase5_com_vec: Vec = Vec::new(); - let mut phase_5a_decom_vec: Vec = Vec::new(); - let mut helgamal_proof_vec = Vec::new(); - let mut dlog_proof_rho_vec = Vec::new(); - // we notice that the proof for V= R^sg^l, B = A^l is a general form of homomorphic elgamal. - for sig in &local_sig_vec { - let (phase5_com, phase_5a_decom, helgamal_proof, dlog_proof_rho) = - sig.phase5a_broadcast_5b_zkproof(); - phase5_com_vec.push(phase5_com); - phase_5a_decom_vec.push(phase_5a_decom); - helgamal_proof_vec.push(helgamal_proof); - dlog_proof_rho_vec.push(dlog_proof_rho); - } - - let mut phase5_com2_vec = Vec::new(); - let mut phase_5d_decom2_vec = Vec::new(); - for i in 0..ttag { - let mut phase_5a_decom_vec_clone = phase_5a_decom_vec.clone(); - let mut phase_5a_com_vec_clone = phase5_com_vec.clone(); - let mut phase_5b_elgamal_vec_clone = helgamal_proof_vec.clone(); - - let _decom_i = phase_5a_decom_vec_clone.remove(i); - let _com_i = phase_5a_com_vec_clone.remove(i); - let _elgamal_i = phase_5b_elgamal_vec_clone.remove(i); - // for j in 0..s_minus_i.len() { - let (phase5_com2, phase_5d_decom2) = local_sig_vec[i] - .phase5c( - &phase_5a_decom_vec_clone, - &phase_5a_com_vec_clone, - &phase_5b_elgamal_vec_clone, - &dlog_proof_rho_vec, - &phase_5a_decom_vec[i].V_i, - &R_vec[0], - ) - .expect("error phase5"); - phase5_com2_vec.push(phase5_com2); - phase_5d_decom2_vec.push(phase_5d_decom2); - // } - } - - // assuming phase5 checks passes each party sends s_i and compute sum_i{s_i} - let mut s_vec: Vec> = Vec::new(); - for sig in &local_sig_vec { - let s_i = sig - .phase5d(&phase_5d_decom2_vec, &phase5_com2_vec, &phase_5a_decom_vec) - .expect("bad com 5d"); - s_vec.push(s_i); - } - - // here we compute the signature only of party i=0 to demonstrate correctness. - s_vec.remove(0); - let sig = local_sig_vec[0] - .output_signature(&s_vec) - .expect("verification failed"); - - assert_eq!(local_sig_vec[0].y, y); - verify(&sig, &local_sig_vec[0].y, &local_sig_vec[0].m).unwrap(); - check_sig(&sig.r, &sig.s, &local_sig_vec[0].m, &y); -} - -fn check_sig(r: &Scalar, s: &Scalar, msg: &BigInt, pk: &Point) { - use secp256k1::{Message, PublicKey, Signature, SECP256K1}; - - let raw_msg = BigInt::to_bytes(msg); - let mut msg: Vec = Vec::new(); // padding - msg.extend(vec![0u8; 32 - raw_msg.len()]); - msg.extend(raw_msg.iter()); - - let msg = Message::from_slice(msg.as_slice()).unwrap(); - let slice = pk.to_bytes(false); - let mut raw_pk = Vec::new(); - if slice.len() != 65 { - // after curv's pk_to_key_slice return 65 bytes, this can be removed - raw_pk.insert(0, 4u8); - raw_pk.extend(vec![0u8; 64 - slice.len()]); - raw_pk.extend(slice.as_ref()); - } else { - raw_pk.extend(slice.as_ref()); - } - - assert_eq!(raw_pk.len(), 65); - - let pk = PublicKey::from_slice(&raw_pk).unwrap(); - - let mut compact: Vec = Vec::new(); - let bytes_r = &r.to_bytes()[..]; - compact.extend(vec![0u8; 32 - bytes_r.len()]); - compact.extend(bytes_r.iter()); - - let bytes_s = &s.to_bytes()[..]; - compact.extend(vec![0u8; 32 - bytes_s.len()]); - compact.extend(bytes_s.iter()); - - let secp_sig = Signature::from_compact(compact.as_slice()).unwrap(); - - let is_correct = SECP256K1.verify(&msg, &secp_sig, &pk).is_ok(); - assert!(is_correct); -} - -#[test] -fn test_serialize_deserialize() { - use serde_json; - - let k = Keys::create(0); - let (commit, decommit) = k.phase1_broadcast_phase3_proof_of_correct_key(); - - let encoded = serde_json::to_string(&commit).unwrap(); - let decoded: KeyGenBroadcastMessage1 = serde_json::from_str(&encoded).unwrap(); - assert_eq!(commit.com, decoded.com); - - let encoded = serde_json::to_string(&decommit).unwrap(); - let decoded: KeyGenDecommitMessage1 = serde_json::from_str(&encoded).unwrap(); - assert_eq!(decommit.y_i, decoded.y_i); -} diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/mod.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/mod.rs index 5dec3258..7f1ca23d 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/mod.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/mod.rs @@ -17,6 +17,10 @@ pub mod blame; pub mod party_i; pub mod state_machine; + +use curv::cryptographic_primitives::secret_sharing::feldman_vss::VerifiableSS as VSS; +pub type VerifiableSS = VSS; + #[cfg(test)] mod test; diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs index cf64924b..0dcfe266 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/party_i.rs @@ -29,7 +29,7 @@ use curv::elliptic::curves::{secp256_k1::Secp256k1, Curve, Point, Scalar}; use curv::BigInt; use sha2::Sha256; -use crate::protocols::multi_party_ecdsa::gg_2018::VerifiableSS; +use super::VerifiableSS; use crate::Error::{self, InvalidSig, Phase5BadSum, Phase6Error}; use paillier::{ Decrypt, DecryptionKey, EncryptionKey, KeyGeneration, Paillier, RawCiphertext, RawPlaintext, diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs index 5159e12e..3e2861d5 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen.rs @@ -15,8 +15,7 @@ use serde::{Deserialize, Serialize}; use sha2::Sha256; use thiserror::Error; -use crate::protocols::multi_party_ecdsa::gg_2018::VerifiableSS; -use crate::protocols::multi_party_ecdsa::gg_2020; +use crate::protocols::multi_party_ecdsa::gg_2020::{self, VerifiableSS}; mod rounds; diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs index 5c23a867..74cb39cd 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/state_machine/keygen/rounds.rs @@ -15,10 +15,9 @@ use round_based::containers::{self, BroadcastMsgs, MessageStore, P2PMsgs, P2PMsg use round_based::{IsCritical, Msg}; use zk_paillier::zkproofs::DLogStatement; -use crate::protocols::multi_party_ecdsa::gg_2018::VerifiableSS; -use crate::protocols::multi_party_ecdsa::gg_2020::party_i::{ +use crate::protocols::multi_party_ecdsa::gg_2020::{VerifiableSS, party_i::{ KeyGenBroadcastMessage1, KeyGenDecommitMessage1, Keys, -}; +},}; use crate::protocols::multi_party_ecdsa::gg_2020::{self, ErrorType}; pub struct Round0 { diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs index 9edc567e..26eeb333 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/gg_2020/test.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case)] -use crate::protocols::multi_party_ecdsa::gg_2018::VerifiableSS; +use crate::protocols::multi_party_ecdsa::gg_2020::VerifiableSS; /* Multi-party ECDSA diff --git a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs index 43ded3aa..af2cbc54 100644 --- a/multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs +++ b/multi-party-ecdsa/src/protocols/multi_party_ecdsa/mod.rs @@ -14,5 +14,4 @@ @license GPL-3.0+ */ -pub mod gg_2018; pub mod gg_2020; diff --git a/multi-party-ecdsa/src/utilities/mta/mod.rs b/multi-party-ecdsa/src/utilities/mta/mod.rs index 570ac872..43009367 100644 --- a/multi-party-ecdsa/src/utilities/mta/mod.rs +++ b/multi-party-ecdsa/src/utilities/mta/mod.rs @@ -27,7 +27,7 @@ use zk_paillier::zkproofs::DLogStatement; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use crate::protocols::multi_party_ecdsa::gg_2018::party_i::PartyPrivate; +use crate::protocols::multi_party_ecdsa::gg_2020::party_i::PartyPrivate; use crate::utilities::mta::range_proofs::AliceProof; use crate::Error::{self, InvalidKey}; From e87307258cbdecf32070112f220542b457142a8a Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 08:45:15 +0800 Subject: [PATCH 24/41] Remove benchmarks as they all apply to removed protocols. --- .../benches/multi_party_ecdsa/gg18/keygen.rs | 135 ------------------ .../two_party_ecdsa/cclst_2019/keygen.rs | 62 -------- .../two_party_ecdsa/cclst_2019/sign.rs | 90 ------------ .../two_party_ecdsa/lindell_2017/keygen.rs | 81 ----------- .../two_party_ecdsa/lindell_2017/sign.rs | 75 ---------- 5 files changed, 443 deletions(-) delete mode 100644 multi-party-ecdsa/benches/multi_party_ecdsa/gg18/keygen.rs delete mode 100644 multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/keygen.rs delete mode 100644 multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/sign.rs delete mode 100644 multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/keygen.rs delete mode 100644 multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/sign.rs diff --git a/multi-party-ecdsa/benches/multi_party_ecdsa/gg18/keygen.rs b/multi-party-ecdsa/benches/multi_party_ecdsa/gg18/keygen.rs deleted file mode 100644 index 061721e2..00000000 --- a/multi-party-ecdsa/benches/multi_party_ecdsa/gg18/keygen.rs +++ /dev/null @@ -1,135 +0,0 @@ -use criterion::criterion_main; - -mod bench { - use criterion::{criterion_group, Criterion}; - use curv::cryptographic_primitives::secret_sharing::feldman_vss::VerifiableSS; - use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; - use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2018::party_i::*; - pub fn bench_full_keygen_party_one_two(c: &mut Criterion) { - c.bench_function("keygen t=1 n=2", move |b| { - b.iter(|| { - keygen_t_n_parties(1, 2); - }) - }); - } - pub fn bench_full_keygen_party_two_three(c: &mut Criterion) { - c.bench_function("keygen t=2 n=3", move |b| { - b.iter(|| { - keygen_t_n_parties(2, 3); - }) - }); - } - pub fn keygen_t_n_parties( - t: u16, - n: u16, - ) -> ( - Vec, - Vec>, - Vec>, - Point, - VerifiableSS, - ) { - let parames = Parameters { - threshold: t, - share_count: n, - }; - let (t, n) = (t as usize, n as usize); - let party_keys_vec = (0..n) - .map(|i| Keys::create(i as u16)) - .collect::>(); - - let mut bc1_vec = Vec::new(); - let mut decom_vec = Vec::new(); - - for key in &party_keys_vec { - let (bc1, decom1) = key.phase1_broadcast_phase3_proof_of_correct_key(); - bc1_vec.push(bc1); - decom_vec.push(decom1); - } - - let y_vec = (0..n) - .map(|i| decom_vec[i].y_i.clone()) - .collect::>>(); - let mut y_vec_iter = y_vec.iter(); - let head = y_vec_iter.next().unwrap(); - let tail = y_vec_iter; - let y_sum = tail.fold(head.clone(), |acc, x| acc + x); - let mut vss_scheme_vec = Vec::new(); - let mut secret_shares_vec = Vec::new(); - let mut index_vec = Vec::new(); - for key in &party_keys_vec { - let (vss_scheme, secret_shares, index) = key - .phase1_verify_com_phase3_verify_correct_key_phase2_distribute( - ¶mes, &decom_vec, &bc1_vec, - ) - .expect("invalid key"); - vss_scheme_vec.push(vss_scheme); - secret_shares_vec.push(secret_shares); - index_vec.push(index as u16); - } - let vss_scheme_for_test = vss_scheme_vec.clone(); - - let party_shares = (0..n) - .map(|i| { - (0..n) - .map(|j| { - let vec_j = &secret_shares_vec[j]; - vec_j[i].clone() - }) - .collect::>>() - }) - .collect::>>>(); - - let mut shared_keys_vec = Vec::new(); - let mut dlog_proof_vec = Vec::new(); - for i in 0..n { - let (shared_keys, dlog_proof) = party_keys_vec[i] - .phase2_verify_vss_construct_keypair_phase3_pok_dlog( - ¶mes, - &y_vec, - &party_shares[i], - &vss_scheme_vec, - (&index_vec[i] + 1).into(), - ) - .expect("invalid vss"); - shared_keys_vec.push(shared_keys); - dlog_proof_vec.push(dlog_proof); - } - - let pk_vec = (0..n) - .map(|i| dlog_proof_vec[i].pk.clone()) - .collect::>>(); - - //both parties run: - Keys::verify_dlog_proofs(¶mes, &dlog_proof_vec, &y_vec).expect("bad dlog proof"); - - //test - let xi_vec = (0..=t) - .map(|i| shared_keys_vec[i].x_i.clone()) - .collect::>>(); - let x = vss_scheme_for_test[0] - .clone() - .reconstruct(&index_vec[0..=t], &xi_vec); - let sum_u_i = party_keys_vec - .iter() - .fold(Scalar::::zero(), |acc, x| acc + &x.u_i); - assert_eq!(x, sum_u_i); - - ( - party_keys_vec, - shared_keys_vec, - pk_vec, - y_sum, - vss_scheme_for_test[0].clone(), - ) - } - - criterion_group! { - name = keygen; - config = Criterion::default().sample_size(10); - targets = - self::bench_full_keygen_party_one_two, - self::bench_full_keygen_party_two_three} -} - -criterion_main!(bench::keygen); diff --git a/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/keygen.rs b/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/keygen.rs deleted file mode 100644 index e6ac17d8..00000000 --- a/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/keygen.rs +++ /dev/null @@ -1,62 +0,0 @@ -use criterion::criterion_main; - -mod bench { - use criterion::{criterion_group, Criterion}; - use curv::arithmetic::Converter; - use curv::elliptic::curves::{Scalar, Secp256k1}; - use curv::BigInt; - use multi_party_ecdsa::protocols::two_party_ecdsa::cclst_2019::{party_one, party_two}; - - pub fn bench_full_keygen_party_one_two(c: &mut Criterion) { - c.bench_function("keygen", move |b| { - b.iter(|| { - - let (party_one_first_message, comm_witness, ec_key_pair_party1) = - party_one::KeyGenFirstMsg::create_commitments_with_fixed_secret_share( - Scalar::::random(), - ); - let (party_two_first_message, _ec_key_pair_party2) = - party_two::KeyGenFirstMsg::create_with_fixed_secret_share(Scalar::::from(&BigInt::from( - 10, - ))); - let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( - comm_witness, - &party_two_first_message.d_log_proof, - ) - .expect("failed to verify and decommit"); - - let _party_two_second_message = party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( - &party_one_first_message, - &party_one_second_message, - ) - .expect("failed to verify commitments and DLog proof"); - - // init HSMCL keypair: - let seed = BigInt::from_str_radix("314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848", 10).unwrap(); - let (hsmcl, hsmcl_public) = party_one::HSMCL::generate_keypair_and_encrypted_share_and_proof( - &ec_key_pair_party1, - &seed, - ); - - //P1 sends P2 hsmcl_public - let _party_one_private = party_one::Party1Private::set_private_key(&ec_key_pair_party1, &hsmcl); - - let _party_two_hsmcl_pub = party_two::Party2Public::verify_setup_and_zkcldl_proof( - &hsmcl_public, - &seed, - &party_one_second_message.comm_witness.public_share, - ) - .expect("proof error"); - - - }) - }); - } - - criterion_group! { - name = keygen; - config = Criterion::default().sample_size(10); - targets =self::bench_full_keygen_party_one_two} -} - -criterion_main!(bench::keygen); diff --git a/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/sign.rs b/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/sign.rs deleted file mode 100644 index c108ae7e..00000000 --- a/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/sign.rs +++ /dev/null @@ -1,90 +0,0 @@ -use criterion::criterion_main; - -mod bench { - use criterion::{criterion_group, Criterion}; - use curv::arithmetic::Converter; - use curv::BigInt; - use multi_party_ecdsa::protocols::two_party_ecdsa::cclst_2019::*; - - pub fn bench_full_sign_party_one_two(c: &mut Criterion) { - c.bench_function("sign", move |b| { - b.iter(|| { - ////////// Simulate KeyGen ///////////////// - // assume party1 and party2 engaged with KeyGen in the past resulting in - // party1 owning private share and HSMCL key-pair - // party2 owning private share and HSMCL encryption of party1 share - let (_party_one_private_share_gen, comm_witness, ec_key_pair_party1) = - party_one::KeyGenFirstMsg::create_commitments(); - let (party_two_private_share_gen, ec_key_pair_party2) = party_two::KeyGenFirstMsg::create(); - - //pi (nothing up my sleeve) - let seed= BigInt::from_str_radix("314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848", 10).unwrap(); - - let (party_one_hsmcl, hsmcl_public) = - party_one::HSMCL::generate_keypair_and_encrypted_share_and_proof( - &ec_key_pair_party1, - &seed, - ); - - let party1_private = - party_one::Party1Private::set_private_key(&ec_key_pair_party1, &party_one_hsmcl); - - let party_two_hsmcl_pub = party_two::Party2Public::verify_setup_and_zkcldl_proof( - &hsmcl_public, - &seed, - &comm_witness.public_share, - ) - .expect("proof error"); - - ////////// Start Signing ///////////////// - // creating the ephemeral private shares: - - let (eph_party_two_first_message, eph_comm_witness, eph_ec_key_pair_party2) = - party_two::EphKeyGenFirstMsg::create_commitments(); - let (eph_party_one_first_message, eph_ec_key_pair_party1) = - party_one::EphKeyGenFirstMsg::create(); - let eph_party_two_second_message = party_two::EphKeyGenSecondMsg::verify_and_decommit( - eph_comm_witness, - &eph_party_one_first_message, - ) - .expect("party1 DLog proof failed"); - - let _eph_party_one_second_message = - party_one::EphKeyGenSecondMsg::verify_commitments_and_dlog_proof( - &eph_party_two_first_message, - &eph_party_two_second_message, - ) - .expect("failed to verify commitments and DLog proof"); - let party2_private = party_two::Party2Private::set_private_key(&ec_key_pair_party2); - let message = BigInt::from(1234); - - let partial_sig = party_two::PartialSig::compute( - party_two_hsmcl_pub, - &party2_private, - &eph_ec_key_pair_party2, - &eph_party_one_first_message.public_share, - &message, - ); - - let signature = party_one::Signature::compute( - &party_one_hsmcl, - &party1_private, - partial_sig.c3, - &eph_ec_key_pair_party1, - &eph_party_two_second_message.comm_witness.public_share, - ); - - let pubkey = - party_one::compute_pubkey(&party1_private, &party_two_private_share_gen.public_share); - party_one::verify(&signature, &pubkey, &message).expect("Invalid signature") - }) - }); - } - - criterion_group! { - name = sign; - config = Criterion::default().sample_size(10); - targets =self::bench_full_sign_party_one_two} -} - -criterion_main!(bench::sign); diff --git a/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/keygen.rs b/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/keygen.rs deleted file mode 100644 index 412ff75c..00000000 --- a/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/keygen.rs +++ /dev/null @@ -1,81 +0,0 @@ -use criterion::criterion_main; - -mod bench { - use criterion::{criterion_group, Criterion}; - use curv::arithmetic::traits::Samplable; - use curv::elliptic::curves::{secp256_k1::Secp256k1, Scalar}; - use curv::BigInt; - use multi_party_ecdsa::protocols::two_party_ecdsa::lindell_2017::*; - - pub fn bench_full_keygen_party_one_two(c: &mut Criterion) { - c.bench_function("keygen", move |b| { - b.iter(|| { - let (party_one_first_message, comm_witness, ec_key_pair_party1) = - party_one::KeyGenFirstMsg::create_commitments_with_fixed_secret_share( - Scalar::::from(&BigInt::sample(253)), - ); - let (party_two_first_message, _ec_key_pair_party2) = - party_two::KeyGenFirstMsg::create_with_fixed_secret_share( - Scalar::::from(&BigInt::from(10)), - ); - let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( - comm_witness, - &party_two_first_message.d_log_proof, - ) - .expect("failed to verify and decommit"); - - let _party_two_second_message = - party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( - &party_one_first_message, - &party_one_second_message, - ) - .expect("failed to verify commitments and DLog proof"); - - // init paillier keypair: - let paillier_key_pair = - party_one::PaillierKeyPair::generate_keypair_and_encrypted_share( - &ec_key_pair_party1, - ); - - let party_one_private = party_one::Party1Private::set_private_key( - &ec_key_pair_party1, - &paillier_key_pair, - ); - - let party_two_paillier = party_two::PaillierPublic { - ek: paillier_key_pair.ek.clone(), - encrypted_secret_share: paillier_key_pair.encrypted_share.clone(), - }; - - // zk proof of correct paillier key - let correct_key_proof = - party_one::PaillierKeyPair::generate_ni_proof_correct_key(&paillier_key_pair); - party_two::PaillierPublic::verify_ni_proof_correct_key( - correct_key_proof, - &party_two_paillier.ek, - ) - .expect("bad paillier key"); - - //zk_pdl - - let (pdl_statement, pdl_proof, composite_dlog_proof) = - party_one::PaillierKeyPair::pdl_proof(&party_one_private, &paillier_key_pair); - party_two::PaillierPublic::pdl_verify( - &composite_dlog_proof, - &pdl_statement, - &pdl_proof, - &party_two_paillier, - &party_one_second_message.comm_witness.public_share, - ) - .expect("PDL error"); - }) - }); - } - - criterion_group! { - name = keygen; - config = Criterion::default().sample_size(10); - targets =self::bench_full_keygen_party_one_two} -} - -criterion_main!(bench::keygen); diff --git a/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/sign.rs b/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/sign.rs deleted file mode 100644 index d93eb9a9..00000000 --- a/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/sign.rs +++ /dev/null @@ -1,75 +0,0 @@ -use criterion::criterion_main; - -mod bench { - use criterion::{criterion_group, Criterion}; - use curv::BigInt; - use multi_party_ecdsa::protocols::two_party_ecdsa::lindell_2017::*; - - pub fn bench_full_sign_party_one_two(c: &mut Criterion) { - c.bench_function("sign", move |b| { - b.iter(|| { - let (_party_one_private_share_gen, _comm_witness, ec_key_pair_party1) = - party_one::KeyGenFirstMsg::create_commitments(); - let (party_two_private_share_gen, ec_key_pair_party2) = - party_two::KeyGenFirstMsg::create(); - - let keypair = party_one::PaillierKeyPair::generate_keypair_and_encrypted_share( - &ec_key_pair_party1, - ); - - // creating the ephemeral private shares: - - let (eph_party_two_first_message, eph_comm_witness, eph_ec_key_pair_party2) = - party_two::EphKeyGenFirstMsg::create_commitments(); - let (eph_party_one_first_message, eph_ec_key_pair_party1) = - party_one::EphKeyGenFirstMsg::create(); - let eph_party_two_second_message = - party_two::EphKeyGenSecondMsg::verify_and_decommit( - eph_comm_witness, - &eph_party_one_first_message, - ) - .expect("party1 DLog proof failed"); - - let _eph_party_one_second_message = - party_one::EphKeyGenSecondMsg::verify_commitments_and_dlog_proof( - &eph_party_two_first_message, - &eph_party_two_second_message, - ) - .expect("failed to verify commitments and DLog proof"); - let party2_private = party_two::Party2Private::set_private_key(&ec_key_pair_party2); - let message = BigInt::from(1234); - let partial_sig = party_two::PartialSig::compute( - &keypair.ek, - &keypair.encrypted_share, - &party2_private, - &eph_ec_key_pair_party2, - &eph_party_one_first_message.public_share, - &message, - ); - - let party1_private = - party_one::Party1Private::set_private_key(&ec_key_pair_party1, &keypair); - - let signature = party_one::Signature::compute( - &party1_private, - &partial_sig.c3, - &eph_ec_key_pair_party1, - &eph_party_two_second_message.comm_witness.public_share, - ); - - let pubkey = party_one::compute_pubkey( - &party1_private, - &party_two_private_share_gen.public_share, - ); - party_one::verify(&signature, &pubkey, &message).expect("Invalid signature") - }) - }); - } - - criterion_group! { - name = sign; - config = Criterion::default().sample_size(10); - targets =self::bench_full_sign_party_one_two} -} - -criterion_main!(bench::sign); From ae54b6807e1d31b8563a7d734e0ce4d6e71c698b Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 3 Oct 2023 08:45:53 +0800 Subject: [PATCH 25/41] Remove obsolete demo folder. --- multi-party-ecdsa/demo/MP-ECDSA demo.gif | Bin 306714 -> 0 bytes multi-party-ecdsa/demo/run.sh | 40 ----------------------- 2 files changed, 40 deletions(-) delete mode 100644 multi-party-ecdsa/demo/MP-ECDSA demo.gif delete mode 100755 multi-party-ecdsa/demo/run.sh diff --git a/multi-party-ecdsa/demo/MP-ECDSA demo.gif b/multi-party-ecdsa/demo/MP-ECDSA demo.gif deleted file mode 100644 index 10cf2261b8d704b1fc8d6c1b15692f35bf8d4fcd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 306714 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�Um?^npjbUE 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� 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�u(WBz!}!^D*5@jz6^# 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

Sg+i>Z5ST+oeXa_Q*G=F7i0)PTg!LC7g9- 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 literal 0 HcmV?d00001 diff --git a/multi-party-ecdsa/benches/multi_party_ecdsa/gg18/keygen.rs b/multi-party-ecdsa/benches/multi_party_ecdsa/gg18/keygen.rs new file mode 100644 index 00000000..061721e2 --- /dev/null +++ b/multi-party-ecdsa/benches/multi_party_ecdsa/gg18/keygen.rs @@ -0,0 +1,135 @@ +use criterion::criterion_main; + +mod bench { + use criterion::{criterion_group, Criterion}; + use curv::cryptographic_primitives::secret_sharing::feldman_vss::VerifiableSS; + use curv::elliptic::curves::{secp256_k1::Secp256k1, Point, Scalar}; + use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2018::party_i::*; + pub fn bench_full_keygen_party_one_two(c: &mut Criterion) { + c.bench_function("keygen t=1 n=2", move |b| { + b.iter(|| { + keygen_t_n_parties(1, 2); + }) + }); + } + pub fn bench_full_keygen_party_two_three(c: &mut Criterion) { + c.bench_function("keygen t=2 n=3", move |b| { + b.iter(|| { + keygen_t_n_parties(2, 3); + }) + }); + } + pub fn keygen_t_n_parties( + t: u16, + n: u16, + ) -> ( + Vec, + Vec>, + Vec>, + Point, + VerifiableSS, + ) { + let parames = Parameters { + threshold: t, + share_count: n, + }; + let (t, n) = (t as usize, n as usize); + let party_keys_vec = (0..n) + .map(|i| Keys::create(i as u16)) + .collect::>(); + + let mut bc1_vec = Vec::new(); + let mut decom_vec = Vec::new(); + + for key in &party_keys_vec { + let (bc1, decom1) = key.phase1_broadcast_phase3_proof_of_correct_key(); + bc1_vec.push(bc1); + decom_vec.push(decom1); + } + + let y_vec = (0..n) + .map(|i| decom_vec[i].y_i.clone()) + .collect::>>(); + let mut y_vec_iter = y_vec.iter(); + let head = y_vec_iter.next().unwrap(); + let tail = y_vec_iter; + let y_sum = tail.fold(head.clone(), |acc, x| acc + x); + let mut vss_scheme_vec = Vec::new(); + let mut secret_shares_vec = Vec::new(); + let mut index_vec = Vec::new(); + for key in &party_keys_vec { + let (vss_scheme, secret_shares, index) = key + .phase1_verify_com_phase3_verify_correct_key_phase2_distribute( + ¶mes, &decom_vec, &bc1_vec, + ) + .expect("invalid key"); + vss_scheme_vec.push(vss_scheme); + secret_shares_vec.push(secret_shares); + index_vec.push(index as u16); + } + let vss_scheme_for_test = vss_scheme_vec.clone(); + + let party_shares = (0..n) + .map(|i| { + (0..n) + .map(|j| { + let vec_j = &secret_shares_vec[j]; + vec_j[i].clone() + }) + .collect::>>() + }) + .collect::>>>(); + + let mut shared_keys_vec = Vec::new(); + let mut dlog_proof_vec = Vec::new(); + for i in 0..n { + let (shared_keys, dlog_proof) = party_keys_vec[i] + .phase2_verify_vss_construct_keypair_phase3_pok_dlog( + ¶mes, + &y_vec, + &party_shares[i], + &vss_scheme_vec, + (&index_vec[i] + 1).into(), + ) + .expect("invalid vss"); + shared_keys_vec.push(shared_keys); + dlog_proof_vec.push(dlog_proof); + } + + let pk_vec = (0..n) + .map(|i| dlog_proof_vec[i].pk.clone()) + .collect::>>(); + + //both parties run: + Keys::verify_dlog_proofs(¶mes, &dlog_proof_vec, &y_vec).expect("bad dlog proof"); + + //test + let xi_vec = (0..=t) + .map(|i| shared_keys_vec[i].x_i.clone()) + .collect::>>(); + let x = vss_scheme_for_test[0] + .clone() + .reconstruct(&index_vec[0..=t], &xi_vec); + let sum_u_i = party_keys_vec + .iter() + .fold(Scalar::::zero(), |acc, x| acc + &x.u_i); + assert_eq!(x, sum_u_i); + + ( + party_keys_vec, + shared_keys_vec, + pk_vec, + y_sum, + vss_scheme_for_test[0].clone(), + ) + } + + criterion_group! { + name = keygen; + config = Criterion::default().sample_size(10); + targets = + self::bench_full_keygen_party_one_two, + self::bench_full_keygen_party_two_three} +} + +criterion_main!(bench::keygen); diff --git a/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/keygen.rs b/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/keygen.rs new file mode 100644 index 00000000..e6ac17d8 --- /dev/null +++ b/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/keygen.rs @@ -0,0 +1,62 @@ +use criterion::criterion_main; + +mod bench { + use criterion::{criterion_group, Criterion}; + use curv::arithmetic::Converter; + use curv::elliptic::curves::{Scalar, Secp256k1}; + use curv::BigInt; + use multi_party_ecdsa::protocols::two_party_ecdsa::cclst_2019::{party_one, party_two}; + + pub fn bench_full_keygen_party_one_two(c: &mut Criterion) { + c.bench_function("keygen", move |b| { + b.iter(|| { + + let (party_one_first_message, comm_witness, ec_key_pair_party1) = + party_one::KeyGenFirstMsg::create_commitments_with_fixed_secret_share( + Scalar::::random(), + ); + let (party_two_first_message, _ec_key_pair_party2) = + party_two::KeyGenFirstMsg::create_with_fixed_secret_share(Scalar::::from(&BigInt::from( + 10, + ))); + let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( + comm_witness, + &party_two_first_message.d_log_proof, + ) + .expect("failed to verify and decommit"); + + let _party_two_second_message = party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( + &party_one_first_message, + &party_one_second_message, + ) + .expect("failed to verify commitments and DLog proof"); + + // init HSMCL keypair: + let seed = BigInt::from_str_radix("314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848", 10).unwrap(); + let (hsmcl, hsmcl_public) = party_one::HSMCL::generate_keypair_and_encrypted_share_and_proof( + &ec_key_pair_party1, + &seed, + ); + + //P1 sends P2 hsmcl_public + let _party_one_private = party_one::Party1Private::set_private_key(&ec_key_pair_party1, &hsmcl); + + let _party_two_hsmcl_pub = party_two::Party2Public::verify_setup_and_zkcldl_proof( + &hsmcl_public, + &seed, + &party_one_second_message.comm_witness.public_share, + ) + .expect("proof error"); + + + }) + }); + } + + criterion_group! { + name = keygen; + config = Criterion::default().sample_size(10); + targets =self::bench_full_keygen_party_one_two} +} + +criterion_main!(bench::keygen); diff --git a/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/sign.rs b/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/sign.rs new file mode 100644 index 00000000..c108ae7e --- /dev/null +++ b/multi-party-ecdsa/benches/two_party_ecdsa/cclst_2019/sign.rs @@ -0,0 +1,90 @@ +use criterion::criterion_main; + +mod bench { + use criterion::{criterion_group, Criterion}; + use curv::arithmetic::Converter; + use curv::BigInt; + use multi_party_ecdsa::protocols::two_party_ecdsa::cclst_2019::*; + + pub fn bench_full_sign_party_one_two(c: &mut Criterion) { + c.bench_function("sign", move |b| { + b.iter(|| { + ////////// Simulate KeyGen ///////////////// + // assume party1 and party2 engaged with KeyGen in the past resulting in + // party1 owning private share and HSMCL key-pair + // party2 owning private share and HSMCL encryption of party1 share + let (_party_one_private_share_gen, comm_witness, ec_key_pair_party1) = + party_one::KeyGenFirstMsg::create_commitments(); + let (party_two_private_share_gen, ec_key_pair_party2) = party_two::KeyGenFirstMsg::create(); + + //pi (nothing up my sleeve) + let seed= BigInt::from_str_radix("314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848", 10).unwrap(); + + let (party_one_hsmcl, hsmcl_public) = + party_one::HSMCL::generate_keypair_and_encrypted_share_and_proof( + &ec_key_pair_party1, + &seed, + ); + + let party1_private = + party_one::Party1Private::set_private_key(&ec_key_pair_party1, &party_one_hsmcl); + + let party_two_hsmcl_pub = party_two::Party2Public::verify_setup_and_zkcldl_proof( + &hsmcl_public, + &seed, + &comm_witness.public_share, + ) + .expect("proof error"); + + ////////// Start Signing ///////////////// + // creating the ephemeral private shares: + + let (eph_party_two_first_message, eph_comm_witness, eph_ec_key_pair_party2) = + party_two::EphKeyGenFirstMsg::create_commitments(); + let (eph_party_one_first_message, eph_ec_key_pair_party1) = + party_one::EphKeyGenFirstMsg::create(); + let eph_party_two_second_message = party_two::EphKeyGenSecondMsg::verify_and_decommit( + eph_comm_witness, + &eph_party_one_first_message, + ) + .expect("party1 DLog proof failed"); + + let _eph_party_one_second_message = + party_one::EphKeyGenSecondMsg::verify_commitments_and_dlog_proof( + &eph_party_two_first_message, + &eph_party_two_second_message, + ) + .expect("failed to verify commitments and DLog proof"); + let party2_private = party_two::Party2Private::set_private_key(&ec_key_pair_party2); + let message = BigInt::from(1234); + + let partial_sig = party_two::PartialSig::compute( + party_two_hsmcl_pub, + &party2_private, + &eph_ec_key_pair_party2, + &eph_party_one_first_message.public_share, + &message, + ); + + let signature = party_one::Signature::compute( + &party_one_hsmcl, + &party1_private, + partial_sig.c3, + &eph_ec_key_pair_party1, + &eph_party_two_second_message.comm_witness.public_share, + ); + + let pubkey = + party_one::compute_pubkey(&party1_private, &party_two_private_share_gen.public_share); + party_one::verify(&signature, &pubkey, &message).expect("Invalid signature") + }) + }); + } + + criterion_group! { + name = sign; + config = Criterion::default().sample_size(10); + targets =self::bench_full_sign_party_one_two} +} + +criterion_main!(bench::sign); diff --git a/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/keygen.rs b/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/keygen.rs new file mode 100644 index 00000000..412ff75c --- /dev/null +++ b/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/keygen.rs @@ -0,0 +1,81 @@ +use criterion::criterion_main; + +mod bench { + use criterion::{criterion_group, Criterion}; + use curv::arithmetic::traits::Samplable; + use curv::elliptic::curves::{secp256_k1::Secp256k1, Scalar}; + use curv::BigInt; + use multi_party_ecdsa::protocols::two_party_ecdsa::lindell_2017::*; + + pub fn bench_full_keygen_party_one_two(c: &mut Criterion) { + c.bench_function("keygen", move |b| { + b.iter(|| { + let (party_one_first_message, comm_witness, ec_key_pair_party1) = + party_one::KeyGenFirstMsg::create_commitments_with_fixed_secret_share( + Scalar::::from(&BigInt::sample(253)), + ); + let (party_two_first_message, _ec_key_pair_party2) = + party_two::KeyGenFirstMsg::create_with_fixed_secret_share( + Scalar::::from(&BigInt::from(10)), + ); + let party_one_second_message = party_one::KeyGenSecondMsg::verify_and_decommit( + comm_witness, + &party_two_first_message.d_log_proof, + ) + .expect("failed to verify and decommit"); + + let _party_two_second_message = + party_two::KeyGenSecondMsg::verify_commitments_and_dlog_proof( + &party_one_first_message, + &party_one_second_message, + ) + .expect("failed to verify commitments and DLog proof"); + + // init paillier keypair: + let paillier_key_pair = + party_one::PaillierKeyPair::generate_keypair_and_encrypted_share( + &ec_key_pair_party1, + ); + + let party_one_private = party_one::Party1Private::set_private_key( + &ec_key_pair_party1, + &paillier_key_pair, + ); + + let party_two_paillier = party_two::PaillierPublic { + ek: paillier_key_pair.ek.clone(), + encrypted_secret_share: paillier_key_pair.encrypted_share.clone(), + }; + + // zk proof of correct paillier key + let correct_key_proof = + party_one::PaillierKeyPair::generate_ni_proof_correct_key(&paillier_key_pair); + party_two::PaillierPublic::verify_ni_proof_correct_key( + correct_key_proof, + &party_two_paillier.ek, + ) + .expect("bad paillier key"); + + //zk_pdl + + let (pdl_statement, pdl_proof, composite_dlog_proof) = + party_one::PaillierKeyPair::pdl_proof(&party_one_private, &paillier_key_pair); + party_two::PaillierPublic::pdl_verify( + &composite_dlog_proof, + &pdl_statement, + &pdl_proof, + &party_two_paillier, + &party_one_second_message.comm_witness.public_share, + ) + .expect("PDL error"); + }) + }); + } + + criterion_group! { + name = keygen; + config = Criterion::default().sample_size(10); + targets =self::bench_full_keygen_party_one_two} +} + +criterion_main!(bench::keygen); diff --git a/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/sign.rs b/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/sign.rs new file mode 100644 index 00000000..d93eb9a9 --- /dev/null +++ b/multi-party-ecdsa/benches/two_party_ecdsa/lindell_2017/sign.rs @@ -0,0 +1,75 @@ +use criterion::criterion_main; + +mod bench { + use criterion::{criterion_group, Criterion}; + use curv::BigInt; + use multi_party_ecdsa::protocols::two_party_ecdsa::lindell_2017::*; + + pub fn bench_full_sign_party_one_two(c: &mut Criterion) { + c.bench_function("sign", move |b| { + b.iter(|| { + let (_party_one_private_share_gen, _comm_witness, ec_key_pair_party1) = + party_one::KeyGenFirstMsg::create_commitments(); + let (party_two_private_share_gen, ec_key_pair_party2) = + party_two::KeyGenFirstMsg::create(); + + let keypair = party_one::PaillierKeyPair::generate_keypair_and_encrypted_share( + &ec_key_pair_party1, + ); + + // creating the ephemeral private shares: + + let (eph_party_two_first_message, eph_comm_witness, eph_ec_key_pair_party2) = + party_two::EphKeyGenFirstMsg::create_commitments(); + let (eph_party_one_first_message, eph_ec_key_pair_party1) = + party_one::EphKeyGenFirstMsg::create(); + let eph_party_two_second_message = + party_two::EphKeyGenSecondMsg::verify_and_decommit( + eph_comm_witness, + &eph_party_one_first_message, + ) + .expect("party1 DLog proof failed"); + + let _eph_party_one_second_message = + party_one::EphKeyGenSecondMsg::verify_commitments_and_dlog_proof( + &eph_party_two_first_message, + &eph_party_two_second_message, + ) + .expect("failed to verify commitments and DLog proof"); + let party2_private = party_two::Party2Private::set_private_key(&ec_key_pair_party2); + let message = BigInt::from(1234); + let partial_sig = party_two::PartialSig::compute( + &keypair.ek, + &keypair.encrypted_share, + &party2_private, + &eph_ec_key_pair_party2, + &eph_party_one_first_message.public_share, + &message, + ); + + let party1_private = + party_one::Party1Private::set_private_key(&ec_key_pair_party1, &keypair); + + let signature = party_one::Signature::compute( + &party1_private, + &partial_sig.c3, + &eph_ec_key_pair_party1, + &eph_party_two_second_message.comm_witness.public_share, + ); + + let pubkey = party_one::compute_pubkey( + &party1_private, + &party_two_private_share_gen.public_share, + ); + party_one::verify(&signature, &pubkey, &message).expect("Invalid signature") + }) + }); + } + + criterion_group! { + name = sign; + config = Criterion::default().sample_size(10); + targets =self::bench_full_sign_party_one_two} +} + +criterion_main!(bench::sign); diff --git a/multi-party-ecdsa/demo/MP-ECDSA demo.gif b/multi-party-ecdsa/demo/MP-ECDSA demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..10cf2261b8d704b1fc8d6c1b15692f35bf8d4fcd GIT binary patch literal 306714 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�Um?^npjbUE z`l&MD2tqEA3ircPqFHoE$VU=jfP&^s6O00)Mxqcoylm=3(x8$!3g@dVC632*h*fad zUFG>?0^=@-F#mc={vuw8o=oqlXl7|;_SE-~5hO#vD-7@1VUKUH}L+sewj6^k=$R3G$Wk2IRX?KwmKT!c%6+3$6?tx{SEOB5s zRlNGNxJnwDZF#R^qOa$NC_7qy#36IAkU0K>rWgHFdG_l%qbQ<}=f{Kni6<#9;lvN< zH*iPrCFp>f>RZ3CW1uJt<3Q4-04BE&6o?RA!3D)HR9jsc*FRvqj>CP(pu$;7;z*(b ze&TvewyuL793bKb!L@~IItgE)Rg@qxINA&%*Vjf#<`#$IoK;pmOHyN_@6^t$it+rA+d3N^1w^_lrda7RCl|F1%+Mr&t>VK6z)!*W z5=P4f$CM>@`~`|lIt56e%PB2rfF<-pjee@hf#i`$j)4Ktyz>*J+}IJkbZLYoj>OCm zJ@Bgj%*l~muRDFZnp z7Kn1*Fqnm(NNZmT{`94TsO&?5Pe%J}b!}ADGq$=vKlrrOv-4PPyBD|C$|}gG*Aa8v zEh9WeB=BLzd%ne9>Sww!w;5&7M6qDF6^~nj^glCkhCI?ckZX8kt0rHF5fMAV%7qzk zJ<5evBYvq)GO$X(@{$H;C2~u&NzhemR~&ya1?$I~!&W~~Y~njbixcH^A`I|O*jx(& z95jcgjBU}R$s-q!nY?gfL;L^U2g+T5Fa9zHexQ8~M6FY_V60=glWH{tUP2;a7 z@5A{fPSQv93&6&1RzjE#Othh!a+0e~3l=yUu|vOQ!C$Ti3N^#Y2>IObf5810ES6+{ zdHBorY9XI1NtCTqX>f^+DlrIx!*>|p;c+Mw0ji@772D>lxH7!mCx!p1sK0hj4=Rb+ zJ;aWU8$v!mk-*$Gd|I#D^99+u^>e3(Z(CXf<9Sc6x<6V(mt7J_^Q-e1sAeHM?wr`Cvn=23a~>(*%Mo;jFV1CMI;V6Q@N&_WvoUoLLnK%$Vh4C)Xph8vcP0L)8*4AOq^9hdBw%riXe6nn zE~mxn$9OS%s>VRonqhgwOqmtaaSE*A!r^*O-lRNbBTgI)yMx>r;_KlD)Er9Ahf`S0 zKKkD!=%ilLG85yOIbobz0EyIpRd8LYMFhUgCVg3E+!1C|S|%(Z#*U`5;J9$>CmzX& z_nO$>oMl&a(+@S+fp020WM8~u0!EDhYizWrI%f}UD+Dro#4XEKxiQ*cK9Nf)vzFnh z1Mt8Hz>t8avD&L@UXJ6*Myn({wiR$;)PzI~aLzwkSD{m_Ohw+3>-P6n6&!i#Y0(r7N8^Z{$c`}C80!$YxgnhvnyqGtvQOkgT6Mqk1 z+{7o8O&2qMb4P#RU$%;_<1*9FsW$o|@U)ih?Yiy`2J=tF-xekSJog9??DE4`+}f8) z1fo3OERcYR=Bx-goABs>a+h01hIYHUe3R$19#Q;tdWEAP(W|-4(?a#)@23uK?;-C9 zQTdl{Ctpy)$*|F85#9bQpjZuiHUjC0^fm5FG0W%iBSB>V0RX5vh|yIY@ZN}TmaL}Z z6Or*I)sH@^iunt<@d}_qxEz0Dm#wq>%Y^b{$82mb%nz+fwqUW1P9FJ{Gh;TbWDmMS z=9ulEW3WDgN(VW|y(<2|-I)Y^a#_k`PSOF!33LMPSw2xP<$d>`CeLDKtrYKke|&o5 z5u={cD~M<^S&z;hsgIcIw#T&ZH-7mtNtzO~qn~os{=0(4vQg~8pP14?%(2!%HHz3x z2+LC$2UdRhk$r<~g%5aIi!8m0J>z@?0fn@z%7F&5*n8tj9I%v0tjjRUa{3p(F`M5&2=en$>hJI7K*dWT8S{xr}`dY71J=u(?7fAPtognGf9Uj%#_7w$Apu$+=X{vUn;8}HQmpcJgi!?(8J*0ElJ{SaIw#Gi!S1$`48b#W-CP zu&%%RSLc6|j&lB5d{uPO=jk7(8`tWP^wASQ;v{D$Obr!4iDmPR7PWSEB>F0R3w8%=Lq}}Q>XUD z^RlvFZ;5c>p}F{cercV?_Hz({_&XhpW5i)47y}TphTxslx zpqrdyBacXQ>&cTBC=hQi9cgSDl>$>XmGvT>9ENErzWH6bk;g8tJQ7aWp*=K& z29l~EgHhd<37n44=KYn5G^9AYwUpYpHg!a-Yb;$cek&Cn#gfr0Ak|N4c&@|KdRk%M z@QeK&pGA%V$yl>*G+j74?kU=WClvrw@h74{ZTcZZI9MJFGJ(*Z1HpGsQy3sP8&UGn zDLou;4%W{G*NIZU&F_Xqd!h$pblgWqaFVeEkcs-1v|fs|y|`F1eppS6g;Sr9Kx787 zNnq~*CDLhO;i+e69soCm2?mzmyIB~Gjzhr}qG%`>z6``QA+)uDFQF{e*&y61B}dnt zNY60SKvd@YJ*4VE!Dv5%q-UKvwWz{<&{IZdV^m5pM#QAHR45R7G1S%wHzBnjrDH_N z*jpX0GNuVe*VUCd)de{!6G$N?FTFoC#&jlpYU=&|@l!LMIUm_F%H@#;a|=T`fM z6VRjWSgREAUcxU{@o2^HM|dmSo5t;P*cpUH77?o;Jbkfm5`H^CO!`pD9<{O*-yXzj7qU$B)m%O9pPU~k=z}A-12ZudC%-@ z9w0si6Ez%q_UMQl#Zs43U)jo*-%EVhItVWXF_tXNfDFrb+0IWKxotaR{_;ykx&W$ol{j?a5i&Xnhlj06R0WfvgLS0)6%ln56ytjy|RO`!9pbj?L8pvmOJULZ{$q zM!LUw=CN8nFH78B1SMZ|Pmg*08Xa~=D&$%T`>u-1DE!&(^%V~(h_Alov)rrklQoM` zG{*N=paJ%dRQ~|3QvRUOU-^*GA4==mmm43g{|Kr&b&ZkZ|2wCweCuwe)9s?Cr!!P3?NbWtP3Ah89kE1wI(>UXF?u~=;wOuCKC5d z`8xO?D@FS!xht0o)=~|QH|-`r@)i?bHJD zEA4`!xG0|&RA1?9s+FI(xYQ`b*!pXlAsqbQ${0sM|G$j!j}iGlkTG~6Ac&B||AmYJ z31mb4Zz=}Ui2UzWjHUmm7_*Bn=l@H^C@K9{!&vypvI1-y2sIp87So$Uvp`^;AN|g~ z?G7$fK?CRn5EjEnp_#gI7C!_fp_6DvsU&^zBj1v#d_<8pQINe(zH!@!Ts)r#Rsa|?)RWH`| zu4>fUCMuQC*P>a#j3nPLKkY|iZ-{+RSolQ$1aB}YljT>$H9vhq6EpN80;D*+wZzhk zjgJJJ7#|zFiyo-=;i=W9|Ao8z3~DkA_XhtYBoIP~3B4P7mm*?l8hQyJAiW7vMMOZl zh#>?BReG1+K?DQ@1Pw)c2}MK!1q4J81w{ocY|c6FyK{DCXLn}bo&B)i^Cg+gJoodz zuKV`~Juhf4B={-3=rFbTM1Cvy^a^WOLj-wMlnc|S{cY_B`HVuv3G`24XTXLwN}RC| zFf|=rd~#E67U&~pnoBOXtaj5Gh*avIXTJRW>&Pp;%A0Cu{_cJhuDkV>r!40l^xIhP zM_wmGa$1Tq(4#~OZ|=$k-GK=8tp>F4EAvL>@-t9E_(Hr-uL=nvaPMF+i40AMwg`a| zt4pFa-V4c?*(V4WZNIx_G;)y$Z0MS0so|*x(N<5{D;ZOi>B$u+KhYT&ABUwko7MtBiQ~*zb6GEuVc0zNPi_$W^ggm5uhDKSp(oAmUFkBqT}^j z+meh(fTlSRx!*_kQNui$1-h!xSLr+0tGi@TneG zBY@TqcJY$M(Jcv(lfHz-v|A%%mEp)&kSAcImn@df(7B}nONm= zWC$RsBWO4b2v8)tWgCNayfHc=Tz!nRr_C7*B?7?p<>i0iL4GQ^tn{rMEa)q2N+?y< z5gI_^&O{e9Q$Rx?F2*)_#ztN(+*?rmnR$Bf<36%p?Ey%ajpCuY;d;Rg3q}}^=?XQM z_pt;)WWP-XfTIWO{&6&-bTU%b0_dlZJKnE-pYKJK%x8O9CEptkZRTz>6p=7h%MmInp$}QQBZaKSdW^ zeBcgqLF1+Vu#1<&%4jn+g4;<9TUD~ZlvNk{;*J6PI(HeE(M6zbHDmnpW&nu0wg{w1 ztIEZdD%}E&!7Nlb&YM2+4Gl29t`sZmwywttuEoJPfV?ZV^4HVkjqRqiQp{5+paw3Y z)4mqT2rgLk;g6oug{%~bgSli2AtL%WcVTe3u5{vJI(9T4!k@Yd3MGx7?{ha|T)tBl zlqGSnb&QP3p({b?($2;9SMfU($u61EVhevlUX>Su;rq88vA-0VNqoTqW%f zykz*K_(0>$B-q80w|R#?yWL02KaO>vsrW}>ZUGzp>^A9bXEdM*y~!SXNARvE?(zGu zU02#L8~|cjQeMVF?6uw`;}%-9_N7C60&N99UwDvp%TO;Pmtd;1+2E+;W)U#+Mk>&) zwXBzNTEqTx(uPr+Txf&E>H2yKmU2^L z6F4@{PeQ--J2EM`)QNq!2@rEH^|M4i7B)D&5~1J8#}IIV;~RFpEDcr*tJ;V2gT1EL#EBRv%^bM_rDVeS=n{Md(jT~Wew z;fbdadk>v*oD1K#>v>F4wOJCMuje`3xTBd8RELsj-ViFa!`l>m&wCFRrU!@gW$VRMArht94hsMF^`%c3XXhFGc!1*X_oW zJ%jZQ8dGq;Gj$TVESendk!;>;E@*d3R-N@h|MsvesCqshu3%Pp_57Raf~fSS*ws%r z@vc!%Wr=h`RX{1m=fY#Du_D;IHYmsxmex8C`;sO8)6dANaoq>PWrY>m_qBgF7}`LU z^7*S53j*YE82Mm|!6Ut!_XV_S^{nc@%WAH^r(k=eFG#}MNQ=URFg6YkEk^tfj`fN_ zV22)BJ?sz9dza4J^*me!X9YuaFvk*(gK%@kwN17W9F`yn#>WUwdf5gk18H z%s{rntQ;+sqma!b<6Vdhq=4UP)w<>FLShW1AmOrvH9~PG>B-V%{g?(O``KZcfv3jQ?90AJB&fwko@0a4BDRL{|byjOSk^7VGP>h z<^QKJ2F;vKTWwH+vPwCQm|@D|P{MJ-ZC+P&9Km6R%OfoxC|=7#THg)6T_4JjV7+8) z=P#<0$ib?o22tP{rwv4Rmcui(jEfn&pb~aN1e_n>$YFeyo-i5h@5#QguqH|MH#g(~ zDA@#xHW+|~k&_w-@GFe$0E$qp4NNz|CydhSUu7-N`oE&4`C-dcLHh1HN@b70h%=8w zOU;`;&|W0s?~38Ry)T1*74uD$UVd1~Ot(C_Kc{%(QGB^B)2In9-`hWKT?oa@8A}Id zvL$`6a)|@{`AVj^S8@JWhPXhcH~vSPlswTy<>atcC%Q8i+?#3h(9i@A9N(#|yvy{0 z-7-CFLv-zSwBpOEbAzIq1X8Eg++yz8CT4;SBHNr0#>$2>h}dnvNDW$>7~%je#kv6? zI&bUO00!cM^I|qd0Z`=t?c#7?Vx5K##0}tsr!8yJKmhX4D2Q!Uu@E;65ST0iVp-j= z6vDiJ_*d|auq5S1>csNb@N<(60J z%{J42tc2!Av5Z{ALjmJ7(KKsxAO*P9xqU4;oUv^f@}AviJCsgIj0&P(2C-P^3&qXf z#OTzTL5J;>ZYX6VbzmSqDN8*bNLy`RG@xUIig~`0j%v^Z(N4fMov~MkqS@ONP!*|) zxzP=D?0^ezj+i@+;Z~dGoPd_V8wV`JT^xY@=?w%dT>gnlKLW3enKV69hFdf|w$GNn zhzY`=s%`)381Ov)!yp2sLr_YzULf0U)FdVH&USrjDy-|s((~rCVsG@x2JY%yjZP|w z>%Qq9Z|!?vMM4=-K_1(MJ~0_!)U3=oF&!VCZ-N&BM^V`zeJE>XI;g(>9G0gU&NQzF zsDF=ybzDpQY{tV{`^}l zIZ~*s=vzTpKF-RZDKN-PkC*>2!AwEF%(Yle zxl?Coe_WZ~-u^%>9+Aj{t)eGN(H6d^uurRkuY0J{nEkp(pU_>AFvs%`OgkqHGi6bY z9#6y$K{YvawT8?H}_$TX!U`26ch5cc+5 z(;h#27pZ#(S~bS3fQ>?6rz$?I3+*RozKB1r+vT}@eNY?u>vPUc1Ma3*U0@zFL4S!E z+=)#Z-EtD3G=uwuqi2VPjB~7|z1fk#axx`KVR`bo+k|WtZJ~{r&ITqR=vCa$eKIf0 z5&t_=Y#O5h#b55k8AN1#?6vG|syAxxb?R)+J=Xp}#lx+PX$`E+%|VJ=XNrdq&ohx0 z;W0d)U9gAM6$qYKL8ayQOI*3y=?Xtf-t^+xu zpR^|k`~iWGq7VY8a~lap;TXh5^fW<%n7b$dgrssSsIuIK1iZoNkPURLy|iK@bIxV3 z3YsBT?L6=?ao6XgwJa#!4hkZgdUaw3V4S(!Ivj}5CIdhz&?Lp=AVC#Ah2buC*z<#a(uQ!`W9)ftiaSIkqR_2F{W{9YvkyfC@Sv>6)20=5<@=i=ja#fON*(8^4U`Dp1 zTQKEY*-&2V#n4qd{uh#`B~LzCgx^Edf?h+oMc_)-_w(UKZ%Y+2mJ1=NSO zf#>&c4<(M8SP;pXN&@~mi7zJ zaz(SFW|*UcV$g306Y`eSw83*7E$oGj`ig?dM0Mp>wTH0`-;M06v?@?xs{~#Nv^6oP z0`g#6PfTwWjX#wrH!JWU-%`nSNOfFeGZG!l>9%~kZuEouRvMjXJv#jw)f!{zqK74! z$D)m+dDf2f-~tLnj-U=+39dev8G$&|1VF_ozF5;1FsmaG{zfAjS9w764-$ro%fEzc zQSELxxBJBgMaM0th(Iwj*d(LMx3OGuwA&ph(Iz*Z)?%qMewSP1K;x#)g zn%Vfx?U9RohwQNP|CWgDio!jjIFJw|@ZBMDuupK|Txjo29KgjO&CuBp5bt=^ugA6h zk<;yaOsz!YAZ@f&^KBU4Op2bPD<(BeM@z5t9r}>SV`&?m1yxLDLsUYgse?*9H)HXU z6{qN@%<+t-=_yi}FhM&pgUk{>kLXgn$sN&=i&|8&uTnYmKgvu7TD&Bf+^2%8eJ^U^ zJkQ4uz*}M=kiLYI6O6s@DI$sZ=!jt)&wIQBG`MJ2F>u_j(93w=L`K@VpUk-9u zxXlrDj%kDQxpzP*Oy4Jk=TG$o0hgg=T zhdV;BK6{_9;R|NhB%a`O0DGciA6rTFkH+C!yQo#3S4UG9&B`?)N{3}!h;%(h`f^4e z1GjhcwV^FgtvzSA9Ey;Da(7JnE=N%?Mo|8xr0JSCl$h`XwD<{Zqs`@gqhQ+SeqD0!eH>gzNYd9hoY6r8+ys z2Jb>USO<5i^?QT3Cx1JNyHnFB`!7S^|Be>9{9}#l5&`XdciG|O7uQ1bX`-DxvGbY0 zPjJ`spclO-(RtS)cZPfZ#23e(-*$yDqnmFA%rfJZLY@ksz29WBfVt2yp4V6e3DTTs zXeXK(dmqIW4Nhk;$0)40LdBcG(q=XyKbRNAqA^^=yw}Vp;zY49qC_51>VLu*@-SlH zroA6~49%RDTXb;ujI6W1$aWshy6R?eAPF%eK?sImR~2AYtmX-aF;?UfImSYQI4v(N zQx@}SC;hcA{ohW3829gSdhoo6(1n z<9jGfzGc$P3W%#_U@O30IK&05f1Ov%RbcBH&DNYaT?LC#!B;qu54aLCxsbUiuxxJl z;z8`1Ka8c6X{{9GEsTOt;-D=IfD1b4dNi?>hA$+-`%Foz3Bh4Wwbh*65MZ3e9b(3F z>owDRGidI5KX=ayi`2%5vMkmUH#mt}q)%RYCqe>H8=0D!0ga*{OfF+y3rTPtEVC(Iu6bFrJh zVzd~~fB3fR;qse@?~Wg?au=_ksua^IS+;1!%K61RZ&ZqOSwb-&d?$<123iSQc1)??g%J`>yNxty-g0U!$cJ zO$npSCM|e2yJ`q7eeZmg)!S<9X1X3%hA=naTi}Gfc;mglznoWKAy?)80=iNnu^z^t zb0h1CSuMV++ONA-{ZusD(hg)j%5F`r3Jwho9}cw$^*k63gQ9a?M5OP4fz(z~e4Rk7 zU45EeZs=%j{I;K_kNEq{s zqySF5#36+^-EGD;U`N6$Uck^CcsJ86@3mDA7Y^UW+7!4sO_FAyCvZ)t;fxAFN&yTK zI~<)TUNwk}<#=jWOPfj3g*X*cx_~Dt4ju4-Dt0D&2O|dxM*v?L)95im!Ghzp8_H1{ zMQ8R@vA^BFK{`+x)eHjbVmm@_c6^1`$Sq;+*w@%F*hjp*(*_ObA3}Se`IcQo%THQj z3!fI;YzJJrj?DQgzsb1Y?EJb@Ysp<_`{~JET;0{01P8CS;xUv%xwvy5bNeDaUqAEX z)wBDGC>D7j9>dc4L(7EM#tYGiYCG^g#`=^{<{A*2;W!� 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�u(WBz!}!^D*5@jz6^# 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